summaryrefslogtreecommitdiffstats
path: root/src/libserver
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/libserver/CMakeLists.txt52
-rw-r--r--src/libserver/async_session.c364
-rw-r--r--src/libserver/async_session.h121
-rw-r--r--src/libserver/backtrace.cxx61
-rw-r--r--src/libserver/cfg_file.h889
-rw-r--r--src/libserver/cfg_file_private.h41
-rw-r--r--src/libserver/cfg_rcl.cxx4110
-rw-r--r--src/libserver/cfg_rcl.h476
-rw-r--r--src/libserver/cfg_utils.cxx2955
-rw-r--r--src/libserver/composites/composites.cxx989
-rw-r--r--src/libserver/composites/composites.h64
-rw-r--r--src/libserver/composites/composites_internal.hxx112
-rw-r--r--src/libserver/composites/composites_manager.cxx330
-rw-r--r--src/libserver/css/CMakeLists.txt9
-rw-r--r--src/libserver/css/css.cxx227
-rw-r--r--src/libserver/css/css.hxx68
-rw-r--r--src/libserver/css/css_colors_list.hxx738
-rw-r--r--src/libserver/css/css_parser.cxx892
-rw-r--r--src/libserver/css/css_parser.hxx244
-rw-r--r--src/libserver/css/css_property.cxx69
-rw-r--r--src/libserver/css/css_property.hxx172
-rw-r--r--src/libserver/css/css_rule.cxx531
-rw-r--r--src/libserver/css/css_rule.hxx153
-rw-r--r--src/libserver/css/css_rule_parser.rl27
-rw-r--r--src/libserver/css/css_selector.cxx226
-rw-r--r--src/libserver/css/css_selector.hxx134
-rw-r--r--src/libserver/css/css_selector_parser.rl27
-rw-r--r--src/libserver/css/css_style.hxx66
-rw-r--r--src/libserver/css/css_syntax.rl110
-rw-r--r--src/libserver/css/css_tokeniser.cxx836
-rw-r--r--src/libserver/css/css_tokeniser.hxx215
-rw-r--r--src/libserver/css/css_util.cxx157
-rw-r--r--src/libserver/css/css_util.hxx37
-rw-r--r--src/libserver/css/css_value.cxx449
-rw-r--r--src/libserver/css/css_value.hxx174
-rw-r--r--src/libserver/css/parse_error.hxx61
-rw-r--r--src/libserver/dkim.c3588
-rw-r--r--src/libserver/dkim.h298
-rw-r--r--src/libserver/dns.c1124
-rw-r--r--src/libserver/dns.h110
-rw-r--r--src/libserver/dynamic_cfg.c743
-rw-r--r--src/libserver/dynamic_cfg.h81
-rw-r--r--src/libserver/fuzzy_backend/fuzzy_backend.c560
-rw-r--r--src/libserver/fuzzy_backend/fuzzy_backend.h131
-rw-r--r--src/libserver/fuzzy_backend/fuzzy_backend_redis.c1666
-rw-r--r--src/libserver/fuzzy_backend/fuzzy_backend_redis.h67
-rw-r--r--src/libserver/fuzzy_backend/fuzzy_backend_sqlite.c1029
-rw-r--r--src/libserver/fuzzy_backend/fuzzy_backend_sqlite.h107
-rw-r--r--src/libserver/fuzzy_wire.h154
-rw-r--r--src/libserver/html/html.cxx2393
-rw-r--r--src/libserver/html/html.h137
-rw-r--r--src/libserver/html/html.hxx146
-rw-r--r--src/libserver/html/html_block.hxx358
-rw-r--r--src/libserver/html/html_entities.cxx2644
-rw-r--r--src/libserver/html/html_entities.hxx31
-rw-r--r--src/libserver/html/html_tag.hxx159
-rw-r--r--src/libserver/html/html_tag_defs.hxx194
-rw-r--r--src/libserver/html/html_tags.h176
-rw-r--r--src/libserver/html/html_tests.cxx304
-rw-r--r--src/libserver/html/html_url.cxx496
-rw-r--r--src/libserver/html/html_url.hxx68
-rw-r--r--src/libserver/http/http_connection.c2649
-rw-r--r--src/libserver/http/http_connection.h320
-rw-r--r--src/libserver/http/http_context.c670
-rw-r--r--src/libserver/http/http_context.h122
-rw-r--r--src/libserver/http/http_message.c725
-rw-r--r--src/libserver/http/http_message.h254
-rw-r--r--src/libserver/http/http_private.h129
-rw-r--r--src/libserver/http/http_router.c559
-rw-r--r--src/libserver/http/http_router.h149
-rw-r--r--src/libserver/http/http_util.c295
-rw-r--r--src/libserver/http/http_util.h47
-rw-r--r--src/libserver/hyperscan_tools.cxx627
-rw-r--r--src/libserver/hyperscan_tools.h77
-rw-r--r--src/libserver/logger.h403
-rw-r--r--src/libserver/logger/logger.c1319
-rw-r--r--src/libserver/logger/logger_console.c211
-rw-r--r--src/libserver/logger/logger_file.c510
-rw-r--r--src/libserver/logger/logger_private.h218
-rw-r--r--src/libserver/logger/logger_syslog.c143
-rw-r--r--src/libserver/maps/map.c3195
-rw-r--r--src/libserver/maps/map.h168
-rw-r--r--src/libserver/maps/map_helpers.c1845
-rw-r--r--src/libserver/maps/map_helpers.h269
-rw-r--r--src/libserver/maps/map_private.h226
-rw-r--r--src/libserver/mempool_vars_internal.h47
-rw-r--r--src/libserver/milter.c2232
-rw-r--r--src/libserver/milter.h188
-rw-r--r--src/libserver/milter_internal.h176
-rw-r--r--src/libserver/monitored.c735
-rw-r--r--src/libserver/monitored.h161
-rw-r--r--src/libserver/protocol.c2185
-rw-r--r--src/libserver/protocol.h130
-rw-r--r--src/libserver/protocol_internal.h99
-rw-r--r--src/libserver/re_cache.c2712
-rw-r--r--src/libserver/re_cache.h212
-rw-r--r--src/libserver/redis_pool.cxx663
-rw-r--r--src/libserver/redis_pool.h91
-rw-r--r--src/libserver/roll_history.c432
-rw-r--r--src/libserver/roll_history.h98
-rw-r--r--src/libserver/rspamd_control.c1334
-rw-r--r--src/libserver/rspamd_control.h328
-rw-r--r--src/libserver/rspamd_symcache.h578
-rw-r--r--src/libserver/spf.c2799
-rw-r--r--src/libserver/spf.h159
-rw-r--r--src/libserver/ssl_util.c1133
-rw-r--r--src/libserver/ssl_util.h120
-rw-r--r--src/libserver/symcache/symcache_c.cxx715
-rw-r--r--src/libserver/symcache/symcache_id_list.hxx95
-rw-r--r--src/libserver/symcache/symcache_impl.cxx1316
-rw-r--r--src/libserver/symcache/symcache_internal.hxx652
-rw-r--r--src/libserver/symcache/symcache_item.cxx652
-rw-r--r--src/libserver/symcache/symcache_item.hxx561
-rw-r--r--src/libserver/symcache/symcache_periodic.hxx89
-rw-r--r--src/libserver/symcache/symcache_runtime.cxx823
-rw-r--r--src/libserver/symcache/symcache_runtime.hxx209
-rw-r--r--src/libserver/task.c1975
-rw-r--r--src/libserver/task.h392
-rw-r--r--src/libserver/url.c4365
-rw-r--r--src/libserver/url.h430
-rw-r--r--src/libserver/worker_util.c2313
-rw-r--r--src/libserver/worker_util.h334
122 files changed, 79613 insertions, 0 deletions
diff --git a/src/libserver/CMakeLists.txt b/src/libserver/CMakeLists.txt
new file mode 100644
index 0000000..dd17865
--- /dev/null
+++ b/src/libserver/CMakeLists.txt
@@ -0,0 +1,52 @@
+# Librspamdserver
+ADD_SUBDIRECTORY(css)
+SET(LIBRSPAMDSERVERSRC
+ ${CMAKE_CURRENT_SOURCE_DIR}/cfg_utils.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/cfg_rcl.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/composites/composites.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/composites/composites_manager.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/dkim.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/dns.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/dynamic_cfg.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/async_session.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/fuzzy_backend/fuzzy_backend.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/fuzzy_backend/fuzzy_backend_sqlite.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/fuzzy_backend/fuzzy_backend_redis.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/milter.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/monitored.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/protocol.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/re_cache.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/redis_pool.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/roll_history.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/spf.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/ssl_util.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/symcache/symcache_impl.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/symcache/symcache_item.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/symcache/symcache_runtime.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/symcache/symcache_c.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/task.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/url.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/worker_util.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/logger/logger.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/logger/logger_file.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/logger/logger_syslog.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/logger/logger_console.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/http/http_util.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/http/http_message.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/http/http_connection.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/http/http_router.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/http/http_context.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/maps/map.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/maps/map_helpers.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/html/html_entities.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/html/html_url.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/html/html.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/html/html_tests.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/hyperscan_tools.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/backtrace.cxx
+ ${LIBCSSSRC})
+
+# Librspamd-server
+SET(RSPAMD_SERVER ${LIBRSPAMDSERVERSRC} PARENT_SCOPE)
+SET(LIBSERVER_DEPENDS "${LIBCSS_DEPENDS}" PARENT_SCOPE)
+SET(LIBSERVER_GENERATED "${LIBCSS_GENERATED}" PARENT_SCOPE)
diff --git a/src/libserver/async_session.c b/src/libserver/async_session.c
new file mode 100644
index 0000000..baaee62
--- /dev/null
+++ b/src/libserver/async_session.c
@@ -0,0 +1,364 @@
+/*
+ * 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 "rspamd.h"
+#include "contrib/uthash/utlist.h"
+#include "contrib/libucl/khash.h"
+#include "async_session.h"
+#include "cryptobox.h"
+
+#define RSPAMD_SESSION_FLAG_DESTROYING (1 << 1)
+#define RSPAMD_SESSION_FLAG_CLEANUP (1 << 2)
+
+#define RSPAMD_SESSION_CAN_ADD_EVENT(s) (!((s)->flags & (RSPAMD_SESSION_FLAG_DESTROYING | RSPAMD_SESSION_FLAG_CLEANUP)))
+
+#define msg_err_session(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "events", session->pool->tag.uid, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+#define msg_warn_session(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ "events", session->pool->tag.uid, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+#define msg_info_session(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ "events", session->pool->tag.uid, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+#define msg_debug_session(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_events_log_id, "events", session->pool->tag.uid, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+
+INIT_LOG_MODULE(events)
+
+/* Average symbols count to optimize hash allocation */
+static struct rspamd_counter_data events_count;
+
+
+struct rspamd_async_event {
+ const gchar *subsystem;
+ const gchar *event_source;
+ event_finalizer_t fin;
+ void *user_data;
+};
+
+static inline bool
+rspamd_event_equal(const struct rspamd_async_event *ev1, const struct rspamd_async_event *ev2)
+{
+ return ev1->fin == ev2->fin && ev1->user_data == ev2->user_data;
+}
+
+static inline guint64
+rspamd_event_hash(const struct rspamd_async_event *ev)
+{
+ union _pointer_fp_thunk {
+ event_finalizer_t f;
+ gpointer p;
+ };
+ struct ev_storage {
+ union _pointer_fp_thunk p;
+ gpointer ud;
+ } st;
+
+ st.p.f = ev->fin;
+ st.ud = ev->user_data;
+
+ return rspamd_cryptobox_fast_hash(&st, sizeof(st), rspamd_hash_seed());
+}
+
+/* Define **SET** of events */
+KHASH_INIT(rspamd_events_hash,
+ struct rspamd_async_event *,
+ char,
+ false,
+ rspamd_event_hash,
+ rspamd_event_equal);
+
+struct rspamd_async_session {
+ session_finalizer_t fin;
+ event_finalizer_t restore;
+ event_finalizer_t cleanup;
+ khash_t(rspamd_events_hash) * events;
+ void *user_data;
+ rspamd_mempool_t *pool;
+ guint flags;
+};
+
+static void
+rspamd_session_dtor(gpointer d)
+{
+ struct rspamd_async_session *s = (struct rspamd_async_session *) d;
+
+ /* Events are usually empty at this point */
+ rspamd_set_counter_ema(&events_count, s->events->n_buckets, 0.5);
+ kh_destroy(rspamd_events_hash, s->events);
+}
+
+struct rspamd_async_session *
+rspamd_session_create(rspamd_mempool_t *pool,
+ session_finalizer_t fin,
+ event_finalizer_t restore,
+ event_finalizer_t cleanup,
+ void *user_data)
+{
+ struct rspamd_async_session *s;
+
+ s = rspamd_mempool_alloc0(pool, sizeof(struct rspamd_async_session));
+ s->pool = pool;
+ s->fin = fin;
+ s->restore = restore;
+ s->cleanup = cleanup;
+ s->user_data = user_data;
+ s->events = kh_init(rspamd_events_hash);
+
+ kh_resize(rspamd_events_hash, s->events, MAX(4, events_count.mean));
+ rspamd_mempool_add_destructor(pool, rspamd_session_dtor, s);
+
+ return s;
+}
+
+struct rspamd_async_event *
+rspamd_session_add_event_full(struct rspamd_async_session *session,
+ event_finalizer_t fin,
+ gpointer user_data,
+ const gchar *subsystem,
+ const gchar *event_source)
+{
+ struct rspamd_async_event *new_event;
+ gint ret;
+
+ if (session == NULL) {
+ msg_err("session is NULL");
+ g_assert_not_reached();
+ }
+
+ if (!RSPAMD_SESSION_CAN_ADD_EVENT(session)) {
+ msg_debug_session("skip adding event subsystem: %s: "
+ "session is destroying/cleaning",
+ subsystem);
+
+ return NULL;
+ }
+
+ new_event = rspamd_mempool_alloc(session->pool,
+ sizeof(struct rspamd_async_event));
+ new_event->fin = fin;
+ new_event->user_data = user_data;
+ new_event->subsystem = subsystem;
+ new_event->event_source = event_source;
+
+ msg_debug_session("added event: %p, pending %d (+1) events, "
+ "subsystem: %s (%s)",
+ user_data,
+ kh_size(session->events),
+ subsystem,
+ event_source);
+
+ kh_put(rspamd_events_hash, session->events, new_event, &ret);
+ g_assert(ret > 0);
+
+ return new_event;
+}
+
+void rspamd_session_remove_event_full(struct rspamd_async_session *session,
+ event_finalizer_t fin,
+ void *ud,
+ const gchar *event_source)
+{
+ struct rspamd_async_event search_ev, *found_ev;
+ khiter_t k;
+
+ if (session == NULL) {
+ msg_err("session is NULL");
+ return;
+ }
+
+ if (!RSPAMD_SESSION_CAN_ADD_EVENT(session)) {
+ /* Session is already cleaned up, ignore this */
+ return;
+ }
+
+ /* Search for event */
+ search_ev.fin = fin;
+ search_ev.user_data = ud;
+ k = kh_get(rspamd_events_hash, session->events, &search_ev);
+ if (k == kh_end(session->events)) {
+
+ msg_err_session("cannot find event: %p(%p) from %s (%d total events)", fin, ud,
+ event_source, (int) kh_size(session->events));
+ kh_foreach_key(session->events, found_ev, {
+ msg_err_session("existing event %s (%s): %p(%p)",
+ found_ev->subsystem,
+ found_ev->event_source,
+ found_ev->fin,
+ found_ev->user_data);
+ });
+
+ g_assert_not_reached();
+ }
+
+ found_ev = kh_key(session->events, k);
+ msg_debug_session("removed event: %p, pending %d (-1) events, "
+ "subsystem: %s (%s), added at %s",
+ ud,
+ kh_size(session->events),
+ found_ev->subsystem,
+ event_source,
+ found_ev->event_source);
+ kh_del(rspamd_events_hash, session->events, k);
+
+ /* Remove event */
+ if (fin) {
+ fin(ud);
+ }
+
+ rspamd_session_pending(session);
+}
+
+gboolean
+rspamd_session_destroy(struct rspamd_async_session *session)
+{
+ if (session == NULL) {
+ msg_err("session is NULL");
+ return FALSE;
+ }
+
+ if (!rspamd_session_blocked(session)) {
+ session->flags |= RSPAMD_SESSION_FLAG_DESTROYING;
+ rspamd_session_cleanup(session, false);
+
+ if (session->cleanup != NULL) {
+ session->cleanup(session->user_data);
+ }
+ }
+
+ return TRUE;
+}
+
+void rspamd_session_cleanup(struct rspamd_async_session *session, bool forced_cleanup)
+{
+ struct rspamd_async_event *ev;
+
+ if (session == NULL) {
+ msg_err("session is NULL");
+ return;
+ }
+
+ session->flags |= RSPAMD_SESSION_FLAG_CLEANUP;
+ khash_t(rspamd_events_hash) *uncancellable_events = kh_init(rspamd_events_hash);
+
+ kh_foreach_key(session->events, ev, {
+ /* Call event's finalizer */
+ int ret;
+
+ if (ev->fin != NULL) {
+ if (forced_cleanup) {
+ msg_info_session("forced removed event on destroy: %p, subsystem: %s, scheduled from: %s",
+ ev->user_data,
+ ev->subsystem,
+ ev->event_source);
+ }
+ else {
+ msg_debug_session("removed event on destroy: %p, subsystem: %s",
+ ev->user_data,
+ ev->subsystem);
+ }
+ ev->fin(ev->user_data);
+ }
+ else {
+ if (forced_cleanup) {
+ msg_info_session("NOT forced removed event on destroy - uncancellable: "
+ "%p, subsystem: %s, scheduled from: %s",
+ ev->user_data,
+ ev->subsystem,
+ ev->event_source);
+ }
+ else {
+ msg_debug_session("NOT removed event on destroy - uncancellable: %p, subsystem: %s",
+ ev->user_data,
+ ev->subsystem);
+ }
+ /* Assume an event is uncancellable, move it to a new hash table */
+ kh_put(rspamd_events_hash, uncancellable_events, ev, &ret);
+ }
+ });
+
+ kh_destroy(rspamd_events_hash, session->events);
+ session->events = uncancellable_events;
+ if (forced_cleanup) {
+ msg_info_session("pending %d uncancellable events", kh_size(uncancellable_events));
+ }
+ else {
+ msg_debug_session("pending %d uncancellable events", kh_size(uncancellable_events));
+ }
+
+ session->flags &= ~RSPAMD_SESSION_FLAG_CLEANUP;
+}
+
+gboolean
+rspamd_session_pending(struct rspamd_async_session *session)
+{
+ gboolean ret = TRUE;
+
+ if (kh_size(session->events) == 0) {
+ if (session->fin != NULL) {
+ msg_debug_session("call fin handler, as no events are pending");
+
+ if (!session->fin(session->user_data)) {
+ /* Session finished incompletely, perform restoration */
+ msg_debug_session("restore incomplete session");
+ if (session->restore != NULL) {
+ session->restore(session->user_data);
+ }
+ }
+ else {
+ ret = FALSE;
+ }
+ }
+
+ ret = FALSE;
+ }
+
+ return ret;
+}
+
+guint rspamd_session_events_pending(struct rspamd_async_session *session)
+{
+ guint npending;
+
+ g_assert(session != NULL);
+
+ npending = kh_size(session->events);
+ msg_debug_session("pending %d events", npending);
+
+ return npending;
+}
+
+rspamd_mempool_t *
+rspamd_session_mempool(struct rspamd_async_session *session)
+{
+ g_assert(session != NULL);
+
+ return session->pool;
+}
+
+gboolean
+rspamd_session_blocked(struct rspamd_async_session *session)
+{
+ g_assert(session != NULL);
+
+ return !RSPAMD_SESSION_CAN_ADD_EVENT(session);
+} \ No newline at end of file
diff --git a/src/libserver/async_session.h b/src/libserver/async_session.h
new file mode 100644
index 0000000..4573545
--- /dev/null
+++ b/src/libserver/async_session.h
@@ -0,0 +1,121 @@
+/*-
+ * 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_ASYNC_SESSION_H
+#define RSPAMD_ASYNC_SESSION_H
+
+#include "config.h"
+#include "mem_pool.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_async_event;
+struct rspamd_async_session;
+
+typedef void (*event_finalizer_t)(gpointer ud);
+
+typedef gboolean (*session_finalizer_t)(gpointer user_data);
+
+/**
+ * Make new async session
+ * @param pool pool to alloc memory from
+ * @param fin a callback called when no events are found in session
+ * @param restore a callback is called to restore processing of session
+ * @param cleanup a callback called when session is forcefully destroyed
+ * @param user_data abstract user data
+ * @return
+ */
+struct rspamd_async_session *rspamd_session_create(rspamd_mempool_t *pool,
+ session_finalizer_t fin, event_finalizer_t restore,
+ event_finalizer_t cleanup, gpointer user_data);
+
+/**
+ * Insert new event to the session
+ * @param session session object
+ * @param fin finalizer callback
+ * @param user_data abstract user_data
+ * @param forced unused
+ */
+struct rspamd_async_event *
+rspamd_session_add_event_full(struct rspamd_async_session *session,
+ event_finalizer_t fin,
+ gpointer user_data,
+ const gchar *subsystem,
+ const gchar *event_source);
+
+#define rspamd_session_add_event(session, fin, user_data, subsystem) \
+ rspamd_session_add_event_full(session, fin, user_data, subsystem, G_STRLOC)
+
+/**
+ * Remove normal event
+ * @param session session object
+ * @param fin final callback
+ * @param ud user data object
+ */
+void rspamd_session_remove_event_full(struct rspamd_async_session *session,
+ event_finalizer_t fin,
+ gpointer ud,
+ const gchar *event_source);
+
+#define rspamd_session_remove_event(session, fin, user_data) \
+ rspamd_session_remove_event_full(session, fin, user_data, G_STRLOC)
+
+/**
+ * Must be called at the end of session, it calls fin functions for all non-forced callbacks
+ * @return true if the whole session was destroyed and false if there are forced events
+ */
+gboolean rspamd_session_destroy(struct rspamd_async_session *session);
+
+/**
+ * Try to remove all events pending
+ */
+void rspamd_session_cleanup(struct rspamd_async_session *session, bool forced_cleanup);
+
+/**
+ * Returns mempool associated with async session
+ * @param session
+ * @return
+ */
+rspamd_mempool_t *rspamd_session_mempool(struct rspamd_async_session *session);
+
+/**
+ * Check session for events pending and call fin callback if no events are pending
+ * @param session session object
+ * @return TRUE if session has pending events
+ */
+gboolean rspamd_session_pending(struct rspamd_async_session *session);
+
+/**
+ * Returns number of events pending
+ * @param session
+ * @return
+ */
+guint rspamd_session_events_pending(struct rspamd_async_session *session);
+
+
+/**
+ * Returns TRUE if an async session is currently destroying
+ * @param s
+ * @return
+ */
+gboolean rspamd_session_blocked(struct rspamd_async_session *s);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /*RSPAMD_ASYNC_SESSION_H*/
diff --git a/src/libserver/backtrace.cxx b/src/libserver/backtrace.cxx
new file mode 100644
index 0000000..6507b96
--- /dev/null
+++ b/src/libserver/backtrace.cxx
@@ -0,0 +1,61 @@
+/*
+ * 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"
+
+#ifdef BACKWARD_ENABLE
+
+#include "contrib/backward-cpp/backward.hpp"
+#include "fmt/core.h"
+#include "logger.h"
+
+namespace rspamd {
+
+void log_backtrace(void)
+{
+ using namespace backward;
+ StackTrace st;
+ st.load_here(128);
+
+ TraceResolver tr;
+ tr.load_stacktrace(st);
+
+ for (auto i = 0ul; i < st.size(); ++i) {
+ auto trace = tr.resolve(st[i]);
+ auto trace_line = fmt::format("#{}: [{}]: ", i, trace.addr);
+
+ if (!trace.source.filename.empty()) {
+ trace_line += fmt::format("{}:{} in {}", trace.source.filename, trace.source.line, trace.source.function);
+ }
+ else {
+ trace_line += fmt::format("{} in {}", trace.object_filename, trace.object_function);
+ }
+
+ msg_err("%s", trace_line.c_str());
+ }
+}
+
+}// namespace rspamd
+#endif
+
+extern "C" void rspamd_print_crash(void);
+
+void rspamd_print_crash(void)
+{
+#ifdef BACKWARD_ENABLE
+ rspamd::log_backtrace();
+#endif
+} \ No newline at end of file
diff --git a/src/libserver/cfg_file.h b/src/libserver/cfg_file.h
new file mode 100644
index 0000000..4cb87d9
--- /dev/null
+++ b/src/libserver/cfg_file.h
@@ -0,0 +1,889 @@
+/*
+ * 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.
+ */
+
+#ifndef CFG_FILE_H
+#define CFG_FILE_H
+
+#include "config.h"
+#include "mem_pool.h"
+#include "upstream.h"
+#include "rspamd_symcache.h"
+#include "cfg_rcl.h"
+#include "ucl.h"
+#include "regexp.h"
+#include "libserver/re_cache.h"
+#include "libutil/ref.h"
+#include "libutil/radix.h"
+#include "monitored.h"
+#include "redis_pool.h"
+
+#define DEFAULT_BIND_PORT 11333
+#define DEFAULT_CONTROL_PORT 11334
+
+/* Default metric name */
+#define DEFAULT_METRIC "default"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct expression;
+struct tokenizer;
+struct rspamd_stat_classifier;
+struct module_s;
+struct worker_s;
+struct rspamd_external_libs_ctx;
+struct rspamd_cryptobox_pubkey;
+struct rspamd_dns_resolver;
+
+/**
+ * Logging type
+ */
+enum rspamd_log_type {
+ RSPAMD_LOG_CONSOLE,
+ RSPAMD_LOG_SYSLOG,
+ RSPAMD_LOG_FILE
+};
+
+enum rspamd_log_cfg_flags {
+ RSPAMD_LOG_FLAG_DEFAULT = 0u,
+ RSPAMD_LOG_FLAG_SYSTEMD = (1u << 0u),
+ RSPAMD_LOG_FLAG_COLOR = (1u << 1u),
+ RSPAMD_LOG_FLAG_RE_CACHE = (1u << 2u),
+ RSPAMD_LOG_FLAG_USEC = (1u << 3u),
+ RSPAMD_LOG_FLAG_RSPAMADM = (1u << 4u),
+ RSPAMD_LOG_FLAG_ENFORCED = (1u << 5u),
+ RSPAMD_LOG_FLAG_SEVERITY = (1u << 6u),
+ RSPAMD_LOG_FLAG_JSON = (1u << 7u),
+};
+
+struct rspamd_worker_log_pipe {
+ gint fd;
+ gint type;
+ struct rspamd_worker_log_pipe *prev, *next;
+};
+
+/**
+ * script module list item
+ */
+struct script_module {
+ gchar *name; /**< name of module */
+ gchar *path; /**< path to module */
+ gchar *digest;
+};
+
+enum rspamd_symbol_group_flags {
+ RSPAMD_SYMBOL_GROUP_NORMAL = 0u,
+ RSPAMD_SYMBOL_GROUP_DISABLED = (1u << 0u),
+ RSPAMD_SYMBOL_GROUP_ONE_SHOT = (1u << 1u),
+ RSPAMD_SYMBOL_GROUP_UNGROUPED = (1u << 2u),
+ RSPAMD_SYMBOL_GROUP_PUBLIC = (1u << 3u),
+};
+
+/**
+ * Symbols group
+ */
+struct rspamd_symbol;
+struct rspamd_symbols_group {
+ gchar *name;
+ gchar *description;
+ GHashTable *symbols;
+ gdouble max_score;
+ guint flags;
+};
+
+enum rspamd_symbol_flags {
+ RSPAMD_SYMBOL_FLAG_NORMAL = 0,
+ RSPAMD_SYMBOL_FLAG_IGNORE_METRIC = (1 << 1),
+ RSPAMD_SYMBOL_FLAG_ONEPARAM = (1 << 2),
+ RSPAMD_SYMBOL_FLAG_UNGROUPED = (1 << 3),
+ RSPAMD_SYMBOL_FLAG_DISABLED = (1 << 4),
+ RSPAMD_SYMBOL_FLAG_UNSCORED = (1 << 5),
+};
+
+/**
+ * Symbol config definition
+ */
+struct rspamd_symbol {
+ gchar *name;
+ gchar *description;
+ gdouble *weight_ptr;
+ gdouble score;
+ guint priority;
+ struct rspamd_symbols_group *gr; /* Main group */
+ GPtrArray *groups; /* Other groups */
+ guint flags;
+ void *cache_item;
+ gint nshots;
+};
+
+/**
+ * Statfile config definition
+ */
+struct rspamd_statfile_config {
+ gchar *symbol; /**< symbol of statfile */
+ gchar *label; /**< label of this statfile */
+ ucl_object_t *opts; /**< other options */
+ gboolean is_spam; /**< spam flag */
+ struct rspamd_classifier_config *clcf; /**< parent pointer of classifier configuration */
+ gpointer data; /**< opaque data */
+};
+
+struct rspamd_tokenizer_config {
+ const ucl_object_t *opts; /**< other options */
+ const gchar *name; /**< name of tokenizer */
+};
+
+
+/* Classifier has all integer values (e.g. bayes) */
+#define RSPAMD_FLAG_CLASSIFIER_INTEGER (1 << 0)
+/*
+ * Set if backend for a classifier is intended to increment and not set values
+ * (e.g. redis)
+ */
+#define RSPAMD_FLAG_CLASSIFIER_INCREMENTING_BACKEND (1 << 1)
+/*
+ * No backend required for classifier
+ */
+#define RSPAMD_FLAG_CLASSIFIER_NO_BACKEND (1 << 2)
+
+/**
+ * Classifier config definition
+ */
+struct rspamd_classifier_config {
+ GList *statfiles; /**< statfiles list */
+ GHashTable *labels; /**< statfiles with labels */
+ gchar *metric; /**< metric of this classifier */
+ gchar *classifier; /**< classifier interface */
+ struct rspamd_tokenizer_config *tokenizer; /**< tokenizer used for classifier */
+ const gchar *backend; /**< name of statfile's backend */
+ ucl_object_t *opts; /**< other options */
+ GList *learn_conditions; /**< list of learn condition callbacks */
+ GList *classify_conditions; /**< list of classify condition callbacks */
+ gchar *name; /**< unique name of classifier */
+ guint32 min_tokens; /**< minimal number of tokens to process classifier */
+ guint32 max_tokens; /**< maximum number of tokens */
+ guint min_token_hits; /**< minimum number of hits for a token to be considered */
+ gdouble min_prob_strength; /**< use only tokens with probability in [0.5 - MPS, 0.5 + MPS] */
+ guint min_learns; /**< minimum number of learns for each statfile */
+ guint flags;
+};
+
+struct rspamd_worker_bind_conf {
+ GPtrArray *addrs;
+ guint cnt;
+ gchar *name;
+ gchar *bind_line;
+ gboolean is_systemd;
+ struct rspamd_worker_bind_conf *next;
+};
+
+struct rspamd_worker_lua_script {
+ gint cbref;
+ struct rspamd_worker_lua_script *prev, *next;
+};
+
+/**
+ * Config params for rspamd worker
+ */
+struct rspamd_worker_conf {
+ struct worker_s *worker; /**< pointer to worker type */
+ GQuark type; /**< type of worker */
+ struct rspamd_worker_bind_conf *bind_conf; /**< bind configuration */
+ gint16 count; /**< number of workers */
+ GList *listen_socks; /**< listening sockets descriptors */
+ guint64 rlimit_nofile; /**< max files limit */
+ guint64 rlimit_maxcore; /**< maximum core file size */
+ GHashTable *params; /**< params for worker */
+ GQueue *active_workers; /**< linked list of spawned workers */
+ gpointer ctx; /**< worker's context */
+ ucl_object_t *options; /**< other worker's options */
+ struct rspamd_worker_lua_script *scripts; /**< registered lua scripts */
+ gboolean enabled;
+ ref_entry_t ref;
+};
+
+enum rspamd_log_format_type {
+ RSPAMD_LOG_STRING = 0,
+ RSPAMD_LOG_MID,
+ RSPAMD_LOG_QID,
+ RSPAMD_LOG_USER,
+ RSPAMD_LOG_ISSPAM,
+ RSPAMD_LOG_ACTION,
+ RSPAMD_LOG_SCORES,
+ RSPAMD_LOG_SYMBOLS,
+ RSPAMD_LOG_IP,
+ RSPAMD_LOG_LEN,
+ RSPAMD_LOG_DNS_REQ,
+ RSPAMD_LOG_SMTP_FROM,
+ RSPAMD_LOG_MIME_FROM,
+ RSPAMD_LOG_SMTP_RCPT,
+ RSPAMD_LOG_MIME_RCPT,
+ RSPAMD_LOG_SMTP_RCPTS,
+ RSPAMD_LOG_MIME_RCPTS,
+ RSPAMD_LOG_TIME_REAL,
+ RSPAMD_LOG_TIME_VIRTUAL,
+ RSPAMD_LOG_LUA,
+ RSPAMD_LOG_DIGEST,
+ RSPAMD_LOG_FILENAME,
+ RSPAMD_LOG_FORCED_ACTION,
+ RSPAMD_LOG_SETTINGS_ID,
+ RSPAMD_LOG_GROUPS,
+ RSPAMD_LOG_PUBLIC_GROUPS,
+ RSPAMD_LOG_MEMPOOL_SIZE,
+ RSPAMD_LOG_MEMPOOL_WASTE,
+};
+
+enum rspamd_log_format_flags {
+ RSPAMD_LOG_FMT_FLAG_DEFAULT = 0,
+ RSPAMD_LOG_FMT_FLAG_OPTIONAL = (1 << 0),
+ RSPAMD_LOG_FMT_FLAG_MIME_ALTERNATIVE = (1 << 1),
+ RSPAMD_LOG_FMT_FLAG_CONDITION = (1 << 2),
+ RSPAMD_LOG_FMT_FLAG_SYMBOLS_SCORES = (1 << 3),
+ RSPAMD_LOG_FMT_FLAG_SYMBOLS_PARAMS = (1 << 4)
+};
+
+struct rspamd_log_format {
+ enum rspamd_log_format_type type;
+ guint flags;
+ gsize len;
+ gpointer data;
+ struct rspamd_log_format *prev, *next;
+};
+
+/**
+ * Standard actions
+ */
+enum rspamd_action_type {
+ METRIC_ACTION_REJECT = 0,
+ METRIC_ACTION_SOFT_REJECT,
+ METRIC_ACTION_REWRITE_SUBJECT,
+ METRIC_ACTION_ADD_HEADER,
+ METRIC_ACTION_GREYLIST,
+ METRIC_ACTION_NOACTION,
+ METRIC_ACTION_MAX,
+ METRIC_ACTION_CUSTOM = 999,
+ METRIC_ACTION_DISCARD,
+ METRIC_ACTION_QUARANTINE
+};
+
+enum rspamd_action_flags {
+ RSPAMD_ACTION_NORMAL = 0u,
+ RSPAMD_ACTION_NO_THRESHOLD = (1u << 0u),
+ RSPAMD_ACTION_THRESHOLD_ONLY = (1u << 1u),
+ RSPAMD_ACTION_HAM = (1u << 2u),
+ RSPAMD_ACTION_MILTER = (1u << 3u),
+};
+
+
+struct rspamd_action;
+
+struct rspamd_config_cfg_lua_script {
+ gint cbref;
+ gint priority;
+ gchar *lua_src_pos;
+ struct rspamd_config_cfg_lua_script *prev, *next;
+};
+
+struct rspamd_config_post_init_script {
+ gint cbref;
+ struct rspamd_config_post_init_script *prev, *next;
+};
+
+struct rspamd_lang_detector;
+struct rspamd_rcl_sections_map;
+
+enum rspamd_config_settings_policy {
+ RSPAMD_SETTINGS_POLICY_DEFAULT = 0,
+ RSPAMD_SETTINGS_POLICY_IMPLICIT_ALLOW = 1,
+ RSPAMD_SETTINGS_POLICY_IMPLICIT_DENY = 2,
+};
+
+enum rspamd_gtube_patterns_policy {
+ RSPAMD_GTUBE_DISABLED = 0, /* Disabled */
+ RSPAMD_GTUBE_REJECT, /* Reject message with GTUBE pattern */
+ RSPAMD_GTUBE_ALL /* Check all GTUBE like patterns */
+};
+
+struct rspamd_config_settings_elt {
+ guint32 id;
+ enum rspamd_config_settings_policy policy;
+ const gchar *name;
+ ucl_object_t *symbols_enabled;
+ ucl_object_t *symbols_disabled;
+ struct rspamd_config_settings_elt *prev, *next;
+ ref_entry_t ref;
+};
+
+/**
+ * Structure that stores all config data
+ */
+struct rspamd_config {
+ gchar *rspamd_user; /**< user to run as */
+ gchar *rspamd_group; /**< group to run as */
+ rspamd_mempool_t *cfg_pool; /**< memory pool for config */
+ gchar *cfg_name; /**< name of config file */
+ gchar *pid_file; /**< name of pid file */
+ gchar *temp_dir; /**< dir for temp files */
+ gchar *control_socket_path; /**< path to the control socket */
+ const ucl_object_t *local_addrs; /**< tree of local addresses */
+#ifdef WITH_GPERF_TOOLS
+ gchar *profile_path;
+#endif
+ gdouble unknown_weight; /**< weight of unknown symbols */
+ gdouble grow_factor; /**< grow factor for metric */
+ GHashTable *symbols; /**< weights of symbols in metric */
+ const gchar *subject; /**< subject rewrite string */
+ GHashTable *groups; /**< groups of symbols */
+ void *actions; /**< all actions of the metric (opaque type) */
+
+ gboolean one_shot_mode; /**< rules add only one symbol */
+ gboolean check_text_attachements; /**< check text attachements as text */
+ gboolean check_all_filters; /**< check all filters */
+ gboolean allow_raw_input; /**< scan messages with invalid mime */
+ gboolean disable_hyperscan; /**< disable hyperscan usage */
+ gboolean vectorized_hyperscan; /**< use vectorized hyperscan matching */
+ gboolean enable_shutdown_workaround; /**< enable workaround for legacy SA clients (exim) */
+ gboolean ignore_received; /**< Ignore data from the first received header */
+ gboolean enable_sessions_cache; /**< Enable session cache for debug */
+ gboolean enable_experimental; /**< Enable experimental plugins */
+ gboolean disable_pcre_jit; /**< Disable pcre JIT */
+ gboolean own_lua_state; /**< True if we have created lua_state internally */
+ gboolean soft_reject_on_timeout; /**< If true emit soft reject on task timeout (if not reject) */
+ gboolean public_groups_only; /**< Output merely public groups everywhere */
+ enum rspamd_gtube_patterns_policy gtube_patterns_policy; /**< Enable test patterns */
+ gboolean enable_css_parser; /**< Enable css parsing in HTML */
+
+ gsize max_cores_size; /**< maximum size occupied by rspamd core files */
+ gsize max_cores_count; /**< maximum number of core files */
+ gchar *cores_dir; /**< directory for core files */
+ gsize max_message; /**< maximum size for messages */
+ gsize max_pic_size; /**< maximum size for a picture to process */
+ gsize images_cache_size; /**< size of LRU cache for DCT data from images */
+ gdouble task_timeout; /**< maximum message processing time */
+ gint default_max_shots; /**< default maximum count of symbols hits permitted (-1 for unlimited) */
+ gint32 heartbeats_loss_max; /**< number of heartbeats lost to consider worker's termination */
+ gdouble heartbeat_interval; /**< interval for heartbeats for workers */
+
+ enum rspamd_log_type log_type; /**< log type */
+ gint log_facility; /**< log facility in case of syslog */
+ gint log_level; /**< log level trigger */
+ gchar *log_file; /**< path to logfile in case of file logging */
+ gboolean log_buffered; /**< whether logging is buffered */
+ gboolean log_silent_workers; /**< silence info messages from workers */
+ guint32 log_buf_size; /**< length of log buffer */
+ const ucl_object_t *debug_ip_map; /**< turn on debugging for specified ip addresses */
+ gboolean log_urls; /**< whether we should log URLs */
+ GHashTable *debug_modules; /**< logging modules to debug */
+ struct rspamd_cryptobox_pubkey *log_encryption_key; /**< encryption key for logs */
+ guint log_flags; /**< logging flags */
+ guint log_error_elts; /**< number of elements in error logbuf */
+ guint log_error_elt_maxlen; /**< maximum size of error log element */
+ guint log_task_max_elts; /**< maximum number of elements in task logging */
+ struct rspamd_worker_log_pipe *log_pipes;
+
+ gboolean compat_messages; /**< use old messages in the protocol (array) */
+
+ GPtrArray *script_modules; /**< a list of script modules to load */
+ GHashTable *explicit_modules; /**< modules that should be always loaded */
+
+ GList *filters; /**< linked list of all filters */
+ GList *workers; /**< linked list of all workers params */
+ struct rspamd_rcl_sections_map *rcl_top_section; /**< top section for RCL config */
+ ucl_object_t *cfg_ucl_obj; /**< ucl object */
+ ucl_object_t *config_comments; /**< comments saved from the config */
+ ucl_object_t *doc_strings; /**< documentation strings for config options */
+ GPtrArray *c_modules; /**< list of C modules */
+ void *composites_manager; /**< hash of composite symbols indexed by its name */
+ GList *classifiers; /**< list of all classifiers defined */
+ GList *statfiles; /**< list of all statfiles in config file order */
+ GHashTable *classifiers_symbols; /**< hashtable indexed by symbol name of classifiers */
+ GHashTable *cfg_params; /**< all cfg params indexed by its name in this structure */
+ gchar *dynamic_conf; /**< path to dynamic configuration */
+ ucl_object_t *current_dynamic_conf; /**< currently loaded dynamic configuration */
+ gint clock_res; /**< resolution of clock used */
+
+ GList *maps; /**< maps active */
+ gdouble map_timeout; /**< maps watch timeout */
+ gdouble map_file_watch_multiplier; /**< multiplier for watch timeout when maps are files */
+ gchar *maps_cache_dir; /**< where to save HTTP cached data */
+
+ gdouble monitored_interval; /**< interval between monitored checks */
+ gboolean disable_monitored; /**< disable monitoring completely */
+ gboolean fips_mode; /**< turn on fips mode for openssl */
+
+ struct rspamd_symcache *cache; /**< symbols cache object */
+ gchar *cache_filename; /**< filename of cache file */
+ gdouble cache_reload_time; /**< how often cache reload should be performed */
+ gchar *checksum; /**< real checksum of config file */
+ gpointer lua_state; /**< pointer to lua state */
+ gpointer lua_thread_pool; /**< pointer to lua thread (coroutine) pool */
+
+ gchar *rrd_file; /**< rrd file to store statistics */
+ gchar *history_file; /**< file to save rolling history */
+ gchar *stats_file; /**< file to save stats */
+ gchar *tld_file; /**< file to load effective tld list from */
+ gchar *hs_cache_dir; /**< directory to save hyperscan databases */
+ gchar *events_backend; /**< string representation of the events backend used */
+
+ gdouble dns_timeout; /**< timeout in milliseconds for waiting for dns reply */
+ guint32 dns_retransmits; /**< maximum retransmits count */
+ guint32 dns_io_per_server; /**< number of sockets per DNS server */
+ const ucl_object_t *nameservers; /**< list of nameservers or NULL to parse resolv.conf */
+ guint32 dns_max_requests; /**< limit of DNS requests per task */
+ gboolean enable_dnssec; /**< enable dnssec stub resolver */
+
+ guint upstream_max_errors; /**< upstream max errors before shutting off */
+ gdouble upstream_error_time; /**< rate of upstream errors */
+ gdouble upstream_revive_time; /**< revive timeout for upstreams */
+ gdouble upstream_lazy_resolve_time; /**< lazy resolve time for upstreams */
+ struct upstream_ctx *ups_ctx; /**< upstream context */
+ struct rspamd_dns_resolver *dns_resolver; /**< dns resolver if loaded */
+
+ guint min_word_len; /**< minimum length of the word to be considered */
+ guint max_word_len; /**< maximum length of the word to be considered */
+ guint words_decay; /**< limit for words for starting adaptive ignoring */
+ guint history_rows; /**< number of history rows stored */
+ guint max_sessions_cache; /**< maximum number of sessions cache elts */
+ guint lua_gc_step; /**< lua gc step */
+ guint lua_gc_pause; /**< lua gc pause */
+ guint full_gc_iters; /**< iterations between full gc cycle */
+ guint max_lua_urls; /**< maximum number of urls to be passed to Lua */
+ guint max_urls; /**< maximum number of urls to be processed in general */
+ gint max_recipients; /**< maximum number of recipients to be processed */
+ guint max_blas_threads; /**< maximum threads for openblas when learning ANN */
+ guint max_opts_len; /**< maximum length for all options for a symbol */
+ gsize max_html_len; /**< maximum length of HTML document */
+
+ struct module_s **compiled_modules; /**< list of compiled C modules */
+ struct worker_s **compiled_workers; /**< list of compiled C modules */
+ struct rspamd_log_format *log_format; /**< parsed log format */
+ gchar *log_format_str; /**< raw log format string */
+
+ struct rspamd_external_libs_ctx *libs_ctx; /**< context for external libraries */
+ struct rspamd_monitored_ctx *monitored_ctx; /**< context for monitored resources */
+ void *redis_pool; /**< redis connection pool */
+
+ struct rspamd_re_cache *re_cache; /**< static regexp cache */
+
+ GHashTable *trusted_keys; /**< list of trusted public keys */
+
+ struct rspamd_config_cfg_lua_script *on_load_scripts; /**< list of scripts executed on workers load */
+ struct rspamd_config_cfg_lua_script *post_init_scripts; /**< list of scripts executed on config being fully loaded */
+ struct rspamd_config_cfg_lua_script *on_term_scripts; /**< list of callbacks called on worker's termination */
+ struct rspamd_config_cfg_lua_script *config_unload_scripts; /**< list of scripts executed on config unload */
+
+ gchar *ssl_ca_path; /**< path to CA certs */
+ gchar *ssl_ciphers; /**< set of preferred ciphers */
+ gchar *zstd_input_dictionary; /**< path to zstd input dictionary */
+ gchar *zstd_output_dictionary; /**< path to zstd output dictionary */
+ ucl_object_t *neighbours; /**< other servers in the cluster */
+
+ struct rspamd_config_settings_elt *setting_ids; /**< preprocessed settings ids */
+ struct rspamd_lang_detector *lang_det; /**< language detector */
+ struct rspamd_worker *cur_worker; /**< set dynamically by each worker */
+
+ ref_entry_t ref; /**< reference counter */
+};
+
+
+/**
+ * Parse bind credits
+ * @param cf config file to use
+ * @param str line that presents bind line
+ * @param type type of credits
+ * @return 1 if line was successfully parsed and 0 in case of error
+ */
+gboolean rspamd_parse_bind_line(struct rspamd_config *cfg,
+ struct rspamd_worker_conf *cf, const gchar *str);
+
+
+enum rspamd_config_init_flags {
+ RSPAMD_CONFIG_INIT_DEFAULT = 0u,
+ RSPAMD_CONFIG_INIT_SKIP_LUA = (1u << 0u),
+ RSPAMD_CONFIG_INIT_WIPE_LUA_MEM = (1u << 1u),
+};
+
+/**
+ * Init default values
+ * @param cfg config file
+ */
+struct rspamd_config *rspamd_config_new(enum rspamd_config_init_flags flags);
+
+/**
+ * Free memory used by config structure
+ * @param cfg config file
+ */
+void rspamd_config_free(struct rspamd_config *cfg);
+
+/**
+ * Gets module option with specified name
+ * @param cfg config file
+ * @param module_name name of module
+ * @param opt_name name of option to get
+ * @return module value or NULL if option does not defined
+ */
+const ucl_object_t *rspamd_config_get_module_opt(struct rspamd_config *cfg,
+ const gchar *module_name,
+ const gchar *opt_name) G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * Parse flag
+ * @param str string representation of flag (eg. 'on')
+ * @return numeric value of flag (0 or 1)
+ */
+gint rspamd_config_parse_flag(const gchar *str, guint len);
+
+enum rspamd_post_load_options {
+ RSPAMD_CONFIG_INIT_URL = 1 << 0,
+ RSPAMD_CONFIG_INIT_LIBS = 1 << 1,
+ RSPAMD_CONFIG_INIT_SYMCACHE = 1 << 2,
+ RSPAMD_CONFIG_INIT_VALIDATE = 1 << 3,
+ RSPAMD_CONFIG_INIT_NO_TLD = 1 << 4,
+ RSPAMD_CONFIG_INIT_PRELOAD_MAPS = 1 << 5,
+ RSPAMD_CONFIG_INIT_POST_LOAD_LUA = 1 << 6,
+};
+
+#define RSPAMD_CONFIG_LOAD_ALL (RSPAMD_CONFIG_INIT_URL | \
+ RSPAMD_CONFIG_INIT_LIBS | \
+ RSPAMD_CONFIG_INIT_SYMCACHE | \
+ RSPAMD_CONFIG_INIT_VALIDATE | \
+ RSPAMD_CONFIG_INIT_PRELOAD_MAPS | \
+ RSPAMD_CONFIG_INIT_POST_LOAD_LUA)
+
+/**
+ * Do post load actions for config
+ * @param cfg config file
+ */
+gboolean rspamd_config_post_load(struct rspamd_config *cfg,
+ enum rspamd_post_load_options opts);
+
+/*
+ * Return a new classifier_config structure, setting default and non-conflicting attributes
+ */
+struct rspamd_classifier_config *rspamd_config_new_classifier(
+ struct rspamd_config *cfg,
+ struct rspamd_classifier_config *c);
+
+/*
+ * Return a new worker_conf structure, setting default and non-conflicting attributes
+ */
+struct rspamd_worker_conf *rspamd_config_new_worker(struct rspamd_config *cfg,
+ struct rspamd_worker_conf *c);
+
+/*
+ * Return a new metric structure, setting default and non-conflicting attributes
+ */
+void rspamd_config_init_metric(struct rspamd_config *cfg);
+
+/*
+ * Return new symbols group definition
+ */
+struct rspamd_symbols_group *rspamd_config_new_group(
+ struct rspamd_config *cfg,
+ const gchar *name);
+
+/*
+ * Return a new statfile structure, setting default and non-conflicting attributes
+ */
+struct rspamd_statfile_config *rspamd_config_new_statfile(
+ struct rspamd_config *cfg,
+ struct rspamd_statfile_config *c);
+
+/*
+ * Register symbols of classifiers inside metrics
+ */
+void rspamd_config_insert_classify_symbols(struct rspamd_config *cfg);
+
+/*
+ * Check statfiles inside a classifier
+ */
+gboolean rspamd_config_check_statfiles(struct rspamd_classifier_config *cf);
+
+/*
+ * Find classifier config by name
+ */
+struct rspamd_classifier_config *rspamd_config_find_classifier(
+ struct rspamd_config *cfg,
+ const gchar *name);
+
+void rspamd_ucl_add_conf_macros(struct ucl_parser *parser,
+ struct rspamd_config *cfg);
+
+void rspamd_ucl_add_conf_variables(struct ucl_parser *parser, GHashTable *vars);
+
+/**
+ * Initialize rspamd filtering system (lua and C filters)
+ * @param cfg
+ * @param reconfig
+ * @return
+ */
+gboolean rspamd_init_filters(struct rspamd_config *cfg, bool reconfig, bool strict);
+
+/**
+ * Add new symbol to the metric
+ * @param cfg
+ * @param metric metric's name (or NULL for the default metric)
+ * @param symbol symbol's name
+ * @param score symbol's score
+ * @param description optional description
+ * @param group optional group name
+ * @param one_shot TRUE if symbol can add its score once
+ * @param rewrite_existing TRUE if we need to rewrite the existing symbol
+ * @param priority use the following priority for a symbol
+ * @param nshots means maximum number of hits for a symbol in metric (-1 for unlimited)
+ * @return TRUE if symbol has been inserted or FALSE if symbol already exists with higher priority
+ */
+gboolean rspamd_config_add_symbol(struct rspamd_config *cfg,
+ const gchar *symbol,
+ gdouble score,
+ const gchar *description,
+ const gchar *group,
+ guint flags,
+ guint priority,
+ gint nshots);
+
+/**
+ * Adds new group for a symbol
+ * @param cfg
+ * @param symbol
+ * @param group
+ * @return
+ */
+gboolean rspamd_config_add_symbol_group(struct rspamd_config *cfg,
+ const gchar *symbol,
+ const gchar *group);
+
+/**
+ * Sets action score for a specified metric with the specified priority
+ * @param cfg config file
+ * @param metric metric name (or NULL for default metric)
+ * @param action_name symbolic name of action
+ * @param obj data to set for action
+ * @return TRUE if symbol has been inserted or FALSE if action already exists with higher priority
+ */
+gboolean rspamd_config_set_action_score(struct rspamd_config *cfg,
+ const gchar *action_name,
+ const ucl_object_t *obj);
+
+/**
+ * Check priority and maybe disable action completely
+ * @param cfg
+ * @param action_name
+ * @param priority
+ * @return
+ */
+gboolean rspamd_config_maybe_disable_action(struct rspamd_config *cfg,
+ const gchar *action_name,
+ guint priority);
+
+/**
+ * Checks if a specified C or lua module is enabled or disabled in the config.
+ * The logic of check is the following:
+ *
+ * - For C modules, we check `filters` line and enable module only if it is found there
+ * - For LUA modules we check the corresponding configuration section:
+ * - if section exists, then we check `enabled` key and check its value
+ * - if section is absent, we consider module as disabled
+ * - For both C and LUA modules we check if the group with the module name is disabled in the default metric
+ * @param cfg config file
+ * @param module_name module name
+ * @return TRUE if a module is enabled
+ */
+gboolean rspamd_config_is_module_enabled(struct rspamd_config *cfg,
+ const gchar *module_name);
+
+/**
+ * Verifies enabled/disabled combination in the specified object
+ * @param obj
+ * @return TRUE if there is no explicit disable in the object found
+ */
+gboolean rspamd_config_is_enabled_from_ucl(rspamd_mempool_t *pool,
+ const ucl_object_t *obj);
+
+/*
+ * Get action from a string
+ */
+gboolean rspamd_action_from_str(const gchar *data, enum rspamd_action_type *result);
+
+/*
+ * Return textual representation of action enumeration
+ */
+const gchar *rspamd_action_to_str(enum rspamd_action_type action);
+
+const gchar *rspamd_action_to_str_alt(enum rspamd_action_type action);
+
+/**
+ * Parse radix tree or radix map from ucl object
+ * @param cfg configuration object
+ * @param obj ucl object with parameter
+ * @param target target radix tree
+ * @param err error pointer
+ * @return
+ */
+struct rspamd_radix_map_helper;
+
+gboolean rspamd_config_radix_from_ucl(struct rspamd_config *cfg, const ucl_object_t *obj, const gchar *description,
+ struct rspamd_radix_map_helper **target, GError **err,
+ struct rspamd_worker *worker, const gchar *map_name);
+
+/**
+ * Adds new settings id to be preprocessed
+ * @param cfg
+ * @param name
+ * @param symbols_enabled (ownership is transferred to callee)
+ * @param symbols_disabled (ownership is transferred to callee)
+ */
+void rspamd_config_register_settings_id(struct rspamd_config *cfg,
+ const gchar *name,
+ ucl_object_t *symbols_enabled,
+ ucl_object_t *symbols_disabled,
+ enum rspamd_config_settings_policy policy);
+
+/**
+ * Convert settings name to settings id
+ * @param name
+ * @param namelen
+ * @return
+ */
+guint32 rspamd_config_name_to_id(const gchar *name, gsize namelen);
+
+/**
+ * Finds settings id element and obtain reference count (must be unrefed by caller)
+ * @param cfg
+ * @param id
+ * @return
+ */
+struct rspamd_config_settings_elt *rspamd_config_find_settings_id_ref(
+ struct rspamd_config *cfg,
+ guint32 id);
+
+/**
+ * Finds settings id element and obtain reference count (must be unrefed by callee)
+ * @param cfg
+ * @param id
+ * @return
+ */
+struct rspamd_config_settings_elt *rspamd_config_find_settings_name_ref(
+ struct rspamd_config *cfg,
+ const gchar *name, gsize namelen);
+
+/**
+ * Returns action object by name
+ * @param cfg
+ * @param name
+ * @return
+ */
+struct rspamd_action *rspamd_config_get_action(struct rspamd_config *cfg,
+ const gchar *name);
+
+struct rspamd_action *rspamd_config_get_action_by_type(struct rspamd_config *cfg,
+ enum rspamd_action_type type);
+
+/**
+ * Iterate over all actions
+ * @param cfg
+ * @param func
+ * @param data
+ */
+void rspamd_config_actions_foreach(struct rspamd_config *cfg,
+ void (*func)(struct rspamd_action *act, void *d),
+ void *data);
+/**
+ * Iterate over all actions with index
+ * @param cfg
+ * @param func
+ * @param data
+ */
+void rspamd_config_actions_foreach_enumerate(struct rspamd_config *cfg,
+ void (*func)(int idx, struct rspamd_action *act, void *d),
+ void *data);
+
+/**
+ * Returns number of actions defined in the config
+ * @param cfg
+ * @return
+ */
+gsize rspamd_config_actions_size(struct rspamd_config *cfg);
+
+int rspamd_config_ev_backend_get(struct rspamd_config *cfg);
+const gchar *rspamd_config_ev_backend_to_string(int ev_backend, gboolean *effective);
+
+struct rspamd_external_libs_ctx;
+
+/**
+ * Initialize rspamd libraries
+ */
+struct rspamd_external_libs_ctx *rspamd_init_libs(void);
+
+/**
+ * Reset and initialize decompressor
+ * @param ctx
+ */
+gboolean rspamd_libs_reset_decompression(struct rspamd_external_libs_ctx *ctx);
+
+/**
+ * Reset and initialize compressor
+ * @param ctx
+ */
+gboolean rspamd_libs_reset_compression(struct rspamd_external_libs_ctx *ctx);
+
+/**
+ * Destroy external libraries context
+ */
+void rspamd_deinit_libs(struct rspamd_external_libs_ctx *ctx);
+
+/**
+ * Returns TRUE if an address belongs to some local address
+ */
+gboolean rspamd_ip_is_local_cfg(struct rspamd_config *cfg,
+ const rspamd_inet_addr_t *addr);
+
+/**
+ * Configure libraries
+ */
+gboolean rspamd_config_libs(struct rspamd_external_libs_ctx *ctx,
+ struct rspamd_config *cfg);
+
+
+#define msg_err_config(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ cfg->cfg_pool->tag.tagname, cfg->checksum, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_err_config_forced(...) rspamd_default_log_function((gint) G_LOG_LEVEL_CRITICAL | (gint) RSPAMD_LOG_FORCED, \
+ cfg->cfg_pool->tag.tagname, cfg->checksum, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_config(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ cfg->cfg_pool->tag.tagname, cfg->checksum, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_config(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ cfg->cfg_pool->tag.tagname, cfg->checksum, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+extern guint rspamd_config_log_id;
+#define msg_debug_config(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_config_log_id, "config", cfg->checksum, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ifdef CFG_FILE_H */
diff --git a/src/libserver/cfg_file_private.h b/src/libserver/cfg_file_private.h
new file mode 100644
index 0000000..8c9fc65
--- /dev/null
+++ b/src/libserver/cfg_file_private.h
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+#ifndef RSPAMD_CFG_FILE_PRIVATE_H
+#define RSPAMD_CFG_FILE_PRIVATE_H
+
+#include "cfg_file.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Action config definition
+ */
+struct rspamd_action {
+ enum rspamd_action_type action_type;
+ int flags; /* enum rspamd_action_flags */
+ guint priority;
+ gdouble threshold;
+ gchar *name;
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/cfg_rcl.cxx b/src/libserver/cfg_rcl.cxx
new file mode 100644
index 0000000..3ac7560
--- /dev/null
+++ b/src/libserver/cfg_rcl.cxx
@@ -0,0 +1,4110 @@
+/*
+ * 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 "lua/lua_common.h"
+#include "cfg_rcl.h"
+#include "rspamd.h"
+#include "cfg_file_private.h"
+#include "utlist.h"
+#include "cfg_file.h"
+#include "expression.h"
+#include "src/libserver/composites/composites.h"
+#include "libserver/worker_util.h"
+#include "unix-std.h"
+#include "cryptobox.h"
+#include "libutil/multipattern.h"
+#include "libmime/email_addr.h"
+#include "libmime/lang_detection.h"
+
+#include <string>
+#include <filesystem>
+#include <algorithm>// for std::transform
+#include <memory>
+#include "contrib/ankerl/unordered_dense.h"
+#include "fmt/core.h"
+#include "libutil/cxx/util.hxx"
+#include "libutil/cxx/file_util.hxx"
+#include "frozen/unordered_set.h"
+#include "frozen/string.h"
+
+#ifdef HAVE_SYSLOG_H
+#include <syslog.h>
+#endif
+
+#include <cmath>
+
+struct rspamd_rcl_default_handler_data {
+ struct rspamd_rcl_struct_parser pd;
+ std::string key;
+ rspamd_rcl_default_handler_t handler;
+};
+
+struct rspamd_rcl_sections_map;
+
+struct rspamd_rcl_section {
+ struct rspamd_rcl_sections_map *top{};
+ std::string name; /**< name of section */
+ std::optional<std::string> key_attr;
+ std::optional<std::string> default_key;
+ rspamd_rcl_handler_t handler{}; /**< handler of section attributes */
+ enum ucl_type type; /**< type of attribute */
+ bool required{}; /**< whether this param is required */
+ bool strict_type{}; /**< whether we need strict type */
+ mutable bool processed{}; /**< whether this section was processed */
+ ankerl::unordered_dense::map<std::string, std::shared_ptr<struct rspamd_rcl_section>> subsections;
+ ankerl::unordered_dense::map<std::string, struct rspamd_rcl_default_handler_data> default_parser; /**< generic parsing fields */
+ rspamd_rcl_section_fin_t fin{}; /** called at the end of section parsing */
+ gpointer fin_ud{};
+ ucl_object_t *doc_ref{}; /**< reference to the section's documentation */
+
+ virtual ~rspamd_rcl_section()
+ {
+ if (doc_ref) {
+ ucl_object_unref(doc_ref);
+ }
+ }
+};
+
+struct rspamd_worker_param_parser {
+ rspamd_rcl_default_handler_t handler; /**< handler function */
+ struct rspamd_rcl_struct_parser parser; /**< parser attributes */
+};
+
+struct rspamd_worker_cfg_parser {
+ struct pair_hash {
+ using is_avalanching = void;
+ template<class T1, class T2>
+ std::size_t operator()(const std::pair<T1, T2> &pair) const
+ {
+ return ankerl::unordered_dense::hash<T1>()(pair.first) ^ ankerl::unordered_dense::hash<T2>()(pair.second);
+ }
+ };
+ ankerl::unordered_dense::map<std::pair<std::string, gpointer>,
+ rspamd_worker_param_parser, pair_hash>
+ parsers; /**< parsers hash */
+ gint type; /**< workers quark */
+ gboolean (*def_obj_parser)(ucl_object_t *obj, gpointer ud); /**< default object parser */
+ gpointer def_ud;
+};
+
+struct rspamd_rcl_sections_map {
+ ankerl::unordered_dense::map<std::string, std::shared_ptr<struct rspamd_rcl_section>> sections;
+ std::vector<std::shared_ptr<struct rspamd_rcl_section>> sections_order;
+ ankerl::unordered_dense::map<int, struct rspamd_worker_cfg_parser> workers_parser;
+ ankerl::unordered_dense::set<std::string> lua_modules_seen;
+};
+
+static bool rspamd_rcl_process_section(struct rspamd_config *cfg,
+ const struct rspamd_rcl_section &sec,
+ gpointer ptr, const ucl_object_t *obj, rspamd_mempool_t *pool,
+ GError **err);
+static bool
+rspamd_rcl_section_parse_defaults(struct rspamd_config *cfg,
+ const struct rspamd_rcl_section &section,
+ rspamd_mempool_t *pool, const ucl_object_t *obj, gpointer ptr,
+ GError **err);
+
+/*
+ * Common section handlers
+ */
+static gboolean
+rspamd_rcl_logging_handler(rspamd_mempool_t *pool, const ucl_object_t *obj,
+ const gchar *key, gpointer ud, struct rspamd_rcl_section *section,
+ GError **err)
+{
+ const ucl_object_t *val;
+ const gchar *facility = nullptr, *log_type = nullptr, *log_level = nullptr;
+ auto *cfg = (struct rspamd_config *) ud;
+
+ val = ucl_object_lookup(obj, "type");
+ if (val != nullptr && ucl_object_tostring_safe(val, &log_type)) {
+ if (g_ascii_strcasecmp(log_type, "file") == 0) {
+ /* Need to get filename */
+ val = ucl_object_lookup(obj, "filename");
+ if (val == nullptr || val->type != UCL_STRING) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ ENOENT,
+ "filename attribute must be specified for file logging type");
+ return FALSE;
+ }
+ cfg->log_type = RSPAMD_LOG_FILE;
+ cfg->log_file = rspamd_mempool_strdup(cfg->cfg_pool,
+ ucl_object_tostring(val));
+ }
+ else if (g_ascii_strcasecmp(log_type, "syslog") == 0) {
+ /* Need to get facility */
+#ifdef HAVE_SYSLOG_H
+ cfg->log_facility = LOG_DAEMON;
+ cfg->log_type = RSPAMD_LOG_SYSLOG;
+ val = ucl_object_lookup(obj, "facility");
+ if (val != nullptr && ucl_object_tostring_safe(val, &facility)) {
+ if (g_ascii_strcasecmp(facility, "LOG_AUTH") == 0 ||
+ g_ascii_strcasecmp(facility, "auth") == 0) {
+ cfg->log_facility = LOG_AUTH;
+ }
+ else if (g_ascii_strcasecmp(facility, "LOG_CRON") == 0 ||
+ g_ascii_strcasecmp(facility, "cron") == 0) {
+ cfg->log_facility = LOG_CRON;
+ }
+ else if (g_ascii_strcasecmp(facility, "LOG_DAEMON") == 0 ||
+ g_ascii_strcasecmp(facility, "daemon") == 0) {
+ cfg->log_facility = LOG_DAEMON;
+ }
+ else if (g_ascii_strcasecmp(facility, "LOG_MAIL") == 0 ||
+ g_ascii_strcasecmp(facility, "mail") == 0) {
+ cfg->log_facility = LOG_MAIL;
+ }
+ else if (g_ascii_strcasecmp(facility, "LOG_USER") == 0 ||
+ g_ascii_strcasecmp(facility, "user") == 0) {
+ cfg->log_facility = LOG_USER;
+ }
+ else if (g_ascii_strcasecmp(facility, "LOG_LOCAL0") == 0 ||
+ g_ascii_strcasecmp(facility, "local0") == 0) {
+ cfg->log_facility = LOG_LOCAL0;
+ }
+ else if (g_ascii_strcasecmp(facility, "LOG_LOCAL1") == 0 ||
+ g_ascii_strcasecmp(facility, "local1") == 0) {
+ cfg->log_facility = LOG_LOCAL1;
+ }
+ else if (g_ascii_strcasecmp(facility, "LOG_LOCAL2") == 0 ||
+ g_ascii_strcasecmp(facility, "local2") == 0) {
+ cfg->log_facility = LOG_LOCAL2;
+ }
+ else if (g_ascii_strcasecmp(facility, "LOG_LOCAL3") == 0 ||
+ g_ascii_strcasecmp(facility, "local3") == 0) {
+ cfg->log_facility = LOG_LOCAL3;
+ }
+ else if (g_ascii_strcasecmp(facility, "LOG_LOCAL4") == 0 ||
+ g_ascii_strcasecmp(facility, "local4") == 0) {
+ cfg->log_facility = LOG_LOCAL4;
+ }
+ else if (g_ascii_strcasecmp(facility, "LOG_LOCAL5") == 0 ||
+ g_ascii_strcasecmp(facility, "local5") == 0) {
+ cfg->log_facility = LOG_LOCAL5;
+ }
+ else if (g_ascii_strcasecmp(facility, "LOG_LOCAL6") == 0 ||
+ g_ascii_strcasecmp(facility, "local6") == 0) {
+ cfg->log_facility = LOG_LOCAL6;
+ }
+ else if (g_ascii_strcasecmp(facility, "LOG_LOCAL7") == 0 ||
+ g_ascii_strcasecmp(facility, "local7") == 0) {
+ cfg->log_facility = LOG_LOCAL7;
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "invalid log facility: %s",
+ facility);
+ return FALSE;
+ }
+ }
+#endif
+ }
+ else if (g_ascii_strcasecmp(log_type,
+ "stderr") == 0 ||
+ g_ascii_strcasecmp(log_type, "console") == 0) {
+ cfg->log_type = RSPAMD_LOG_CONSOLE;
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "invalid log type: %s",
+ log_type);
+ return FALSE;
+ }
+ }
+ else {
+ /* No type specified */
+ msg_warn_config(
+ "logging type is not specified correctly, log output to the console");
+ }
+
+ /* Handle log level */
+ val = ucl_object_lookup(obj, "level");
+ if (val != nullptr && ucl_object_tostring_safe(val, &log_level)) {
+ if (g_ascii_strcasecmp(log_level, "error") == 0) {
+ cfg->log_level = G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL;
+ }
+ else if (g_ascii_strcasecmp(log_level, "warning") == 0) {
+ cfg->log_level = G_LOG_LEVEL_WARNING;
+ }
+ else if (g_ascii_strcasecmp(log_level, "info") == 0) {
+ cfg->log_level = G_LOG_LEVEL_INFO | G_LOG_LEVEL_MESSAGE;
+ }
+ else if (g_ascii_strcasecmp(log_level, "message") == 0 ||
+ g_ascii_strcasecmp(log_level, "notice") == 0) {
+ cfg->log_level = G_LOG_LEVEL_MESSAGE;
+ }
+ else if (g_ascii_strcasecmp(log_level, "silent") == 0) {
+ cfg->log_level = G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_INFO;
+ cfg->log_silent_workers = TRUE;
+ }
+ else if (g_ascii_strcasecmp(log_level, "debug") == 0) {
+ cfg->log_level = G_LOG_LEVEL_DEBUG;
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "invalid log level: %s",
+ log_level);
+ return FALSE;
+ }
+ }
+
+ /* Handle flags */
+ val = ucl_object_lookup_any(obj, "color", "log_color", nullptr);
+ if (val && ucl_object_toboolean(val)) {
+ cfg->log_flags |= RSPAMD_LOG_FLAG_COLOR;
+ }
+
+ val = ucl_object_lookup_any(obj, "severity", "log_severity", nullptr);
+ if (val && ucl_object_toboolean(val)) {
+ cfg->log_flags |= RSPAMD_LOG_FLAG_SEVERITY;
+ }
+
+ val = ucl_object_lookup_any(obj, "systemd", "log_systemd", nullptr);
+ if (val && ucl_object_toboolean(val)) {
+ cfg->log_flags |= RSPAMD_LOG_FLAG_SYSTEMD;
+ }
+
+ val = ucl_object_lookup_any(obj, "json", "log_json", nullptr);
+ if (val && ucl_object_toboolean(val)) {
+ cfg->log_flags |= RSPAMD_LOG_FLAG_JSON;
+ }
+
+ val = ucl_object_lookup(obj, "log_re_cache");
+ if (val && ucl_object_toboolean(val)) {
+ cfg->log_flags |= RSPAMD_LOG_FLAG_RE_CACHE;
+ }
+
+ val = ucl_object_lookup_any(obj, "usec", "log_usec", nullptr);
+ if (val && ucl_object_toboolean(val)) {
+ cfg->log_flags |= RSPAMD_LOG_FLAG_USEC;
+ }
+
+ return rspamd_rcl_section_parse_defaults(cfg, *section, cfg->cfg_pool, obj,
+ (void *) cfg, err);
+}
+
+static gboolean
+rspamd_rcl_options_handler(rspamd_mempool_t *pool, const ucl_object_t *obj,
+ const gchar *key, gpointer ud,
+ struct rspamd_rcl_section *section, GError **err)
+{
+ const ucl_object_t *dns, *upstream, *neighbours;
+ auto *cfg = (struct rspamd_config *) ud;
+
+ auto maybe_subsection = rspamd::find_map(section->subsections, "dns");
+
+ dns = ucl_object_lookup(obj, "dns");
+ if (maybe_subsection && dns != nullptr) {
+ if (!rspamd_rcl_section_parse_defaults(cfg,
+ *maybe_subsection.value().get(), cfg->cfg_pool, dns,
+ cfg, err)) {
+ return FALSE;
+ }
+ }
+
+ maybe_subsection = rspamd::find_map(section->subsections, "upstream");
+
+ upstream = ucl_object_lookup_any(obj, "upstream", "upstreams", nullptr);
+ if (maybe_subsection && upstream != nullptr) {
+ if (!rspamd_rcl_section_parse_defaults(cfg,
+ *maybe_subsection.value().get(), cfg->cfg_pool,
+ upstream, cfg, err)) {
+ return FALSE;
+ }
+ }
+
+ maybe_subsection = rspamd::find_map(section->subsections, "neighbours");
+
+ neighbours = ucl_object_lookup(obj, "neighbours");
+ if (maybe_subsection && neighbours != nullptr) {
+ const ucl_object_t *cur;
+
+ LL_FOREACH(neighbours, cur)
+ {
+ if (!rspamd_rcl_process_section(cfg, *maybe_subsection.value().get(), cfg, cur,
+ pool, err)) {
+ return FALSE;
+ }
+ }
+ }
+
+ const auto *gtube_patterns = ucl_object_lookup(obj, "gtube_patterns");
+ if (gtube_patterns != nullptr && ucl_object_type(gtube_patterns) == UCL_STRING) {
+ auto gtube_st = std::string{ucl_object_tostring(gtube_patterns)};
+ std::transform(gtube_st.begin(), gtube_st.end(), gtube_st.begin(), [](const auto c) -> int {
+ if (c <= 'Z' && c >= 'A')
+ return c - ('Z' - 'z');
+ return c;
+ });
+
+
+ if (gtube_st == "all") {
+ cfg->gtube_patterns_policy = RSPAMD_GTUBE_ALL;
+ }
+ else if (gtube_st == "reject") {
+ cfg->gtube_patterns_policy = RSPAMD_GTUBE_REJECT;
+ }
+ else if (gtube_st == "disabled" || gtube_st == "disable") {
+ cfg->gtube_patterns_policy = RSPAMD_GTUBE_DISABLED;
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "invalid GTUBE patterns policy: %s",
+ gtube_st.c_str());
+ return FALSE;
+ }
+ }
+ else if (auto *enable_test_patterns = ucl_object_lookup(obj, "enable_test_patterns"); enable_test_patterns != nullptr) {
+ /* Legacy setting */
+ if (!!ucl_object_toboolean(enable_test_patterns)) {
+ cfg->gtube_patterns_policy = RSPAMD_GTUBE_ALL;
+ }
+ }
+
+ if (rspamd_rcl_section_parse_defaults(cfg,
+ *section, cfg->cfg_pool, obj,
+ cfg, err)) {
+ /* We need to init this early */
+ rspamd_multipattern_library_init(cfg->hs_cache_dir);
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+struct rspamd_rcl_symbol_data {
+ struct rspamd_symbols_group *gr;
+ struct rspamd_config *cfg;
+};
+
+static gboolean
+rspamd_rcl_group_handler(rspamd_mempool_t *pool, const ucl_object_t *obj,
+ const gchar *key, gpointer ud,
+ struct rspamd_rcl_section *section, GError **err)
+{
+ auto *cfg = static_cast<rspamd_config *>(ud);
+
+ g_assert(key != nullptr);
+
+ auto *gr = static_cast<rspamd_symbols_group *>(g_hash_table_lookup(cfg->groups, key));
+
+ if (gr == nullptr) {
+ gr = rspamd_config_new_group(cfg, key);
+ }
+
+ if (!rspamd_rcl_section_parse_defaults(cfg, *section, pool, obj,
+ gr, err)) {
+ return FALSE;
+ }
+
+ if (const auto *elt = ucl_object_lookup(obj, "one_shot"); elt != nullptr) {
+ if (ucl_object_type(elt) != UCL_BOOLEAN) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "one_shot attribute is not boolean for symbol: '%s'",
+ key);
+
+ return FALSE;
+ }
+ if (ucl_object_toboolean(elt)) {
+ gr->flags |= RSPAMD_SYMBOL_GROUP_ONE_SHOT;
+ }
+ }
+
+ if (const auto *elt = ucl_object_lookup(obj, "disabled"); elt != nullptr) {
+ if (ucl_object_type(elt) != UCL_BOOLEAN) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "disabled attribute is not boolean for symbol: '%s'",
+ key);
+
+ return FALSE;
+ }
+ if (ucl_object_toboolean(elt)) {
+ gr->flags |= RSPAMD_SYMBOL_GROUP_DISABLED;
+ }
+ }
+
+ if (const auto *elt = ucl_object_lookup(obj, "enabled"); elt != nullptr) {
+ if (ucl_object_type(elt) != UCL_BOOLEAN) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "enabled attribute is not boolean for symbol: '%s'",
+ key);
+
+ return FALSE;
+ }
+ if (!ucl_object_toboolean(elt)) {
+ gr->flags |= RSPAMD_SYMBOL_GROUP_DISABLED;
+ }
+ }
+
+ if (const auto *elt = ucl_object_lookup(obj, "public"); elt != nullptr) {
+ if (ucl_object_type(elt) != UCL_BOOLEAN) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "public attribute is not boolean for symbol: '%s'",
+ key);
+
+ return FALSE;
+ }
+ if (ucl_object_toboolean(elt)) {
+ gr->flags |= RSPAMD_SYMBOL_GROUP_PUBLIC;
+ }
+ }
+
+ if (const auto *elt = ucl_object_lookup(obj, "private"); elt != nullptr) {
+ if (ucl_object_type(elt) != UCL_BOOLEAN) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "private attribute is not boolean for symbol: '%s'",
+ key);
+
+ return FALSE;
+ }
+ if (!ucl_object_toboolean(elt)) {
+ gr->flags |= RSPAMD_SYMBOL_GROUP_PUBLIC;
+ }
+ }
+
+
+ if (const auto *elt = ucl_object_lookup(obj, "description"); elt != nullptr) {
+ gr->description = rspamd_mempool_strdup(cfg->cfg_pool,
+ ucl_object_tostring(elt));
+ }
+
+ struct rspamd_rcl_symbol_data sd = {
+ .gr = gr,
+ .cfg = cfg,
+ };
+
+ /* Handle symbols */
+ if (const auto *val = ucl_object_lookup(obj, "symbols"); val != nullptr && ucl_object_type(val) == UCL_OBJECT) {
+ auto subsection = rspamd::find_map(section->subsections, "symbols");
+
+ g_assert(subsection.has_value());
+ if (!rspamd_rcl_process_section(cfg, *subsection.value().get(), &sd, val,
+ pool, err)) {
+
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_rcl_symbol_handler(rspamd_mempool_t *pool, const ucl_object_t *obj,
+ const gchar *key, gpointer ud,
+ struct rspamd_rcl_section *section, GError **err)
+{
+ auto *sd = static_cast<rspamd_rcl_symbol_data *>(ud);
+ struct rspamd_config *cfg;
+ const ucl_object_t *elt;
+ const gchar *description = nullptr;
+ gdouble score = NAN;
+ guint priority = 1, flags = 0;
+ gint nshots = 0;
+
+ g_assert(key != nullptr);
+ cfg = sd->cfg;
+
+ if ((elt = ucl_object_lookup(obj, "one_shot")) != nullptr) {
+ if (ucl_object_type(elt) != UCL_BOOLEAN) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "one_shot attribute is not boolean for symbol: '%s'",
+ key);
+
+ return FALSE;
+ }
+ if (ucl_object_toboolean(elt)) {
+ nshots = 1;
+ }
+ }
+
+ if ((elt = ucl_object_lookup(obj, "any_shot")) != nullptr) {
+ if (ucl_object_type(elt) != UCL_BOOLEAN) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "any_shot attribute is not boolean for symbol: '%s'",
+ key);
+
+ return FALSE;
+ }
+ if (ucl_object_toboolean(elt)) {
+ nshots = -1;
+ }
+ }
+
+ if ((elt = ucl_object_lookup(obj, "one_param")) != nullptr) {
+ if (ucl_object_type(elt) != UCL_BOOLEAN) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "one_param attribute is not boolean for symbol: '%s'",
+ key);
+
+ return FALSE;
+ }
+
+ if (ucl_object_toboolean(elt)) {
+ flags |= RSPAMD_SYMBOL_FLAG_ONEPARAM;
+ }
+ }
+
+ if ((elt = ucl_object_lookup(obj, "ignore")) != nullptr) {
+ if (ucl_object_type(elt) != UCL_BOOLEAN) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "ignore attribute is not boolean for symbol: '%s'",
+ key);
+
+ return FALSE;
+ }
+
+ if (ucl_object_toboolean(elt)) {
+ flags |= RSPAMD_SYMBOL_FLAG_IGNORE_METRIC;
+ }
+ }
+
+ if ((elt = ucl_object_lookup(obj, "enabled")) != nullptr) {
+ if (ucl_object_type(elt) != UCL_BOOLEAN) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "enabled attribute is not boolean for symbol: '%s'",
+ key);
+
+ return FALSE;
+ }
+
+ if (!ucl_object_toboolean(elt)) {
+ flags |= RSPAMD_SYMBOL_FLAG_DISABLED;
+ }
+ }
+
+ if ((elt = ucl_object_lookup(obj, "nshots")) != nullptr) {
+ if (ucl_object_type(elt) != UCL_FLOAT && ucl_object_type(elt) != UCL_INT) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "nshots attribute is not numeric for symbol: '%s'",
+ key);
+
+ return FALSE;
+ }
+
+ nshots = ucl_object_toint(elt);
+ }
+
+ elt = ucl_object_lookup_any(obj, "score", "weight", nullptr);
+ if (elt) {
+ if (ucl_object_type(elt) != UCL_FLOAT && ucl_object_type(elt) != UCL_INT) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "score attribute is not numeric for symbol: '%s'",
+ key);
+
+ return FALSE;
+ }
+
+ score = ucl_object_todouble(elt);
+ }
+
+ elt = ucl_object_lookup(obj, "priority");
+ if (elt) {
+ if (ucl_object_type(elt) != UCL_FLOAT && ucl_object_type(elt) != UCL_INT) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "priority attribute is not numeric for symbol: '%s'",
+ key);
+
+ return FALSE;
+ }
+
+ priority = ucl_object_toint(elt);
+ }
+ else {
+ priority = ucl_object_get_priority(obj) + 1;
+ }
+
+ elt = ucl_object_lookup(obj, "description");
+ if (elt) {
+ description = ucl_object_tostring(elt);
+ }
+
+ if (sd->gr) {
+ rspamd_config_add_symbol(cfg, key, score,
+ description, sd->gr->name, flags, priority, nshots);
+ }
+ else {
+ rspamd_config_add_symbol(cfg, key, score,
+ description, nullptr, flags, priority, nshots);
+ }
+
+ elt = ucl_object_lookup(obj, "groups");
+
+ if (elt) {
+ ucl_object_iter_t gr_it;
+ const ucl_object_t *cur_gr;
+
+ gr_it = ucl_object_iterate_new(elt);
+
+ while ((cur_gr = ucl_object_iterate_safe(gr_it, true)) != nullptr) {
+ rspamd_config_add_symbol_group(cfg, key,
+ ucl_object_tostring(cur_gr));
+ }
+
+ ucl_object_iterate_free(gr_it);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_rcl_actions_handler(rspamd_mempool_t *pool, const ucl_object_t *obj,
+ const gchar *key, gpointer ud,
+ struct rspamd_rcl_section *section, GError **err)
+{
+ auto *cfg = static_cast<rspamd_config *>(ud);
+ const ucl_object_t *cur;
+ ucl_object_iter_t it;
+
+ it = ucl_object_iterate_new(obj);
+
+ while ((cur = ucl_object_iterate_safe(it, true)) != nullptr) {
+ gint type = ucl_object_type(cur);
+
+ if (type == UCL_NULL) {
+ rspamd_config_maybe_disable_action(cfg, ucl_object_key(cur),
+ ucl_object_get_priority(cur));
+ }
+ else if (type == UCL_OBJECT || type == UCL_FLOAT || type == UCL_INT) {
+ /* Exceptions */
+ auto default_elt = false;
+
+ for (const auto &[name, def_elt]: section->default_parser) {
+ if (def_elt.key == ucl_object_key(cur)) {
+ default_elt = true;
+ break;
+ }
+ }
+
+ if (default_elt) {
+ continue;
+ }
+
+ /* Something non-default */
+ if (!rspamd_config_set_action_score(cfg,
+ ucl_object_key(cur),
+ cur)) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "invalid action definition for: '%s'",
+ ucl_object_key(cur));
+ ucl_object_iterate_free(it);
+
+ return FALSE;
+ }
+ }
+ }
+
+ ucl_object_iterate_free(it);
+
+ return rspamd_rcl_section_parse_defaults(cfg, *section, pool, obj, cfg, err);
+}
+constexpr const auto known_worker_attributes = frozen::make_unordered_set<frozen::string>({
+ "bind_socket",
+ "listen",
+ "bind",
+ "count",
+ "max_files",
+ "max_core",
+ "enabled",
+});
+static gboolean
+rspamd_rcl_worker_handler(rspamd_mempool_t *pool, const ucl_object_t *obj,
+ const gchar *key, gpointer ud,
+ struct rspamd_rcl_section *section, GError **err)
+{
+ auto *cfg = static_cast<rspamd_config *>(ud);
+
+ g_assert(key != nullptr);
+ const auto *worker_type = key;
+
+ auto qtype = g_quark_try_string(worker_type);
+ if (qtype == 0) {
+ msg_err_config("unknown worker type: %s", worker_type);
+ return FALSE;
+ }
+
+ auto *wrk = rspamd_config_new_worker(cfg, nullptr);
+ wrk->options = ucl_object_copy(obj);
+ wrk->worker = rspamd_get_worker_by_type(cfg, qtype);
+
+ if (wrk->worker == nullptr) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "unknown worker type: %s",
+ worker_type);
+ return FALSE;
+ }
+
+ wrk->type = qtype;
+
+ if (wrk->worker->worker_init_func) {
+ wrk->ctx = wrk->worker->worker_init_func(cfg);
+ }
+
+ const auto *val = ucl_object_lookup_any(obj, "bind_socket", "listen", "bind", nullptr);
+ /* This name is more logical */
+ if (val != nullptr) {
+ auto it = ucl_object_iterate_new(val);
+ const ucl_object_t *cur;
+ const char *worker_bind = nullptr;
+
+ while ((cur = ucl_object_iterate_safe(it, true)) != nullptr) {
+ if (!ucl_object_tostring_safe(cur, &worker_bind)) {
+ continue;
+ }
+ if (!rspamd_parse_bind_line(cfg, wrk, worker_bind)) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot parse bind line: %s",
+ worker_bind);
+ ucl_object_iterate_free(it);
+ return FALSE;
+ }
+ }
+
+ ucl_object_iterate_free(it);
+ }
+
+ if (!rspamd_rcl_section_parse_defaults(cfg, *section, cfg->cfg_pool, obj,
+ wrk, err)) {
+ return FALSE;
+ }
+
+ /* Parse other attributes */
+ auto maybe_wparser = rspamd::find_map(section->top->workers_parser, wrk->type);
+
+ if (maybe_wparser && obj->type == UCL_OBJECT) {
+ auto &wparser = maybe_wparser.value().get();
+ auto it = ucl_object_iterate_new(obj);
+ const ucl_object_t *cur;
+
+ while ((cur = ucl_object_iterate_full(it, UCL_ITERATE_EXPLICIT)) != nullptr) {
+ auto srch = std::make_pair(ucl_object_key(cur), (gpointer) wrk->ctx);
+ auto maybe_specific = rspamd::find_map(wparser.parsers, srch);
+
+ if (maybe_specific) {
+ auto &whandler = maybe_specific.value().get();
+ const ucl_object_t *cur_obj;
+
+ LL_FOREACH(cur, cur_obj)
+ {
+ if (!whandler.handler(cfg->cfg_pool,
+ cur_obj,
+ (void *) &whandler.parser,
+ section,
+ err)) {
+
+ ucl_object_iterate_free(it);
+ return FALSE;
+ }
+
+ if (!(whandler.parser.flags & RSPAMD_CL_FLAG_MULTIPLE)) {
+ break;
+ }
+ }
+ }
+ else if (!(wrk->worker->flags & RSPAMD_WORKER_NO_STRICT_CONFIG) &&
+ known_worker_attributes.find(std::string_view{ucl_object_key(cur)}) == known_worker_attributes.end()) {
+ msg_warn_config("unknown worker attribute: %s; worker type: %s", ucl_object_key(cur), worker_type);
+ }
+ }
+
+ ucl_object_iterate_free(it);
+
+ if (wparser.def_obj_parser != nullptr) {
+ auto *robj = ucl_object_ref(obj);
+
+ if (!wparser.def_obj_parser(robj, wparser.def_ud)) {
+ ucl_object_unref(robj);
+
+ return FALSE;
+ }
+
+ ucl_object_unref(robj);
+ }
+ }
+
+ cfg->workers = g_list_prepend(cfg->workers, wrk);
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_rcl_lua_handler(rspamd_mempool_t *pool, const ucl_object_t *obj,
+ const gchar *key, gpointer ud,
+ struct rspamd_rcl_section *section, GError **err)
+{
+ namespace fs = std::filesystem;
+ auto *cfg = static_cast<rspamd_config *>(ud);
+ auto lua_src = fs::path{ucl_object_tostring(obj)};
+ auto *L = RSPAMD_LUA_CFG_STATE(cfg);
+ std::error_code ec1;
+
+ auto lua_dir = fs::weakly_canonical(lua_src.parent_path(), ec1);
+ auto lua_file = lua_src.filename();
+
+ if (!ec1 && !lua_dir.empty() && !lua_file.empty()) {
+ auto cur_dir = fs::current_path(ec1);
+ if (!ec1 && !cur_dir.empty() && ::chdir(lua_dir.c_str()) != -1) {
+ /* Push traceback function */
+ lua_pushcfunction(L, &rspamd_lua_traceback);
+ auto err_idx = lua_gettop(L);
+
+ /* Load file */
+ if (luaL_loadfile(L, lua_file.c_str()) != 0) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot load lua file %s: %s",
+ lua_src.c_str(),
+ lua_tostring(L, -1));
+ if (::chdir(cur_dir.c_str()) == -1) {
+ msg_err_config("cannot chdir to %s: %s", cur_dir.c_str(),
+ strerror(errno));
+ }
+
+ return FALSE;
+ }
+
+ /* Now do it */
+ if (lua_pcall(L, 0, 0, err_idx) != 0) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot init lua file %s: %s",
+ lua_src.c_str(),
+ lua_tostring(L, -1));
+ lua_settop(L, 0);
+
+ if (::chdir(cur_dir.c_str()) == -1) {
+ msg_err_config("cannot chdir to %s: %s", cur_dir.c_str(),
+ strerror(errno));
+ }
+
+ return FALSE;
+ }
+
+ lua_pop(L, 1);
+ }
+ else {
+ g_set_error(err, CFG_RCL_ERROR, ENOENT, "cannot chdir to %s: %s",
+ lua_dir.c_str(), strerror(errno));
+ if (::chdir(cur_dir.c_str()) == -1) {
+ msg_err_config("cannot chdir back to %s: %s", cur_dir.c_str(), strerror(errno));
+ }
+
+ return FALSE;
+ }
+ if (::chdir(cur_dir.c_str()) == -1) {
+ msg_err_config("cannot chdir back to %s: %s", cur_dir.c_str(), strerror(errno));
+ }
+ }
+ else {
+
+ g_set_error(err, CFG_RCL_ERROR, ENOENT, "cannot find to %s: %s",
+ lua_src.c_str(), strerror(errno));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static int
+rspamd_lua_mod_sort_fn(gconstpointer a, gconstpointer b)
+{
+ auto *m1 = *(const script_module **) a;
+ auto *m2 = *(const script_module **) b;
+
+ return strcmp(m1->name, m2->name);
+}
+
+gboolean
+rspamd_rcl_add_lua_plugins_path(struct rspamd_rcl_sections_map *sections,
+ struct rspamd_config *cfg,
+ const gchar *path,
+ gboolean main_path,
+ GError **err)
+{
+ namespace fs = std::filesystem;
+ auto dir = fs::path{path};
+ std::error_code ec;
+
+ auto add_single_file = [&](const fs::path &fpath) -> bool {
+ auto fname = fpath.filename();
+ auto modname = fname.string();
+
+ if (fname.has_extension()) {
+ modname = modname.substr(0, modname.size() - fname.extension().native().size());
+ }
+ auto *cur_mod = rspamd_mempool_alloc_type(cfg->cfg_pool,
+ struct script_module);
+ cur_mod->path = rspamd_mempool_strdup(cfg->cfg_pool, fpath.c_str());
+ cur_mod->name = rspamd_mempool_strdup(cfg->cfg_pool, modname.c_str());
+
+ if (sections->lua_modules_seen.contains(modname)) {
+ msg_info_config("already seen module %s, skip %s",
+ cur_mod->name, cur_mod->path);
+ return false;
+ }
+
+ g_ptr_array_add(cfg->script_modules, cur_mod);
+ sections->lua_modules_seen.insert(fname.string());
+
+ return true;
+ };
+
+ if (fs::is_regular_file(dir, ec) && dir.has_extension() && dir.extension() == ".lua") {
+ add_single_file(dir);
+ }
+ else if (!fs::is_directory(dir, ec)) {
+ if (!fs::exists(dir) && !main_path) {
+ msg_debug_config("optional plugins path %s is absent, skip it", path);
+
+ return TRUE;
+ }
+
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ errno,
+ "invalid lua path spec %s, %s",
+ path,
+ ec.message().c_str());
+ return FALSE;
+ }
+ else {
+ /* Handle directory */
+ for (const auto &p: fs::recursive_directory_iterator(dir, ec)) {
+ auto fpath = p.path().string();
+ if (p.is_regular_file() && fpath.ends_with(".lua")) {
+ add_single_file(p.path());
+ }
+ }
+ }
+
+ g_ptr_array_sort(cfg->script_modules, rspamd_lua_mod_sort_fn);
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_rcl_modules_handler(rspamd_mempool_t *pool, const ucl_object_t *obj,
+ const gchar *key, gpointer ud,
+ struct rspamd_rcl_section *section, GError **err)
+{
+ auto *cfg = static_cast<rspamd_config *>(ud);
+ const char *data;
+
+ if (obj->type == UCL_OBJECT) {
+ const auto *val = ucl_object_lookup(obj, "path");
+
+ if (val) {
+ const auto *cur = val;
+ LL_FOREACH(val, cur)
+ {
+ if (ucl_object_tostring_safe(cur, &data)) {
+ if (!rspamd_rcl_add_lua_plugins_path(section->top,
+ cfg,
+ data,
+ TRUE,
+ err)) {
+ return FALSE;
+ }
+ }
+ }
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "path attribute is missing");
+
+ return FALSE;
+ }
+
+ val = ucl_object_lookup(obj, "fallback_path");
+
+ if (val) {
+ const auto *cur = val;
+ LL_FOREACH(val, cur)
+ {
+ if (ucl_object_tostring_safe(cur, &data)) {
+ if (!rspamd_rcl_add_lua_plugins_path(section->top,
+ cfg,
+ data,
+ FALSE,
+ err)) {
+
+ return FALSE;
+ }
+ }
+ }
+ }
+
+ val = ucl_object_lookup(obj, "try_path");
+
+ if (val) {
+ const auto *cur = val;
+ LL_FOREACH(val, cur)
+ {
+ if (ucl_object_tostring_safe(cur, &data)) {
+ if (!rspamd_rcl_add_lua_plugins_path(section->top,
+ cfg,
+ data,
+ FALSE,
+ err)) {
+
+ return FALSE;
+ }
+ }
+ }
+ }
+ }
+ else if (ucl_object_tostring_safe(obj, &data)) {
+ if (!rspamd_rcl_add_lua_plugins_path(section->top, cfg, data, TRUE, err)) {
+ return FALSE;
+ }
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "module parameter has wrong type (must be an object or a string)");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+struct statfile_parser_data {
+ struct rspamd_config *cfg;
+ struct rspamd_classifier_config *ccf;
+};
+
+static gboolean
+rspamd_rcl_statfile_handler(rspamd_mempool_t *pool, const ucl_object_t *obj,
+ const gchar *key, gpointer ud,
+ struct rspamd_rcl_section *section, GError **err)
+{
+ auto *stud = (struct statfile_parser_data *) ud;
+ GList *labels;
+
+ g_assert(key != nullptr);
+
+ auto *cfg = stud->cfg;
+ auto *ccf = stud->ccf;
+
+ auto *st = rspamd_config_new_statfile(cfg, nullptr);
+ st->symbol = rspamd_mempool_strdup(cfg->cfg_pool, key);
+
+ if (rspamd_rcl_section_parse_defaults(cfg, *section, pool, obj, st, err)) {
+ ccf->statfiles = rspamd_mempool_glist_prepend(pool, ccf->statfiles, st);
+
+ if (st->label != nullptr) {
+ labels = (GList *) g_hash_table_lookup(ccf->labels, st->label);
+ if (labels != nullptr) {
+ /* Must use append to preserve the head stored in the hash table */
+ labels = g_list_append(labels, st);
+ }
+ else {
+ g_hash_table_insert(ccf->labels, st->label,
+ g_list_prepend(nullptr, st));
+ }
+ }
+
+ if (st->symbol != nullptr) {
+ g_hash_table_insert(cfg->classifiers_symbols, st->symbol, st);
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "statfile must have a symbol defined");
+ return FALSE;
+ }
+
+ st->opts = (ucl_object_t *) obj;
+ st->clcf = ccf;
+
+ const auto *val = ucl_object_lookup(obj, "spam");
+ if (val == nullptr) {
+ msg_info_config(
+ "statfile %s has no explicit 'spam' setting, trying to guess by symbol",
+ st->symbol);
+ if (rspamd_substring_search_caseless(st->symbol,
+ strlen(st->symbol), "spam", 4) != -1) {
+ st->is_spam = TRUE;
+ }
+ else if (rspamd_substring_search_caseless(st->symbol,
+ strlen(st->symbol), "ham", 3) != -1) {
+ st->is_spam = FALSE;
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot guess spam setting from %s",
+ st->symbol);
+ return FALSE;
+ }
+ msg_info_config("guessed that statfile with symbol %s is %s",
+ st->symbol,
+ st->is_spam ? "spam" : "ham");
+ }
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+rspamd_rcl_classifier_handler(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ const gchar *key,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *cfg = static_cast<rspamd_config *>(ud);
+
+ g_assert(key != nullptr);
+ auto *ccf = rspamd_config_new_classifier(cfg, nullptr);
+ auto *tkcf = (rspamd_tokenizer_config *) nullptr;
+
+ ccf->classifier = rspamd_mempool_strdup(cfg->cfg_pool, key);
+
+ if (rspamd_rcl_section_parse_defaults(cfg, *section, cfg->cfg_pool, obj,
+ ccf, err)) {
+
+ auto stat_section = rspamd::find_map(section->subsections, "statfile");
+
+ if (ccf->classifier == nullptr) {
+ ccf->classifier = rspamd_mempool_strdup(cfg->cfg_pool, "bayes");
+ }
+
+ if (ccf->name == nullptr) {
+ ccf->name = ccf->classifier;
+ }
+
+ auto it = ucl_object_iterate_new(obj);
+ const auto *val = obj;
+ auto res = TRUE;
+
+ while ((val = ucl_object_iterate_safe(it, true)) != nullptr && res) {
+ const auto *st_key = ucl_object_key(val);
+
+ if (st_key != nullptr) {
+ if (g_ascii_strcasecmp(st_key, "statfile") == 0) {
+ const auto *cur = val;
+ LL_FOREACH(val, cur)
+ {
+ struct statfile_parser_data stud = {.cfg = cfg, .ccf = ccf};
+ res = rspamd_rcl_process_section(cfg, *stat_section.value().get(), &stud,
+ cur, cfg->cfg_pool, err);
+
+ if (!res) {
+ ucl_object_iterate_free(it);
+
+ return FALSE;
+ }
+ }
+ }
+ else if (g_ascii_strcasecmp(st_key, "tokenizer") == 0) {
+ tkcf = rspamd_mempool_alloc0_type(cfg->cfg_pool, rspamd_tokenizer_config);
+
+ if (ucl_object_type(val) == UCL_STRING) {
+ tkcf->name = ucl_object_tostring(val);
+ }
+ else if (ucl_object_type(val) == UCL_OBJECT) {
+ const auto *cur = ucl_object_lookup(val, "name");
+ if (cur != nullptr) {
+ tkcf->name = ucl_object_tostring(cur);
+ tkcf->opts = val;
+ }
+ else {
+ cur = ucl_object_lookup(val, "type");
+ if (cur != nullptr) {
+ tkcf->name = ucl_object_tostring(cur);
+ tkcf->opts = val;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ ucl_object_iterate_free(it);
+ }
+ else {
+ msg_err_config("fatal configuration error, cannot parse statfile definition");
+ }
+
+ if (tkcf == nullptr) {
+ tkcf = rspamd_mempool_alloc0_type(cfg->cfg_pool, rspamd_tokenizer_config);
+ tkcf->name = nullptr;
+ }
+
+ ccf->tokenizer = tkcf;
+
+ /* Handle lua conditions */
+ const auto *val = ucl_object_lookup_any(obj, "learn_condition", nullptr);
+
+ if (val) {
+ const auto *cur = val;
+ LL_FOREACH(val, cur)
+ {
+ if (ucl_object_type(cur) == UCL_STRING) {
+ const gchar *lua_script;
+ gsize slen;
+ gint ref_idx;
+
+ lua_script = ucl_object_tolstring(cur, &slen);
+ ref_idx = rspamd_lua_function_ref_from_str(RSPAMD_LUA_CFG_STATE(cfg),
+ lua_script, slen, "learn_condition", err);
+
+ if (ref_idx == LUA_NOREF) {
+ return FALSE;
+ }
+
+ rspamd_lua_add_ref_dtor(RSPAMD_LUA_CFG_STATE(cfg), cfg->cfg_pool, ref_idx);
+ ccf->learn_conditions = rspamd_mempool_glist_append(
+ cfg->cfg_pool,
+ ccf->learn_conditions,
+ GINT_TO_POINTER(ref_idx));
+ }
+ }
+ }
+
+ val = ucl_object_lookup_any(obj, "classify_condition", nullptr);
+
+ if (val) {
+ const auto *cur = val;
+ LL_FOREACH(val, cur)
+ {
+ if (ucl_object_type(cur) == UCL_STRING) {
+ const gchar *lua_script;
+ gsize slen;
+ gint ref_idx;
+
+ lua_script = ucl_object_tolstring(cur, &slen);
+ ref_idx = rspamd_lua_function_ref_from_str(RSPAMD_LUA_CFG_STATE(cfg),
+ lua_script, slen, "classify_condition", err);
+
+ if (ref_idx == LUA_NOREF) {
+ return FALSE;
+ }
+
+ rspamd_lua_add_ref_dtor(RSPAMD_LUA_CFG_STATE(cfg), cfg->cfg_pool, ref_idx);
+ ccf->classify_conditions = rspamd_mempool_glist_append(
+ cfg->cfg_pool,
+ ccf->classify_conditions,
+ GINT_TO_POINTER(ref_idx));
+ }
+ }
+ }
+
+ ccf->opts = (ucl_object_t *) obj;
+ cfg->classifiers = g_list_prepend(cfg->classifiers, ccf);
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_rcl_composite_handler(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ const gchar *key,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *cfg = static_cast<rspamd_config *>(ud);
+ void *composite;
+ const gchar *composite_name;
+
+ g_assert(key != nullptr);
+
+ composite_name = key;
+
+ const auto *val = ucl_object_lookup(obj, "enabled");
+ if (val != nullptr && !ucl_object_toboolean(val)) {
+ msg_info_config("composite %s is disabled", composite_name);
+ return TRUE;
+ }
+
+ if ((composite = rspamd_composites_manager_add_from_ucl(cfg->composites_manager,
+ composite_name, obj)) != nullptr) {
+ rspamd_symcache_add_symbol(cfg->cache, composite_name, 0,
+ nullptr, composite, SYMBOL_TYPE_COMPOSITE, -1);
+ }
+
+ return composite != nullptr;
+}
+
+static gboolean
+rspamd_rcl_composites_handler(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ const gchar *key,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto success = TRUE;
+
+ auto it = ucl_object_iterate_new(obj);
+ const auto *cur = obj;
+
+ while ((cur = ucl_object_iterate_safe(it, true))) {
+ success = rspamd_rcl_composite_handler(pool, cur,
+ ucl_object_key(cur), ud, section, err);
+ if (!success) {
+ break;
+ }
+ }
+
+ ucl_object_iterate_free(it);
+
+ return success;
+}
+
+static gboolean
+rspamd_rcl_neighbours_handler(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ const gchar *key,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *cfg = static_cast<rspamd_config *>(ud);
+ auto has_port = FALSE, has_proto = FALSE;
+ const gchar *p;
+
+ if (key == nullptr) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "missing name for neighbour");
+ return FALSE;
+ }
+
+ const auto *hostval = ucl_object_lookup(obj, "host");
+
+ if (hostval == nullptr || ucl_object_type(hostval) != UCL_STRING) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "missing host for neighbour: %s", ucl_object_key(obj));
+ return FALSE;
+ }
+
+ auto *neigh = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(neigh, ucl_object_copy(hostval), "host", 0, false);
+
+ if ((p = strrchr(ucl_object_tostring(hostval), ':')) != nullptr) {
+ if (g_ascii_isdigit(p[1])) {
+ has_port = TRUE;
+ }
+ }
+
+ if (strstr(ucl_object_tostring(hostval), "://") != nullptr) {
+ has_proto = TRUE;
+ }
+
+ /* Now make url */
+ auto urlstr = std::string{};
+ const auto *pathval = ucl_object_lookup(obj, "path");
+
+ if (!has_proto) {
+ urlstr += "http://";
+ }
+
+ urlstr += ucl_object_tostring(hostval);
+
+ if (!has_port) {
+ urlstr += ":11334";
+ }
+
+ if (pathval == nullptr) {
+ urlstr += "/";
+ }
+ else {
+ urlstr += ucl_object_tostring(pathval);
+ }
+
+ ucl_object_insert_key(neigh,
+ ucl_object_fromlstring(urlstr.data(), urlstr.size()),
+ "url", 0, false);
+ ucl_object_insert_key(cfg->neighbours, neigh, key, 0, true);
+
+ return TRUE;
+}
+
+
+struct rspamd_rcl_section *
+rspamd_rcl_add_section(struct rspamd_rcl_sections_map **top,
+ struct rspamd_rcl_section *parent_section,
+ const gchar *name, const gchar *key_attr, rspamd_rcl_handler_t handler,
+ enum ucl_type type, gboolean required, gboolean strict_type)
+{
+ return rspamd_rcl_add_section_doc(top, parent_section, name, key_attr, handler,
+ type, required, strict_type, nullptr, nullptr);
+}
+
+struct rspamd_rcl_section *
+rspamd_rcl_add_section_doc(struct rspamd_rcl_sections_map **top,
+ struct rspamd_rcl_section *parent_section,
+ const gchar *name, const gchar *key_attr, rspamd_rcl_handler_t handler,
+ enum ucl_type type, gboolean required, gboolean strict_type,
+ ucl_object_t *doc_target,
+ const gchar *doc_string)
+{
+ if (top == nullptr) {
+ g_error("invalid arguments to rspamd_rcl_add_section");
+ return nullptr;
+ }
+ if (*top == nullptr) {
+ *top = new rspamd_rcl_sections_map;
+ }
+
+ auto fill_section = [&](struct rspamd_rcl_section *section) {
+ section->name = name;
+ if (key_attr) {
+ section->key_attr = std::string{key_attr};
+ }
+ section->handler = handler;
+ section->type = type;
+ section->strict_type = strict_type;
+
+ if (doc_target == nullptr) {
+ if (parent_section && parent_section->doc_ref) {
+ section->doc_ref = ucl_object_ref(rspamd_rcl_add_doc_obj(parent_section->doc_ref,
+ doc_string,
+ name,
+ type,
+ nullptr,
+ 0,
+ nullptr,
+ 0));
+ }
+ else {
+ section->doc_ref = nullptr;
+ }
+ }
+ else {
+ section->doc_ref = ucl_object_ref(rspamd_rcl_add_doc_obj(doc_target,
+ doc_string,
+ name,
+ type,
+ nullptr,
+ 0,
+ nullptr,
+ 0));
+ }
+ section->top = *top;
+ };
+
+ /* Select the appropriate container and insert section inside it */
+ if (parent_section) {
+ auto it = parent_section->subsections.insert(std::make_pair(std::string{name},
+ std::make_shared<rspamd_rcl_section>()));
+ if (!it.second) {
+ g_error("invalid arguments to rspamd_rcl_add_section");
+ return nullptr;
+ }
+
+ fill_section(it.first->second.get());
+ return it.first->second.get();
+ }
+ else {
+ auto it = (*top)->sections.insert(std::make_pair(std::string{name},
+ std::make_shared<rspamd_rcl_section>()));
+ if (!it.second) {
+ g_error("invalid arguments to rspamd_rcl_add_section");
+ return nullptr;
+ }
+
+ (*top)->sections_order.push_back(it.first->second);
+ fill_section(it.first->second.get());
+ return it.first->second.get();
+ }
+}
+
+struct rspamd_rcl_default_handler_data *
+rspamd_rcl_add_default_handler(struct rspamd_rcl_section *section,
+ const gchar *name,
+ rspamd_rcl_default_handler_t handler,
+ goffset offset,
+ gint flags,
+ const gchar *doc_string)
+{
+ auto it = section->default_parser.emplace(std::make_pair(std::string{name}, rspamd_rcl_default_handler_data{}));
+
+ auto &nhandler = it.first->second;
+ nhandler.key = name;
+ nhandler.handler = handler;
+ nhandler.pd.offset = offset;
+ nhandler.pd.flags = flags;
+
+ if (section->doc_ref != nullptr) {
+ rspamd_rcl_add_doc_obj(section->doc_ref,
+ doc_string,
+ name,
+ UCL_NULL,
+ handler,
+ flags,
+ nullptr,
+ 0);
+ }
+
+ return &nhandler;
+}
+
+struct rspamd_rcl_sections_map *
+rspamd_rcl_config_init(struct rspamd_config *cfg, GHashTable *skip_sections)
+{
+ auto *top = new rspamd_rcl_sections_map;
+ /*
+ * Important notice:
+ * the order of parsing is equal to order of this initialization, therefore
+ * it is possible to init some portions of config prior to others
+ */
+
+ /**
+ * Logging section
+ */
+ if (!(skip_sections && g_hash_table_lookup(skip_sections, "logging"))) {
+ auto *sub = rspamd_rcl_add_section_doc(&top, nullptr,
+ "logging", nullptr,
+ rspamd_rcl_logging_handler,
+ UCL_OBJECT,
+ FALSE,
+ TRUE,
+ cfg->doc_strings,
+ "Configure rspamd logging");
+ /* Default handlers */
+ rspamd_rcl_add_default_handler(sub,
+ "log_buffer",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, log_buf_size),
+ RSPAMD_CL_FLAG_INT_32,
+ "Size of log buffer in bytes (for file logging)");
+ rspamd_rcl_add_default_handler(sub,
+ "log_urls",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, log_urls),
+ 0,
+ "Write each URL found in a message to the log file");
+ rspamd_rcl_add_default_handler(sub,
+ "debug_ip",
+ rspamd_rcl_parse_struct_ucl,
+ G_STRUCT_OFFSET(struct rspamd_config, debug_ip_map),
+ 0,
+ "Enable debugging log for the specified IP addresses");
+ rspamd_rcl_add_default_handler(sub,
+ "debug_modules",
+ rspamd_rcl_parse_struct_string_list,
+ G_STRUCT_OFFSET(struct rspamd_config, debug_modules),
+ RSPAMD_CL_FLAG_STRING_LIST_HASH,
+ "Enable debugging for the specified modules");
+ rspamd_rcl_add_default_handler(sub,
+ "log_format",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, log_format_str),
+ 0,
+ "Specify format string for the task logging output "
+ "(https://rspamd.com/doc/configuration/logging.html "
+ "for details)");
+ rspamd_rcl_add_default_handler(sub,
+ "encryption_key",
+ rspamd_rcl_parse_struct_pubkey,
+ G_STRUCT_OFFSET(struct rspamd_config, log_encryption_key),
+ 0,
+ "Encrypt sensitive information in logs using this pubkey");
+ rspamd_rcl_add_default_handler(sub,
+ "error_elts",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, log_error_elts),
+ RSPAMD_CL_FLAG_UINT,
+ "Size of circular buffer for last errors (10 by default)");
+ rspamd_rcl_add_default_handler(sub,
+ "error_maxlen",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, log_error_elt_maxlen),
+ RSPAMD_CL_FLAG_UINT,
+ "Size of each element in error log buffer (1000 by default)");
+ rspamd_rcl_add_default_handler(sub,
+ "task_max_elts",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, log_task_max_elts),
+ RSPAMD_CL_FLAG_UINT,
+ "Maximum number of elements in task log entry (7 by default)");
+
+ /* Documentation only options, handled in log_handler to map flags */
+ rspamd_rcl_add_doc_by_path(cfg,
+ "logging",
+ "Enable colored output (for console logging)",
+ "log_color",
+ UCL_BOOLEAN,
+ nullptr,
+ 0,
+ nullptr,
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "logging",
+ "Enable severity logging output (e.g. [error] or [warning])",
+ "log_severity",
+ UCL_BOOLEAN,
+ nullptr,
+ 0,
+ nullptr,
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "logging",
+ "Enable systemd compatible logging",
+ "systemd",
+ UCL_BOOLEAN,
+ nullptr,
+ 0,
+ nullptr,
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "logging",
+ "Write statistics of regexp processing to log (useful for hyperscan)",
+ "log_re_cache",
+ UCL_BOOLEAN,
+ nullptr,
+ 0,
+ nullptr,
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "logging",
+ "Use microseconds resolution for timestamps",
+ "log_usec",
+ UCL_BOOLEAN,
+ nullptr,
+ 0,
+ nullptr,
+ 0);
+ }
+ if (!(skip_sections && g_hash_table_lookup(skip_sections, "options"))) {
+ /**
+ * Options section
+ */
+ auto *sub = rspamd_rcl_add_section_doc(&top, nullptr,
+ "options", nullptr,
+ rspamd_rcl_options_handler,
+ UCL_OBJECT,
+ FALSE,
+ TRUE,
+ cfg->doc_strings,
+ "Global rspamd options");
+ rspamd_rcl_add_default_handler(sub,
+ "cache_file",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, cache_filename),
+ RSPAMD_CL_FLAG_STRING_PATH,
+ "Path to the cache file");
+ rspamd_rcl_add_default_handler(sub,
+ "cache_reload",
+ rspamd_rcl_parse_struct_time,
+ G_STRUCT_OFFSET(struct rspamd_config, cache_reload_time),
+ RSPAMD_CL_FLAG_TIME_FLOAT,
+ "How often cache reload should be performed");
+
+ /* Old DNS configuration */
+ rspamd_rcl_add_default_handler(sub,
+ "dns_nameserver",
+ rspamd_rcl_parse_struct_ucl,
+ G_STRUCT_OFFSET(struct rspamd_config, nameservers),
+ 0,
+ "Legacy option for DNS servers used");
+ rspamd_rcl_add_default_handler(sub,
+ "dns_timeout",
+ rspamd_rcl_parse_struct_time,
+ G_STRUCT_OFFSET(struct rspamd_config, dns_timeout),
+ RSPAMD_CL_FLAG_TIME_FLOAT,
+ "Legacy option for DNS request timeout");
+ rspamd_rcl_add_default_handler(sub,
+ "dns_retransmits",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, dns_retransmits),
+ RSPAMD_CL_FLAG_INT_32,
+ "Legacy option for DNS retransmits count");
+ rspamd_rcl_add_default_handler(sub,
+ "dns_sockets",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, dns_io_per_server),
+ RSPAMD_CL_FLAG_INT_32,
+ "Legacy option for DNS sockets per server count");
+ rspamd_rcl_add_default_handler(sub,
+ "dns_max_requests",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, dns_max_requests),
+ RSPAMD_CL_FLAG_INT_32,
+ "Maximum DNS requests per task (default: 64)");
+ rspamd_rcl_add_default_handler(sub,
+ "control_socket",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, control_socket_path),
+ 0,
+ "Path to the control socket");
+ rspamd_rcl_add_default_handler(sub,
+ "explicit_modules",
+ rspamd_rcl_parse_struct_string_list,
+ G_STRUCT_OFFSET(struct rspamd_config, explicit_modules),
+ RSPAMD_CL_FLAG_STRING_LIST_HASH,
+ "Always load these modules even if they are not configured explicitly");
+ rspamd_rcl_add_default_handler(sub,
+ "allow_raw_input",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, allow_raw_input),
+ 0,
+ "Allow non MIME input for rspamd");
+ rspamd_rcl_add_default_handler(sub,
+ "one_shot",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, one_shot_mode),
+ 0,
+ "Add all symbols only once per message");
+ rspamd_rcl_add_default_handler(sub,
+ "check_attachements",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, check_text_attachements),
+ 0,
+ "Treat text attachments as normal text parts");
+ rspamd_rcl_add_default_handler(sub,
+ "tempdir",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, temp_dir),
+ RSPAMD_CL_FLAG_STRING_PATH,
+ "Directory for temporary files");
+ rspamd_rcl_add_default_handler(sub,
+ "pidfile",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, pid_file),
+ RSPAMD_CL_FLAG_STRING_PATH,
+ "Path to the pid file");
+ rspamd_rcl_add_default_handler(sub,
+ "filters",
+ rspamd_rcl_parse_struct_string_list,
+ G_STRUCT_OFFSET(struct rspamd_config, filters),
+ 0,
+ "List of internal filters enabled");
+ rspamd_rcl_add_default_handler(sub,
+ "map_watch_interval",
+ rspamd_rcl_parse_struct_time,
+ G_STRUCT_OFFSET(struct rspamd_config, map_timeout),
+ RSPAMD_CL_FLAG_TIME_FLOAT,
+ "Interval for checking maps");
+ rspamd_rcl_add_default_handler(sub,
+ "map_file_watch_multiplier",
+ rspamd_rcl_parse_struct_double,
+ G_STRUCT_OFFSET(struct rspamd_config, map_file_watch_multiplier),
+ 0,
+ "Multiplier for map watch interval when map is file");
+ rspamd_rcl_add_default_handler(sub,
+ "maps_cache_dir",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, maps_cache_dir),
+ 0,
+ "Directory to save maps cached data (default: $DBDIR)");
+ rspamd_rcl_add_default_handler(sub,
+ "monitoring_watch_interval",
+ rspamd_rcl_parse_struct_time,
+ G_STRUCT_OFFSET(struct rspamd_config, monitored_interval),
+ RSPAMD_CL_FLAG_TIME_FLOAT,
+ "Interval for checking monitored instances");
+ rspamd_rcl_add_default_handler(sub,
+ "disable_monitoring",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, disable_monitored),
+ 0,
+ "Disable monitoring completely");
+ rspamd_rcl_add_default_handler(sub,
+ "fips_mode",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, fips_mode),
+ 0,
+ "Enable FIPS 140-2 mode in OpenSSL");
+ rspamd_rcl_add_default_handler(sub,
+ "dynamic_conf",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, dynamic_conf),
+ 0,
+ "Path to the dynamic configuration");
+ rspamd_rcl_add_default_handler(sub,
+ "rrd",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, rrd_file),
+ RSPAMD_CL_FLAG_STRING_PATH,
+ "Path to RRD file");
+ rspamd_rcl_add_default_handler(sub,
+ "stats_file",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, stats_file),
+ RSPAMD_CL_FLAG_STRING_PATH,
+ "Path to stats file");
+ rspamd_rcl_add_default_handler(sub,
+ "history_file",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, history_file),
+ RSPAMD_CL_FLAG_STRING_PATH,
+ "Path to history file");
+ rspamd_rcl_add_default_handler(sub,
+ "check_all_filters",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, check_all_filters),
+ 0,
+ "Always check all filters");
+ rspamd_rcl_add_default_handler(sub,
+ "public_groups_only",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, public_groups_only),
+ 0,
+ "Output merely public groups everywhere");
+ rspamd_rcl_add_default_handler(sub,
+ "enable_css_parser",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, enable_css_parser),
+ 0,
+ "Enable CSS parser (experimental)");
+ rspamd_rcl_add_default_handler(sub,
+ "enable_experimental",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, enable_experimental),
+ 0,
+ "Enable experimental plugins");
+ rspamd_rcl_add_default_handler(sub,
+ "disable_pcre_jit",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, disable_pcre_jit),
+ 0,
+ "Disable PCRE JIT");
+ rspamd_rcl_add_default_handler(sub,
+ "min_word_len",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, min_word_len),
+ RSPAMD_CL_FLAG_UINT,
+ "Minimum length of the word to be considered in statistics/fuzzy");
+ rspamd_rcl_add_default_handler(sub,
+ "max_word_len",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, max_word_len),
+ RSPAMD_CL_FLAG_UINT,
+ "Maximum length of the word to be considered in statistics/fuzzy");
+ rspamd_rcl_add_default_handler(sub,
+ "max_html_len",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, max_word_len),
+ RSPAMD_CL_FLAG_INT_SIZE,
+ "Maximum length of the html part to be parsed");
+ rspamd_rcl_add_default_handler(sub,
+ "words_decay",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, words_decay),
+ RSPAMD_CL_FLAG_UINT,
+ "Start skipping words at this amount");
+ rspamd_rcl_add_default_handler(sub,
+ "url_tld",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, tld_file),
+ RSPAMD_CL_FLAG_STRING_PATH,
+ "Path to the TLD file for urls detector");
+ rspamd_rcl_add_default_handler(sub,
+ "tld",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, tld_file),
+ RSPAMD_CL_FLAG_STRING_PATH,
+ "Path to the TLD file for urls detector");
+ rspamd_rcl_add_default_handler(sub,
+ "hs_cache_dir",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, hs_cache_dir),
+ RSPAMD_CL_FLAG_STRING_PATH,
+ "Path directory where rspamd would save hyperscan cache");
+ rspamd_rcl_add_default_handler(sub,
+ "history_rows",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, history_rows),
+ RSPAMD_CL_FLAG_UINT,
+ "Number of records in the history file");
+ rspamd_rcl_add_default_handler(sub,
+ "disable_hyperscan",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, disable_hyperscan),
+ 0,
+ "Disable hyperscan optimizations for regular expressions");
+ rspamd_rcl_add_default_handler(sub,
+ "vectorized_hyperscan",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, vectorized_hyperscan),
+ 0,
+ "Use hyperscan in vectorized mode (obsoleted, do not use)");
+ rspamd_rcl_add_default_handler(sub,
+ "cores_dir",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, cores_dir),
+ RSPAMD_CL_FLAG_STRING_PATH,
+ "Path to the directory where rspamd core files are intended to be dumped");
+ rspamd_rcl_add_default_handler(sub,
+ "max_cores_size",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, max_cores_size),
+ RSPAMD_CL_FLAG_INT_SIZE,
+ "Limit of joint size of all files in `cores_dir`");
+ rspamd_rcl_add_default_handler(sub,
+ "max_cores_count",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, max_cores_count),
+ RSPAMD_CL_FLAG_INT_SIZE,
+ "Limit of files count in `cores_dir`");
+ rspamd_rcl_add_default_handler(sub,
+ "local_addrs",
+ rspamd_rcl_parse_struct_ucl,
+ G_STRUCT_OFFSET(struct rspamd_config, local_addrs),
+ 0,
+ "Use the specified addresses as local ones");
+ rspamd_rcl_add_default_handler(sub,
+ "local_networks",
+ rspamd_rcl_parse_struct_ucl,
+ G_STRUCT_OFFSET(struct rspamd_config, local_addrs),
+ 0,
+ "Use the specified addresses as local ones (alias for `local_addrs`)");
+ rspamd_rcl_add_default_handler(sub,
+ "trusted_keys",
+ rspamd_rcl_parse_struct_string_list,
+ G_STRUCT_OFFSET(struct rspamd_config, trusted_keys),
+ RSPAMD_CL_FLAG_STRING_LIST_HASH,
+ "List of trusted public keys used for signatures in base32 encoding");
+ rspamd_rcl_add_default_handler(sub,
+ "enable_shutdown_workaround",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, enable_shutdown_workaround),
+ 0,
+ "Enable workaround for legacy clients");
+ rspamd_rcl_add_default_handler(sub,
+ "ignore_received",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, ignore_received),
+ 0,
+ "Ignore data from the first received header");
+ rspamd_rcl_add_default_handler(sub,
+ "ssl_ca_path",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, ssl_ca_path),
+ RSPAMD_CL_FLAG_STRING_PATH,
+ "Path to ssl CA file");
+ rspamd_rcl_add_default_handler(sub,
+ "ssl_ciphers",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, ssl_ciphers),
+ 0,
+ "List of ssl ciphers (e.g. HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4)");
+ rspamd_rcl_add_default_handler(sub,
+ "max_message",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, max_message),
+ RSPAMD_CL_FLAG_INT_SIZE,
+ "Maximum size of the message to be scanned (50Mb by default)");
+ rspamd_rcl_add_default_handler(sub,
+ "max_pic",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, max_pic_size),
+ RSPAMD_CL_FLAG_INT_SIZE,
+ "Maximum size of the picture to be normalized (1Mb by default)");
+ rspamd_rcl_add_default_handler(sub,
+ "images_cache",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, max_pic_size),
+ RSPAMD_CL_FLAG_INT_SIZE,
+ "Size of DCT data cache for images (256 elements by default)");
+ rspamd_rcl_add_default_handler(sub,
+ "zstd_input_dictionary",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, zstd_input_dictionary),
+ RSPAMD_CL_FLAG_STRING_PATH,
+ "Dictionary for zstd inbound protocol compression");
+ rspamd_rcl_add_default_handler(sub,
+ "zstd_output_dictionary",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, zstd_output_dictionary),
+ RSPAMD_CL_FLAG_STRING_PATH,
+ "Dictionary for outbound zstd compression");
+ rspamd_rcl_add_default_handler(sub,
+ "compat_messages",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, compat_messages),
+ 0,
+ "Use pre 1.4 style of messages in the protocol");
+ rspamd_rcl_add_default_handler(sub,
+ "max_shots",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, default_max_shots),
+ 0,
+ "Maximum number of hits per a single symbol (default: 100)");
+ rspamd_rcl_add_default_handler(sub,
+ "sessions_cache",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, enable_sessions_cache),
+ 0,
+ "Enable sessions cache to debug dangling sessions");
+ rspamd_rcl_add_default_handler(sub,
+ "max_sessions_cache",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, max_sessions_cache),
+ 0,
+ "Maximum number of sessions in cache before warning (default: 100)");
+ rspamd_rcl_add_default_handler(sub,
+ "task_timeout",
+ rspamd_rcl_parse_struct_time,
+ G_STRUCT_OFFSET(struct rspamd_config, task_timeout),
+ RSPAMD_CL_FLAG_TIME_FLOAT,
+ "Maximum time for checking a message");
+ rspamd_rcl_add_default_handler(sub,
+ "soft_reject_on_timeout",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, soft_reject_on_timeout),
+ 0,
+ "Emit soft reject if task timeout takes place");
+ rspamd_rcl_add_default_handler(sub,
+ "check_timeout",
+ rspamd_rcl_parse_struct_time,
+ G_STRUCT_OFFSET(struct rspamd_config, task_timeout),
+ RSPAMD_CL_FLAG_TIME_FLOAT,
+ "Maximum time for checking a message (alias for task_timeout)");
+ rspamd_rcl_add_default_handler(sub,
+ "lua_gc_step",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, lua_gc_step),
+ RSPAMD_CL_FLAG_UINT,
+ "Lua garbage-collector step (default: 200)");
+ rspamd_rcl_add_default_handler(sub,
+ "lua_gc_pause",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, lua_gc_pause),
+ RSPAMD_CL_FLAG_UINT,
+ "Lua garbage-collector pause (default: 200)");
+ rspamd_rcl_add_default_handler(sub,
+ "full_gc_iters",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, full_gc_iters),
+ RSPAMD_CL_FLAG_UINT,
+ "Task scanned before memory gc is performed (default: 0 - disabled)");
+ rspamd_rcl_add_default_handler(sub,
+ "heartbeat_interval",
+ rspamd_rcl_parse_struct_time,
+ G_STRUCT_OFFSET(struct rspamd_config, heartbeat_interval),
+ RSPAMD_CL_FLAG_TIME_FLOAT,
+ "Time between workers heartbeats");
+ rspamd_rcl_add_default_handler(sub,
+ "heartbeats_loss_max",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, heartbeats_loss_max),
+ RSPAMD_CL_FLAG_INT_32,
+ "Maximum count of heartbeats to be lost before trying to "
+ "terminate a worker (default: 0 - disabled)");
+ rspamd_rcl_add_default_handler(sub,
+ "max_lua_urls",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, max_lua_urls),
+ RSPAMD_CL_FLAG_INT_32,
+ "Maximum count of URLs to pass to Lua to avoid DoS (default: 1024)");
+ rspamd_rcl_add_default_handler(sub,
+ "max_urls",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, max_urls),
+ RSPAMD_CL_FLAG_INT_32,
+ "Maximum count of URLs to process to avoid DoS (default: 10240)");
+ rspamd_rcl_add_default_handler(sub,
+ "max_recipients",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, max_recipients),
+ RSPAMD_CL_FLAG_INT_32,
+ "Maximum count of recipients to process to avoid DoS (default: 1024)");
+ rspamd_rcl_add_default_handler(sub,
+ "max_blas_threads",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, max_blas_threads),
+ RSPAMD_CL_FLAG_INT_32,
+ "Maximum number of Blas threads for learning neural networks (default: 1)");
+ rspamd_rcl_add_default_handler(sub,
+ "max_opts_len",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, max_opts_len),
+ RSPAMD_CL_FLAG_INT_32,
+ "Maximum size of all options for a single symbol (default: 4096)");
+ rspamd_rcl_add_default_handler(sub,
+ "events_backend",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, events_backend),
+ 0,
+ "Events backend to use: kqueue, epoll, select, poll or auto (default: auto)");
+
+ rspamd_rcl_add_doc_by_path(cfg,
+ "options",
+ "Swtich mode of gtube patterns: disable, reject, all",
+ "gtube_patterns",
+ UCL_STRING,
+ nullptr,
+ 0,
+ "reject",
+ 0);
+
+ /* Neighbours configuration */
+ rspamd_rcl_add_section_doc(&top, sub, "neighbours", "name",
+ rspamd_rcl_neighbours_handler,
+ UCL_OBJECT, FALSE, TRUE,
+ cfg->doc_strings,
+ "List of members of Rspamd cluster");
+
+ /* New DNS configuration */
+ auto *ssub = rspamd_rcl_add_section_doc(&top, sub, "dns", nullptr, nullptr,
+ UCL_OBJECT, FALSE, TRUE,
+ cfg->doc_strings,
+ "Options for DNS resolver");
+ rspamd_rcl_add_default_handler(ssub,
+ "nameserver",
+ rspamd_rcl_parse_struct_ucl,
+ G_STRUCT_OFFSET(struct rspamd_config, nameservers),
+ 0,
+ "List of DNS servers");
+ rspamd_rcl_add_default_handler(ssub,
+ "server",
+ rspamd_rcl_parse_struct_ucl,
+ G_STRUCT_OFFSET(struct rspamd_config, nameservers),
+ 0,
+ "List of DNS servers");
+ rspamd_rcl_add_default_handler(ssub,
+ "timeout",
+ rspamd_rcl_parse_struct_time,
+ G_STRUCT_OFFSET(struct rspamd_config, dns_timeout),
+ RSPAMD_CL_FLAG_TIME_FLOAT,
+ "DNS request timeout");
+ rspamd_rcl_add_default_handler(ssub,
+ "retransmits",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, dns_retransmits),
+ RSPAMD_CL_FLAG_INT_32,
+ "DNS request retransmits");
+ rspamd_rcl_add_default_handler(ssub,
+ "sockets",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, dns_io_per_server),
+ RSPAMD_CL_FLAG_INT_32,
+ "Number of sockets per DNS server");
+ rspamd_rcl_add_default_handler(ssub,
+ "connections",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, dns_io_per_server),
+ RSPAMD_CL_FLAG_INT_32,
+ "Number of sockets per DNS server");
+ rspamd_rcl_add_default_handler(ssub,
+ "enable_dnssec",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_config, enable_dnssec),
+ 0,
+ "Enable DNSSEC support in Rspamd");
+
+
+ /* New upstreams configuration */
+ ssub = rspamd_rcl_add_section_doc(&top, sub, "upstream", nullptr, nullptr,
+ UCL_OBJECT, FALSE, TRUE,
+ cfg->doc_strings,
+ "Upstreams configuration parameters");
+ rspamd_rcl_add_default_handler(ssub,
+ "max_errors",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_config, upstream_max_errors),
+ RSPAMD_CL_FLAG_UINT,
+ "Maximum number of errors during `error_time` to consider upstream down");
+ rspamd_rcl_add_default_handler(ssub,
+ "error_time",
+ rspamd_rcl_parse_struct_time,
+ G_STRUCT_OFFSET(struct rspamd_config, upstream_error_time),
+ RSPAMD_CL_FLAG_TIME_FLOAT,
+ "Time frame to check errors");
+ rspamd_rcl_add_default_handler(ssub,
+ "revive_time",
+ rspamd_rcl_parse_struct_time,
+ G_STRUCT_OFFSET(struct rspamd_config, upstream_revive_time),
+ RSPAMD_CL_FLAG_TIME_FLOAT,
+ "Time before attempting to recover upstream after an error");
+ rspamd_rcl_add_default_handler(ssub,
+ "lazy_resolve_time",
+ rspamd_rcl_parse_struct_time,
+ G_STRUCT_OFFSET(struct rspamd_config, upstream_lazy_resolve_time),
+ RSPAMD_CL_FLAG_TIME_FLOAT,
+ "Time to resolve upstreams addresses in lazy mode");
+ }
+
+ if (!(skip_sections && g_hash_table_lookup(skip_sections, "actions"))) {
+ /**
+ * Symbols and actions sections
+ */
+ auto *sub = rspamd_rcl_add_section_doc(&top, nullptr,
+ "actions", nullptr,
+ rspamd_rcl_actions_handler,
+ UCL_OBJECT,
+ FALSE,
+ TRUE,
+ cfg->doc_strings,
+ "Actions configuration");
+ rspamd_rcl_add_default_handler(sub,
+ "unknown_weight",
+ rspamd_rcl_parse_struct_double,
+ G_STRUCT_OFFSET(struct rspamd_config, unknown_weight),
+ 0,
+ "Accept unknown symbols with the specified weight");
+ rspamd_rcl_add_default_handler(sub,
+ "grow_factor",
+ rspamd_rcl_parse_struct_double,
+ G_STRUCT_OFFSET(struct rspamd_config, grow_factor),
+ 0,
+ "Multiply the subsequent symbols by this number "
+ "(does not affect symbols with score less or "
+ "equal to zero)");
+ rspamd_rcl_add_default_handler(sub,
+ "subject",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_config, subject),
+ 0,
+ "Rewrite subject with this value");
+ }
+
+ if (!(skip_sections && g_hash_table_lookup(skip_sections, "group"))) {
+ auto *sub = rspamd_rcl_add_section_doc(&top, nullptr,
+ "group", "name",
+ rspamd_rcl_group_handler,
+ UCL_OBJECT,
+ FALSE,
+ TRUE,
+ cfg->doc_strings,
+ "Symbol groups configuration");
+ rspamd_rcl_add_section_doc(&top, sub, "symbols", "name",
+ rspamd_rcl_symbol_handler,
+ UCL_OBJECT, FALSE, TRUE,
+ cfg->doc_strings,
+ "Symbols configuration");
+
+ /* Group part */
+ rspamd_rcl_add_default_handler(sub,
+ "max_score",
+ rspamd_rcl_parse_struct_double,
+ G_STRUCT_OFFSET(struct rspamd_symbols_group, max_score),
+ 0,
+ "Maximum score that could be reached by this symbols group");
+ }
+
+ if (!(skip_sections && g_hash_table_lookup(skip_sections, "worker"))) {
+ /**
+ * Worker section
+ */
+ auto *sub = rspamd_rcl_add_section_doc(&top, nullptr, "worker", "type",
+ rspamd_rcl_worker_handler,
+ UCL_OBJECT,
+ FALSE,
+ TRUE,
+ cfg->doc_strings,
+ "Workers common options");
+ rspamd_rcl_add_default_handler(sub,
+ "count",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_worker_conf, count),
+ RSPAMD_CL_FLAG_INT_16,
+ "Number of workers to spawn");
+ rspamd_rcl_add_default_handler(sub,
+ "max_files",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_worker_conf, rlimit_nofile),
+ RSPAMD_CL_FLAG_INT_64,
+ "Maximum number of opened files per worker");
+ rspamd_rcl_add_default_handler(sub,
+ "max_core",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_worker_conf, rlimit_maxcore),
+ RSPAMD_CL_FLAG_INT_64,
+ "Max size of core file in bytes");
+ rspamd_rcl_add_default_handler(sub,
+ "enabled",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_worker_conf, enabled),
+ 0,
+ "Enable or disable a worker (true by default)");
+ }
+
+ if (!(skip_sections && g_hash_table_lookup(skip_sections, "modules"))) {
+ /**
+ * Modules handler
+ */
+ rspamd_rcl_add_section_doc(&top, nullptr,
+ "modules", nullptr,
+ rspamd_rcl_modules_handler,
+ UCL_OBJECT,
+ FALSE,
+ FALSE,
+ cfg->doc_strings,
+ "Lua plugins to load");
+ }
+
+ if (!(skip_sections && g_hash_table_lookup(skip_sections, "classifier"))) {
+ /**
+ * Classifiers handler
+ */
+ auto *sub = rspamd_rcl_add_section_doc(&top, nullptr,
+ "classifier", "type",
+ rspamd_rcl_classifier_handler,
+ UCL_OBJECT,
+ FALSE,
+ TRUE,
+ cfg->doc_strings,
+ "CLassifier options");
+ /* Default classifier is 'bayes' for now */
+ sub->default_key = "bayes";
+
+ rspamd_rcl_add_default_handler(sub,
+ "min_tokens",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_classifier_config, min_tokens),
+ RSPAMD_CL_FLAG_INT_32,
+ "Minimum count of tokens (words) to be considered for statistics");
+ rspamd_rcl_add_default_handler(sub,
+ "min_token_hits",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_classifier_config, min_token_hits),
+ RSPAMD_CL_FLAG_UINT,
+ "Minimum number of hits for a token to be considered");
+ rspamd_rcl_add_default_handler(sub,
+ "min_prob_strength",
+ rspamd_rcl_parse_struct_double,
+ G_STRUCT_OFFSET(struct rspamd_classifier_config, min_token_hits),
+ 0,
+ "Use only tokens with probability in [0.5 - MPS, 0.5 + MPS]");
+ rspamd_rcl_add_default_handler(sub,
+ "max_tokens",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_classifier_config, max_tokens),
+ RSPAMD_CL_FLAG_INT_32,
+ "Maximum count of tokens (words) to be considered for statistics");
+ rspamd_rcl_add_default_handler(sub,
+ "min_learns",
+ rspamd_rcl_parse_struct_integer,
+ G_STRUCT_OFFSET(struct rspamd_classifier_config, min_learns),
+ RSPAMD_CL_FLAG_UINT,
+ "Minimum number of learns for each statfile to use this classifier");
+ rspamd_rcl_add_default_handler(sub,
+ "backend",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_classifier_config, backend),
+ 0,
+ "Statfiles engine");
+ rspamd_rcl_add_default_handler(sub,
+ "name",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_classifier_config, name),
+ 0,
+ "Name of classifier");
+
+ /*
+ * Statfile defaults
+ */
+ auto *ssub = rspamd_rcl_add_section_doc(&top, sub,
+ "statfile", "symbol",
+ rspamd_rcl_statfile_handler,
+ UCL_OBJECT,
+ TRUE,
+ TRUE,
+ sub->doc_ref,
+ "Statfiles options");
+ rspamd_rcl_add_default_handler(ssub,
+ "label",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_statfile_config, label),
+ 0,
+ "Statfile unique label");
+ rspamd_rcl_add_default_handler(ssub,
+ "spam",
+ rspamd_rcl_parse_struct_boolean,
+ G_STRUCT_OFFSET(struct rspamd_statfile_config, is_spam),
+ 0,
+ "Sets if this statfile contains spam samples");
+ }
+
+ if (!(skip_sections && g_hash_table_lookup(skip_sections, "composite"))) {
+ /**
+ * Composites handlers
+ */
+ rspamd_rcl_add_section_doc(&top, nullptr,
+ "composite", "name",
+ rspamd_rcl_composite_handler,
+ UCL_OBJECT,
+ FALSE,
+ TRUE,
+ cfg->doc_strings,
+ "Rspamd composite symbols");
+ rspamd_rcl_add_section_doc(&top, nullptr,
+ "composites", nullptr,
+ rspamd_rcl_composites_handler,
+ UCL_OBJECT,
+ FALSE,
+ TRUE,
+ cfg->doc_strings,
+ "Rspamd composite symbols");
+ }
+
+ if (!(skip_sections && g_hash_table_lookup(skip_sections, "lua"))) {
+ /**
+ * Lua handler
+ */
+ rspamd_rcl_add_section_doc(&top, nullptr,
+ "lua", nullptr,
+ rspamd_rcl_lua_handler,
+ UCL_STRING,
+ FALSE,
+ TRUE,
+ cfg->doc_strings,
+ "Lua files to load");
+ }
+
+ cfg->rcl_top_section = top;
+
+ return top;
+}
+
+static bool
+rspamd_rcl_process_section(struct rspamd_config *cfg,
+ const struct rspamd_rcl_section &sec,
+ gpointer ptr, const ucl_object_t *obj, rspamd_mempool_t *pool,
+ GError **err)
+{
+ ucl_object_iter_t it;
+ const ucl_object_t *cur;
+ auto is_nested = true;
+ const gchar *key = nullptr;
+
+ if (sec.processed) {
+ /* Section has been already processed */
+ return TRUE;
+ }
+
+ g_assert(obj != nullptr);
+ g_assert(sec.handler != nullptr);
+
+ if (sec.key_attr) {
+ it = ucl_object_iterate_new(obj);
+
+ while ((cur = ucl_object_iterate_full(it, UCL_ITERATE_EXPLICIT)) != nullptr) {
+ if (ucl_object_type(cur) != UCL_OBJECT) {
+ is_nested = false;
+ break;
+ }
+ }
+
+ ucl_object_iterate_free(it);
+ }
+ else {
+ is_nested = false;
+ }
+
+ if (is_nested) {
+ /* Just reiterate on all subobjects */
+ it = ucl_object_iterate_new(obj);
+
+ while ((cur = ucl_object_iterate_full(it, UCL_ITERATE_EXPLICIT)) != nullptr) {
+ if (!sec.handler(pool, cur, ucl_object_key(cur), ptr, const_cast<rspamd_rcl_section *>(&sec), err)) {
+ ucl_object_iterate_free(it);
+
+ return false;
+ }
+ }
+
+ ucl_object_iterate_free(it);
+
+ return true;
+ }
+ else {
+ if (sec.key_attr) {
+ /* First of all search for required attribute and use it as a key */
+ cur = ucl_object_lookup(obj, sec.key_attr.value().c_str());
+
+ if (cur == nullptr) {
+ if (!sec.default_key) {
+ g_set_error(err, CFG_RCL_ERROR, EINVAL, "required attribute "
+ "'%s' is missing for section '%s', current key: %s",
+ sec.key_attr.value().c_str(),
+ sec.name.c_str(),
+ ucl_object_key(obj));
+
+ return false;
+ }
+ else {
+ msg_info("using default key '%s' for mandatory field '%s' "
+ "for section '%s'",
+ sec.default_key.value().c_str(), sec.key_attr.value().c_str(),
+ sec.name.c_str());
+ key = sec.default_key.value().c_str();
+ }
+ }
+ else if (ucl_object_type(cur) != UCL_STRING) {
+ g_set_error(err, CFG_RCL_ERROR, EINVAL, "required attribute %s"
+ " is not a string for section %s",
+ sec.key_attr.value().c_str(), sec.name.c_str());
+
+ return false;
+ }
+ else {
+ key = ucl_object_tostring(cur);
+ }
+ }
+ }
+
+ return sec.handler(pool, obj, key, ptr, const_cast<rspamd_rcl_section *>(&sec), err);
+}
+
+gboolean
+rspamd_rcl_parse(struct rspamd_rcl_sections_map *top,
+ struct rspamd_config *cfg,
+ gpointer ptr, rspamd_mempool_t *pool,
+ const ucl_object_t *obj, GError **err)
+{
+ if (obj->type != UCL_OBJECT) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "top configuration must be an object");
+ return FALSE;
+ }
+
+ /* Iterate over known sections and ignore unknown ones */
+ for (const auto &sec_ptr: top->sections_order) {
+ if (sec_ptr->name == "*") {
+ /* Default section handler */
+ const auto *cur_obj = obj;
+ LL_FOREACH(obj, cur_obj)
+ {
+ if (!top->sections.contains(ucl_object_key(cur_obj))) {
+ if (sec_ptr->handler != nullptr) {
+ if (!rspamd_rcl_process_section(cfg, *sec_ptr, ptr, cur_obj,
+ pool, err)) {
+ return FALSE;
+ }
+ }
+ else {
+ rspamd_rcl_section_parse_defaults(cfg,
+ *sec_ptr,
+ pool,
+ cur_obj,
+ ptr,
+ err);
+ }
+ }
+ }
+ }
+ else {
+ const auto *found = ucl_object_lookup(obj, sec_ptr->name.c_str());
+ if (found == nullptr) {
+ if (sec_ptr->required) {
+ g_set_error(err, CFG_RCL_ERROR, ENOENT,
+ "required section %s is missing", sec_ptr->name.c_str());
+ return FALSE;
+ }
+ }
+ else {
+ /* Check type */
+ if (sec_ptr->strict_type) {
+ if (sec_ptr->type != found->type) {
+ g_set_error(err, CFG_RCL_ERROR, EINVAL,
+ "object in section %s has invalid type", sec_ptr->name.c_str());
+ return FALSE;
+ }
+ }
+
+ const auto *cur_obj = found;
+ LL_FOREACH(found, cur_obj)
+ {
+ if (sec_ptr->handler != nullptr) {
+ if (!rspamd_rcl_process_section(cfg, *sec_ptr, ptr, cur_obj,
+ pool, err)) {
+ return FALSE;
+ }
+ }
+ else {
+ rspamd_rcl_section_parse_defaults(cfg, *sec_ptr,
+ pool,
+ cur_obj,
+ ptr,
+ err);
+ }
+ }
+ }
+ }
+ if (sec_ptr->fin) {
+ sec_ptr->fin(pool, sec_ptr->fin_ud);
+ }
+ }
+
+ return TRUE;
+}
+
+static bool
+rspamd_rcl_section_parse_defaults(struct rspamd_config *cfg,
+ const struct rspamd_rcl_section &section,
+ rspamd_mempool_t *pool, const ucl_object_t *obj, gpointer ptr,
+ GError **err)
+{
+
+ if (obj->type != UCL_OBJECT) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "default configuration must be an object for section %s "
+ "(actual type is %s)",
+ section.name.c_str(), ucl_object_type_to_string(ucl_object_type(obj)));
+ return FALSE;
+ }
+
+ for (const auto &cur: section.default_parser) {
+ const auto *found = ucl_object_lookup(obj, cur.first.c_str());
+ if (found != nullptr) {
+ auto new_pd = cur.second.pd;
+ new_pd.user_struct = ptr;
+ new_pd.cfg = cfg;
+ const auto *cur_obj = found;
+
+ LL_FOREACH(found, cur_obj)
+ {
+ if (!cur.second.handler(pool, cur_obj, &new_pd, const_cast<rspamd_rcl_section *>(&section), err)) {
+ return FALSE;
+ }
+
+ if (!(new_pd.flags & RSPAMD_CL_FLAG_MULTIPLE)) {
+ break;
+ }
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_rcl_parse_struct_string(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *pd = (struct rspamd_rcl_struct_parser *) ud;
+ const gsize num_str_len = 32;
+
+ auto target = (gchar **) (((gchar *) pd->user_struct) + pd->offset);
+ switch (obj->type) {
+ case UCL_STRING:
+ *target =
+ rspamd_mempool_strdup(pool, ucl_copy_value_trash(obj));
+ break;
+ case UCL_INT:
+ *target = (gchar *) rspamd_mempool_alloc(pool, num_str_len);
+ rspamd_snprintf(*target, num_str_len, "%L", obj->value.iv);
+ break;
+ case UCL_FLOAT:
+ *target = (gchar *) rspamd_mempool_alloc(pool, num_str_len);
+ rspamd_snprintf(*target, num_str_len, "%f", obj->value.dv);
+ break;
+ case UCL_BOOLEAN:
+ *target = (gchar *) rspamd_mempool_alloc(pool, num_str_len);
+ rspamd_snprintf(*target, num_str_len, "%s",
+ ((gboolean) obj->value.iv) ? "true" : "false");
+ break;
+ case UCL_NULL:
+ /* String is enforced to be null */
+ *target = nullptr;
+ break;
+ default:
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot convert %s to string in option %s",
+ ucl_object_type_to_string(ucl_object_type(obj)),
+ ucl_object_key(obj));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_rcl_parse_struct_integer(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *pd = (struct rspamd_rcl_struct_parser *) ud;
+ union {
+ gint *ip;
+ gint32 *i32p;
+ gint16 *i16p;
+ gint64 *i64p;
+ guint *up;
+ gsize *sp;
+ } target;
+ int64_t val;
+
+ if (pd->flags == RSPAMD_CL_FLAG_INT_32) {
+ target.i32p = (gint32 *) (((gchar *) pd->user_struct) + pd->offset);
+ if (!ucl_object_toint_safe(obj, &val)) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot convert %s to integer in option %s",
+ ucl_object_type_to_string(ucl_object_type(obj)),
+ ucl_object_key(obj));
+ return FALSE;
+ }
+ *target.i32p = val;
+ }
+ else if (pd->flags == RSPAMD_CL_FLAG_INT_64) {
+ target.i64p = (gint64 *) (((gchar *) pd->user_struct) + pd->offset);
+ if (!ucl_object_toint_safe(obj, &val)) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot convert %s to integer in option %s",
+ ucl_object_type_to_string(ucl_object_type(obj)),
+ ucl_object_key(obj));
+ return FALSE;
+ }
+ *target.i64p = val;
+ }
+ else if (pd->flags == RSPAMD_CL_FLAG_INT_SIZE) {
+ target.sp = (gsize *) (((gchar *) pd->user_struct) + pd->offset);
+ if (!ucl_object_toint_safe(obj, &val)) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot convert %s to integer in option %s",
+ ucl_object_type_to_string(ucl_object_type(obj)),
+ ucl_object_key(obj));
+ return FALSE;
+ }
+ *target.sp = val;
+ }
+ else if (pd->flags == RSPAMD_CL_FLAG_INT_16) {
+ target.i16p = (gint16 *) (((gchar *) pd->user_struct) + pd->offset);
+ if (!ucl_object_toint_safe(obj, &val)) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot convert %s to integer in option %s",
+ ucl_object_type_to_string(ucl_object_type(obj)),
+ ucl_object_key(obj));
+ return FALSE;
+ }
+ *target.i16p = val;
+ }
+ else if (pd->flags == RSPAMD_CL_FLAG_UINT) {
+ target.up = (guint *) (((gchar *) pd->user_struct) + pd->offset);
+ if (!ucl_object_toint_safe(obj, &val)) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot convert %s to integer in option %s",
+ ucl_object_type_to_string(ucl_object_type(obj)),
+ ucl_object_key(obj));
+ return FALSE;
+ }
+ *target.up = val;
+ }
+ else {
+ target.ip = (gint *) (((gchar *) pd->user_struct) + pd->offset);
+ if (!ucl_object_toint_safe(obj, &val)) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot convert %s to integer in option %s",
+ ucl_object_type_to_string(ucl_object_type(obj)),
+ ucl_object_key(obj));
+ return FALSE;
+ }
+ *target.ip = val;
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_rcl_parse_struct_double(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *pd = (struct rspamd_rcl_struct_parser *) ud;
+ gdouble *target;
+
+ target = (gdouble *) (((gchar *) pd->user_struct) + pd->offset);
+
+ if (!ucl_object_todouble_safe(obj, target)) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot convert %s to double in option %s",
+ ucl_object_type_to_string(ucl_object_type(obj)),
+ ucl_object_key(obj));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_rcl_parse_struct_time(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *pd = (struct rspamd_rcl_struct_parser *) ud;
+ union {
+ gint *psec;
+ guint32 *pu32;
+ gdouble *pdv;
+ struct timeval *ptv;
+ struct timespec *pts;
+ } target;
+ gdouble val;
+
+ if (!ucl_object_todouble_safe(obj, &val)) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot convert %s to double in option %s",
+ ucl_object_type_to_string(ucl_object_type(obj)),
+ ucl_object_key(obj));
+ return FALSE;
+ }
+
+ if (pd->flags == RSPAMD_CL_FLAG_TIME_TIMEVAL) {
+ target.ptv =
+ (struct timeval *) (((gchar *) pd->user_struct) + pd->offset);
+ target.ptv->tv_sec = (glong) val;
+ target.ptv->tv_usec = (val - (glong) val) * 1000000;
+ }
+ else if (pd->flags == RSPAMD_CL_FLAG_TIME_TIMESPEC) {
+ target.pts =
+ (struct timespec *) (((gchar *) pd->user_struct) + pd->offset);
+ target.pts->tv_sec = (glong) val;
+ target.pts->tv_nsec = (val - (glong) val) * 1000000000000LL;
+ }
+ else if (pd->flags == RSPAMD_CL_FLAG_TIME_FLOAT) {
+ target.pdv = (double *) (((gchar *) pd->user_struct) + pd->offset);
+ *target.pdv = val;
+ }
+ else if (pd->flags == RSPAMD_CL_FLAG_TIME_INTEGER) {
+ target.psec = (gint *) (((gchar *) pd->user_struct) + pd->offset);
+ *target.psec = val * 1000;
+ }
+ else if (pd->flags == RSPAMD_CL_FLAG_TIME_UINT_32) {
+ target.pu32 = (guint32 *) (((gchar *) pd->user_struct) + pd->offset);
+ *target.pu32 = val * 1000;
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot convert %s to time in option %s",
+ ucl_object_type_to_string(ucl_object_type(obj)),
+ ucl_object_key(obj));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_rcl_parse_struct_keypair(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *pd = (struct rspamd_rcl_struct_parser *) ud;
+ struct rspamd_cryptobox_keypair **target, *kp;
+
+ target = (struct rspamd_cryptobox_keypair **) (((gchar *) pd->user_struct) +
+ pd->offset);
+ if (obj->type == UCL_OBJECT) {
+ kp = rspamd_keypair_from_ucl(obj);
+
+ if (kp != nullptr) {
+ rspamd_mempool_add_destructor(pool,
+ (rspamd_mempool_destruct_t) rspamd_keypair_unref, kp);
+ *target = kp;
+ }
+ else {
+ gchar *dump = (char *) ucl_object_emit(obj, UCL_EMIT_JSON_COMPACT);
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot load the keypair specified: %s; section: %s; value: %s",
+ ucl_object_key(obj), section->name.c_str(), dump);
+ free(dump);
+
+ return FALSE;
+ }
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "no sane pubkey or privkey found in the keypair: %s",
+ ucl_object_key(obj));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_rcl_parse_struct_pubkey(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *pd = (struct rspamd_rcl_struct_parser *) ud;
+ struct rspamd_cryptobox_pubkey **target, *pk;
+ gsize len;
+ const gchar *str;
+ rspamd_cryptobox_keypair_type keypair_type = RSPAMD_KEYPAIR_KEX;
+ rspamd_cryptobox_mode keypair_mode = RSPAMD_CRYPTOBOX_MODE_25519;
+
+ if (pd->flags & RSPAMD_CL_FLAG_SIGNKEY) {
+ keypair_type = RSPAMD_KEYPAIR_SIGN;
+ }
+ if (pd->flags & RSPAMD_CL_FLAG_NISTKEY) {
+ keypair_mode = RSPAMD_CRYPTOBOX_MODE_NIST;
+ }
+
+ target = (struct rspamd_cryptobox_pubkey **) (((gchar *) pd->user_struct) +
+ pd->offset);
+ if (obj->type == UCL_STRING) {
+ str = ucl_object_tolstring(obj, &len);
+ pk = rspamd_pubkey_from_base32(str, len, keypair_type,
+ keypair_mode);
+
+ if (pk != nullptr) {
+ *target = pk;
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot load the pubkey specified: %s",
+ ucl_object_key(obj));
+ return FALSE;
+ }
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "no sane pubkey found in the element: %s",
+ ucl_object_key(obj));
+ return FALSE;
+ }
+
+ rspamd_mempool_add_destructor(pool,
+ (rspamd_mempool_destruct_t) rspamd_pubkey_unref, pk);
+
+ return TRUE;
+}
+
+static void
+rspamd_rcl_insert_string_list_item(gpointer *target, rspamd_mempool_t *pool,
+ std::string_view elt, gboolean is_hash)
+{
+ union {
+ GHashTable *hv;
+ GList *lv;
+ gpointer p;
+ } d;
+ gchar *val;
+
+ d.p = *target;
+
+ if (is_hash) {
+ if (d.hv == nullptr) {
+ d.hv = g_hash_table_new(rspamd_str_hash, rspamd_str_equal);
+ rspamd_mempool_add_destructor(pool,
+ (rspamd_mempool_destruct_t) g_hash_table_unref, d.hv);
+ }
+
+ val = rspamd_mempool_strdup_len(pool, elt.data(), elt.size());
+ g_hash_table_insert(d.hv, val, val);
+ }
+ else {
+ val = rspamd_mempool_strdup_len(pool, elt.data(), elt.size());
+ d.lv = g_list_prepend(d.lv, val);
+ }
+
+ *target = d.p;
+}
+
+gboolean
+rspamd_rcl_parse_struct_string_list(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *pd = (struct rspamd_rcl_struct_parser *) ud;
+ constexpr const auto num_str_len = 32;
+ auto need_destructor = true;
+
+
+ auto is_hash = pd->flags & RSPAMD_CL_FLAG_STRING_LIST_HASH;
+ auto *target = (gpointer *) (((gchar *) pd->user_struct) + pd->offset);
+
+ if (!is_hash && *target != nullptr) {
+ need_destructor = FALSE;
+ }
+
+ auto iter = ucl_object_iterate_new(obj);
+ const auto *cur = obj;
+
+ while ((cur = ucl_object_iterate_safe(iter, true)) != nullptr) {
+ switch (cur->type) {
+ case UCL_STRING: {
+ rspamd::string_foreach_delim(ucl_object_tostring(cur), ", ", [&](const auto &elt) {
+ rspamd_rcl_insert_string_list_item(target, pool, elt, is_hash);
+ });
+
+ /* Go to the next object */
+ continue;
+ }
+ case UCL_INT: {
+ auto *val = (gchar *) rspamd_mempool_alloc(pool, num_str_len);
+ rspamd_snprintf(val, num_str_len, "%L", cur->value.iv);
+ rspamd_rcl_insert_string_list_item(target, pool, val, is_hash);
+ break;
+ }
+ case UCL_FLOAT: {
+ auto *val = (gchar *) rspamd_mempool_alloc(pool, num_str_len);
+ rspamd_snprintf(val, num_str_len, "%f", cur->value.dv);
+ rspamd_rcl_insert_string_list_item(target, pool, val, is_hash);
+ break;
+ }
+ case UCL_BOOLEAN: {
+ auto *val = (gchar *) rspamd_mempool_alloc(pool, num_str_len);
+ rspamd_snprintf(val, num_str_len, "%s",
+ ((gboolean) cur->value.iv) ? "true" : "false");
+ rspamd_rcl_insert_string_list_item(target, pool, val, is_hash);
+ break;
+ }
+ default:
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot convert %s to a string list in option %s",
+ ucl_object_type_to_string(ucl_object_type(obj)),
+ ucl_object_key(obj));
+ ucl_object_iterate_free(iter);
+
+ return FALSE;
+ }
+ }
+
+ ucl_object_iterate_free(iter);
+
+#if 0
+ /* WTF: why don't we allow empty list here?? */
+ if (*target == nullptr) {
+ g_set_error (err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "non-empty array of strings is expected: %s, "
+ "got: %s, of length: %d",
+ ucl_object_key (obj), ucl_object_type_to_string (obj->type),
+ obj->len);
+ return FALSE;
+ }
+#endif
+
+ if (!is_hash && *target != nullptr) {
+ *target = g_list_reverse(*(GList **) target);
+
+ if (need_destructor) {
+ rspamd_mempool_add_destructor(pool,
+ (rspamd_mempool_destruct_t) g_list_free,
+ *target);
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_rcl_parse_struct_ucl(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *pd = (struct rspamd_rcl_struct_parser *) ud;
+ const ucl_object_t **target;
+
+ target = (const ucl_object_t **) (((gchar *) pd->user_struct) + pd->offset);
+
+ *target = obj;
+
+ return TRUE;
+}
+
+
+gboolean
+rspamd_rcl_parse_struct_boolean(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *pd = (struct rspamd_rcl_struct_parser *) ud;
+ gboolean *target;
+
+ target = (gboolean *) (((gchar *) pd->user_struct) + pd->offset);
+
+ if (obj->type == UCL_BOOLEAN) {
+ *target = obj->value.iv;
+ }
+ else if (obj->type == UCL_INT) {
+ *target = obj->value.iv;
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot convert %s to boolean in option %s",
+ ucl_object_type_to_string(ucl_object_type(obj)),
+ ucl_object_key(obj));
+ return FALSE;
+ }
+
+ if (pd->flags & RSPAMD_CL_FLAG_BOOLEAN_INVERSE) {
+ *target = !*target;
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_rcl_parse_struct_addr(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *pd = (struct rspamd_rcl_struct_parser *) ud;
+ rspamd_inet_addr_t **target;
+ const gchar *val;
+ gsize size;
+
+ target = (rspamd_inet_addr_t **) (((gchar *) pd->user_struct) + pd->offset);
+
+ if (ucl_object_type(obj) == UCL_STRING) {
+ val = ucl_object_tolstring(obj, &size);
+
+ if (!rspamd_parse_inet_address(target, val, size,
+ RSPAMD_INET_ADDRESS_PARSE_DEFAULT)) {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot parse inet address: %s", val);
+ return FALSE;
+ }
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot convert %s to inet address in option %s",
+ ucl_object_type_to_string(ucl_object_type(obj)),
+ ucl_object_key(obj));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_rcl_parse_struct_mime_addr(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ auto *pd = (struct rspamd_rcl_struct_parser *) ud;
+ GPtrArray **target, *tmp_addr = nullptr;
+ const gchar *val;
+ ucl_object_iter_t it;
+ const ucl_object_t *cur;
+
+ target = (GPtrArray **) (((gchar *) pd->user_struct) + pd->offset);
+ it = ucl_object_iterate_new(obj);
+
+ while ((cur = ucl_object_iterate_safe(it, true)) != nullptr) {
+ if (ucl_object_type(cur) == UCL_STRING) {
+ val = ucl_object_tostring(obj);
+ tmp_addr = rspamd_email_address_from_mime(pool, val,
+ strlen(val), tmp_addr, -1);
+ }
+ else {
+ g_set_error(err,
+ CFG_RCL_ERROR,
+ EINVAL,
+ "cannot get inet address from ucl object in %s",
+ ucl_object_key(obj));
+ ucl_object_iterate_free(it);
+
+ return FALSE;
+ }
+ }
+
+ ucl_object_iterate_free(it);
+ *target = tmp_addr;
+
+ return TRUE;
+}
+
+void rspamd_rcl_register_worker_option(struct rspamd_config *cfg,
+ GQuark type,
+ const gchar *name,
+ rspamd_rcl_default_handler_t handler,
+ gpointer target,
+ glong offset,
+ gint flags,
+ const gchar *doc_string)
+{
+ auto parser_it = cfg->rcl_top_section->workers_parser.try_emplace(type, rspamd_worker_cfg_parser{});
+ auto &parser = parser_it.first->second;
+ auto handler_it = parser.parsers.try_emplace(std::make_pair(std::string{name}, target), rspamd_worker_param_parser{});
+
+ if (!handler_it.second) {
+ msg_warn_config(
+ "handler for parameter %s is already registered for worker type %s",
+ name,
+ g_quark_to_string(type));
+ return;
+ }
+
+ auto &nhandler = handler_it.first->second;
+ nhandler.parser.flags = flags;
+ nhandler.parser.offset = offset;
+ nhandler.parser.user_struct = target;
+ nhandler.handler = handler;
+
+ const auto *doc_workers = ucl_object_lookup(cfg->doc_strings, "workers");
+
+ if (doc_workers == nullptr) {
+ auto *doc_obj = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(cfg->doc_strings, doc_obj, "workers", 0, false);
+ doc_workers = doc_obj;
+ }
+
+ const auto *doc_target = ucl_object_lookup(doc_workers, g_quark_to_string(type));
+
+ if (doc_target == nullptr) {
+ auto *doc_obj = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key((ucl_object_t *) doc_workers, doc_obj,
+ g_quark_to_string(type), 0, true);
+ doc_target = doc_obj;
+ }
+
+ rspamd_rcl_add_doc_obj((ucl_object_t *) doc_target,
+ doc_string,
+ name,
+ UCL_NULL,
+ handler,
+ flags,
+ nullptr,
+ 0);
+}
+
+/* Checksum functions */
+static int
+rspamd_rcl_emitter_append_c(unsigned char c, size_t nchars, void *ud)
+{
+ auto *hs = (rspamd_cryptobox_hash_state_t *) ud;
+ guint64 d[2];
+
+ d[0] = nchars;
+ d[1] = c;
+
+ rspamd_cryptobox_hash_update(hs, (const guchar *) d, sizeof(d));
+
+ return 0;
+}
+
+static int
+rspamd_rcl_emitter_append_len(unsigned const char *str, size_t len, void *ud)
+{
+ auto *hs = (rspamd_cryptobox_hash_state_t *) ud;
+
+ rspamd_cryptobox_hash_update(hs, str, len);
+
+ return 0;
+}
+static int
+rspamd_rcl_emitter_append_int(int64_t elt, void *ud)
+{
+ auto *hs = (rspamd_cryptobox_hash_state_t *) ud;
+
+ rspamd_cryptobox_hash_update(hs, (const guchar *) &elt, sizeof(elt));
+
+ return 0;
+}
+
+static int
+rspamd_rcl_emitter_append_double(double elt, void *ud)
+{
+ auto *hs = (rspamd_cryptobox_hash_state_t *) ud;
+
+ rspamd_cryptobox_hash_update(hs, (const guchar *) &elt, sizeof(elt));
+
+ return 0;
+}
+
+void rspamd_rcl_sections_free(struct rspamd_rcl_sections_map *sections)
+{
+ delete sections;
+}
+
+/**
+ * Calls for an external lua function to apply potential config transformations
+ * if needed. This function can change the cfg->rcl_obj.
+ *
+ * Example of transformation function:
+ *
+ * function(obj)
+ * if obj.something == 'foo' then
+ * obj.something = "bla"
+ * return true, obj
+ * end
+ *
+ * return false, nil
+ * end
+ *
+ * If function returns 'false' then rcl_obj is not touched. Otherwise,
+ * it is changed, then rcl_obj is imported from lua. Old config is dereferenced.
+ * @param cfg
+ */
+void rspamd_rcl_maybe_apply_lua_transform(struct rspamd_config *cfg)
+{
+ auto *L = RSPAMD_LUA_CFG_STATE(cfg);
+ static const char *transform_script = "lua_cfg_transform";
+
+ g_assert(L != nullptr);
+
+ if (!rspamd_lua_require_function(L, transform_script, nullptr)) {
+ /* No function defined */
+ msg_warn_config("cannot execute lua script %s: %s",
+ transform_script, lua_tostring(L, -1));
+
+ return;
+ }
+
+ lua_pushcfunction(L, &rspamd_lua_traceback);
+ auto err_idx = lua_gettop(L);
+
+ /* Push function */
+ lua_pushvalue(L, -2);
+
+ /* Push the existing config */
+ ucl_object_push_lua(L, cfg->cfg_ucl_obj, true);
+
+ if (auto ret = lua_pcall(L, 1, 2, err_idx); ret != 0) {
+ msg_err("call to rspamadm lua script failed (%d): %s", ret,
+ lua_tostring(L, -1));
+ lua_settop(L, 0);
+
+ return;
+ }
+
+ if (lua_toboolean(L, -2) && lua_type(L, -1) == LUA_TTABLE) {
+ ucl_object_t *old_cfg = cfg->cfg_ucl_obj;
+
+ msg_info_config("configuration has been transformed in Lua");
+ cfg->cfg_ucl_obj = ucl_object_lua_import(L, -1);
+ ucl_object_unref(old_cfg);
+ }
+
+ /* error function */
+ lua_settop(L, 0);
+}
+
+static bool
+rspamd_rcl_decrypt_handler(struct ucl_parser *parser,
+ const unsigned char *source, size_t source_len,
+ unsigned char **destination, size_t *dest_len,
+ void *user_data)
+{
+ GError *err = nullptr;
+ auto *kp = (struct rspamd_cryptobox_keypair *) user_data;
+
+ if (!rspamd_keypair_decrypt(kp, source, source_len,
+ destination, dest_len, &err)) {
+ msg_err("cannot decrypt file: %e", err);
+ g_error_free(err);
+
+ return false;
+ }
+
+ return true;
+}
+
+static bool
+rspamd_rcl_jinja_handler(struct ucl_parser *parser,
+ const unsigned char *source, size_t source_len,
+ unsigned char **destination, size_t *dest_len,
+ void *user_data)
+{
+ auto *cfg = (struct rspamd_config *) user_data;
+ auto *L = RSPAMD_LUA_CFG_STATE(cfg);
+
+ lua_pushcfunction(L, &rspamd_lua_traceback);
+ auto err_idx = lua_gettop(L);
+
+ /* Obtain function */
+ if (!rspamd_lua_require_function(L, "lua_util", "jinja_template")) {
+ msg_err_config("cannot require lua_util.jinja_template");
+ lua_settop(L, err_idx - 1);
+
+ return false;
+ }
+
+ lua_pushlstring(L, (const char *) source, source_len);
+ lua_getglobal(L, "rspamd_env");
+ lua_pushboolean(L, false);
+
+ if (lua_pcall(L, 3, 1, err_idx) != 0) {
+ msg_err_config("cannot call lua jinja_template script: %s",
+ lua_tostring(L, -1));
+ lua_settop(L, err_idx - 1);
+
+ return false;
+ }
+
+ if (lua_type(L, -1) == LUA_TSTRING) {
+ const char *ndata;
+ gsize nsize;
+
+ ndata = lua_tolstring(L, -1, &nsize);
+ *destination = (unsigned char *) UCL_ALLOC(nsize);
+ memcpy(*destination, ndata, nsize);
+ *dest_len = nsize;
+ }
+ else {
+ msg_err_config("invalid return type when templating jinja %s",
+ lua_typename(L, lua_type(L, -1)));
+ lua_settop(L, err_idx - 1);
+
+ return false;
+ }
+
+ lua_settop(L, err_idx - 1);
+
+ return true;
+}
+
+static void
+rspamd_rcl_decrypt_free(unsigned char *data, size_t len, void *user_data)
+{
+ g_free(data);
+}
+
+void rspamd_config_calculate_cksum(struct rspamd_config *cfg)
+{
+ rspamd_cryptobox_hash_state_t hs;
+ unsigned char cksumbuf[rspamd_cryptobox_HASHBYTES];
+ struct ucl_emitter_functions f;
+
+ /* Calculate checksum */
+ rspamd_cryptobox_hash_init(&hs, nullptr, 0);
+ f.ucl_emitter_append_character = rspamd_rcl_emitter_append_c;
+ f.ucl_emitter_append_double = rspamd_rcl_emitter_append_double;
+ f.ucl_emitter_append_int = rspamd_rcl_emitter_append_int;
+ f.ucl_emitter_append_len = rspamd_rcl_emitter_append_len;
+ f.ucl_emitter_free_func = nullptr;
+ f.ud = &hs;
+ ucl_object_emit_full(cfg->cfg_ucl_obj, UCL_EMIT_MSGPACK,
+ &f, cfg->config_comments);
+ rspamd_cryptobox_hash_final(&hs, cksumbuf);
+ cfg->checksum = rspamd_encode_base32(cksumbuf, sizeof(cksumbuf), RSPAMD_BASE32_DEFAULT);
+ /* Also change the tag of cfg pool to be equal to the checksum */
+ rspamd_strlcpy(cfg->cfg_pool->tag.uid, cfg->checksum,
+ MIN(sizeof(cfg->cfg_pool->tag.uid), strlen(cfg->checksum)));
+}
+
+gboolean
+rspamd_config_parse_ucl(struct rspamd_config *cfg,
+ const gchar *filename,
+ GHashTable *vars,
+ ucl_include_trace_func_t inc_trace,
+ void *trace_data,
+ gboolean skip_jinja,
+ GError **err)
+{
+ struct rspamd_cryptobox_keypair *decrypt_keypair = nullptr;
+ auto cfg_file_maybe = rspamd::util::raii_mmaped_file::mmap_shared(filename, O_RDONLY, PROT_READ, 0);
+
+ if (!cfg_file_maybe) {
+ g_set_error(err, cfg_rcl_error_quark(), errno,
+ "cannot open %s: %*s", filename, (int) cfg_file_maybe.error().error_message.size(),
+ cfg_file_maybe.error().error_message.data());
+ return FALSE;
+ }
+
+ auto &cfg_file = cfg_file_maybe.value();
+
+ /* Try to load keyfile if available */
+ rspamd::util::raii_file::open(fmt::format("{}.key", filename), O_RDONLY).map([&](const auto &keyfile) {
+ auto *kp_parser = ucl_parser_new(0);
+ if (ucl_parser_add_fd(kp_parser, keyfile.get_fd())) {
+ auto *kp_obj = ucl_parser_get_object(kp_parser);
+
+ g_assert(kp_obj != nullptr);
+ decrypt_keypair = rspamd_keypair_from_ucl(kp_obj);
+
+ if (decrypt_keypair == nullptr) {
+ msg_err_config_forced("cannot load keypair from %s.key: invalid keypair",
+ filename);
+ }
+ else {
+ /* Add decryption support to UCL */
+ rspamd_mempool_add_destructor(cfg->cfg_pool,
+ (rspamd_mempool_destruct_t) rspamd_keypair_unref,
+ decrypt_keypair);
+ }
+
+ ucl_object_unref(kp_obj);
+ }
+ else {
+ msg_err_config_forced("cannot load keypair from %s.key: %s",
+ filename, ucl_parser_get_error(kp_parser));
+ }
+ ucl_parser_free(kp_parser);
+ });
+
+ auto parser = std::shared_ptr<ucl_parser>(ucl_parser_new(UCL_PARSER_SAVE_COMMENTS), ucl_parser_free);
+ rspamd_ucl_add_conf_variables(parser.get(), vars);
+ rspamd_ucl_add_conf_macros(parser.get(), cfg);
+ ucl_parser_set_filevars(parser.get(), filename, true);
+
+ if (inc_trace) {
+ ucl_parser_set_include_tracer(parser.get(), inc_trace, trace_data);
+ }
+
+ if (decrypt_keypair) {
+ auto *decrypt_handler = rspamd_mempool_alloc0_type(cfg->cfg_pool,
+ struct ucl_parser_special_handler);
+ decrypt_handler->user_data = decrypt_keypair;
+ decrypt_handler->magic = encrypted_magic;
+ decrypt_handler->magic_len = sizeof(encrypted_magic);
+ decrypt_handler->handler = rspamd_rcl_decrypt_handler;
+ decrypt_handler->free_function = rspamd_rcl_decrypt_free;
+
+ ucl_parser_add_special_handler(parser.get(), decrypt_handler);
+ }
+
+ if (!skip_jinja) {
+ auto *jinja_handler = rspamd_mempool_alloc0_type(cfg->cfg_pool,
+ struct ucl_parser_special_handler);
+ jinja_handler->user_data = cfg;
+ jinja_handler->flags = UCL_SPECIAL_HANDLER_PREPROCESS_ALL;
+ jinja_handler->handler = rspamd_rcl_jinja_handler;
+
+ ucl_parser_add_special_handler(parser.get(), jinja_handler);
+ }
+
+ if (!ucl_parser_add_chunk(parser.get(), (unsigned char *) cfg_file.get_map(), cfg_file.get_size())) {
+ g_set_error(err, cfg_rcl_error_quark(), errno,
+ "ucl parser error: %s", ucl_parser_get_error(parser.get()));
+
+ return FALSE;
+ }
+
+ cfg->cfg_ucl_obj = ucl_parser_get_object(parser.get());
+ cfg->config_comments = ucl_object_ref(ucl_parser_get_comments(parser.get()));
+
+ return TRUE;
+}
+
+gboolean
+rspamd_config_read(struct rspamd_config *cfg,
+ const gchar *filename,
+ rspamd_rcl_section_fin_t logger_fin,
+ gpointer logger_ud,
+ GHashTable *vars,
+ gboolean skip_jinja,
+ gchar **lua_env)
+{
+ GError *err = nullptr;
+
+ rspamd_lua_set_path(RSPAMD_LUA_CFG_STATE(cfg), nullptr, vars);
+
+ if (!rspamd_lua_set_env(RSPAMD_LUA_CFG_STATE(cfg), vars, lua_env, &err)) {
+ msg_err_config_forced("failed to set up environment: %e", err);
+ g_error_free(err);
+
+ return FALSE;
+ }
+
+ if (!rspamd_config_parse_ucl(cfg, filename, vars, nullptr, nullptr, skip_jinja, &err)) {
+ msg_err_config_forced("failed to load config: %e", err);
+ g_error_free(err);
+
+ return FALSE;
+ }
+
+ auto *top = rspamd_rcl_config_init(cfg, nullptr);
+ cfg->rcl_top_section = top;
+ /* Add new paths if defined in options */
+ rspamd_lua_set_path(RSPAMD_LUA_CFG_STATE(cfg), cfg->cfg_ucl_obj, vars);
+ rspamd_lua_set_globals(cfg, RSPAMD_LUA_CFG_STATE(cfg));
+ rspamd_mempool_add_destructor(cfg->cfg_pool, (rspamd_mempool_destruct_t) rspamd_rcl_sections_free, top);
+ err = nullptr;
+
+ /* Pre-init logging if possible */
+ if (logger_fin != nullptr) {
+ auto logging_section_maybe = rspamd::find_map(top->sections, "logging");
+
+ if (logging_section_maybe) {
+ const auto *logger_obj = ucl_object_lookup_any(cfg->cfg_ucl_obj, "logging",
+ "logger", nullptr);
+
+ if (logger_obj == nullptr) {
+ logger_fin(cfg->cfg_pool, logger_ud);
+ }
+ else {
+ if (!rspamd_rcl_process_section(cfg, *logging_section_maybe.value().get().get(), cfg,
+ logger_obj, cfg->cfg_pool, &err)) {
+ msg_err_config_forced("cannot init logger: %e", err);
+ g_error_free(err);
+
+ return FALSE;
+ }
+ else {
+ logger_fin(cfg->cfg_pool, logger_ud);
+ }
+
+ /* Init lua logging */
+ lua_pushcfunction(RSPAMD_LUA_CFG_STATE(cfg), &rspamd_lua_traceback);
+ auto err_idx = lua_gettop(RSPAMD_LUA_CFG_STATE(cfg));
+
+ /* Obtain function */
+ if (!rspamd_lua_require_function(RSPAMD_LUA_CFG_STATE(cfg), "lua_util",
+ "init_debug_logging")) {
+ msg_err_config("cannot require lua_util.init_debug_logging");
+ lua_settop(RSPAMD_LUA_CFG_STATE(cfg), err_idx - 1);
+
+ return FALSE;
+ }
+
+ void *pcfg = lua_newuserdata(RSPAMD_LUA_CFG_STATE(cfg), sizeof(void *));
+ memcpy(pcfg, &cfg, sizeof(void *));
+ rspamd_lua_setclass(RSPAMD_LUA_CFG_STATE(cfg), "rspamd{config}", -1);
+
+ if (lua_pcall(RSPAMD_LUA_CFG_STATE(cfg), 1, 0, err_idx) != 0) {
+ msg_err_config("cannot call lua init_debug_logging script: %s",
+ lua_tostring(RSPAMD_LUA_CFG_STATE(cfg), -1));
+ lua_settop(RSPAMD_LUA_CFG_STATE(cfg), err_idx - 1);
+
+ return FALSE;
+ }
+
+ lua_settop(RSPAMD_LUA_CFG_STATE(cfg), err_idx - 1);
+ }
+ }
+ }
+
+ /* Transform config if needed */
+ rspamd_rcl_maybe_apply_lua_transform(cfg);
+ rspamd_config_calculate_cksum(cfg);
+
+ if (!rspamd_rcl_parse(top, cfg, cfg, cfg->cfg_pool, cfg->cfg_ucl_obj, &err)) {
+ msg_err_config("rcl parse error: %e", err);
+
+ if (err) {
+ g_error_free(err);
+ }
+
+ return FALSE;
+ }
+
+ cfg->lang_det = rspamd_language_detector_init(cfg);
+ rspamd_mempool_add_destructor(cfg->cfg_pool,
+ (rspamd_mempool_destruct_t) rspamd_language_detector_unref,
+ cfg->lang_det);
+
+ return TRUE;
+}
+
+static void
+rspamd_rcl_doc_obj_from_handler(ucl_object_t *doc_obj,
+ rspamd_rcl_default_handler_t handler,
+ gint flags)
+{
+ auto has_example = ucl_object_lookup(doc_obj, "example") != nullptr;
+ auto has_type = ucl_object_lookup(doc_obj, "type") != nullptr;
+
+ if (handler == rspamd_rcl_parse_struct_string) {
+ if (!has_type) {
+ ucl_object_insert_key(doc_obj, ucl_object_fromstring("string"),
+ "type", 0, false);
+ }
+ }
+ else if (handler == rspamd_rcl_parse_struct_integer) {
+ auto *type = "int";
+
+ if (flags & RSPAMD_CL_FLAG_INT_16) {
+ type = "int16";
+ }
+ else if (flags & RSPAMD_CL_FLAG_INT_32) {
+ type = "int32";
+ }
+ else if (flags & RSPAMD_CL_FLAG_INT_64) {
+ type = "int64";
+ }
+ else if (flags & RSPAMD_CL_FLAG_INT_SIZE) {
+ type = "size";
+ }
+ else if (flags & RSPAMD_CL_FLAG_UINT) {
+ type = "uint";
+ }
+
+ if (!has_type) {
+ ucl_object_insert_key(doc_obj, ucl_object_fromstring(type),
+ "type", 0, false);
+ }
+ }
+ else if (handler == rspamd_rcl_parse_struct_double) {
+ if (!has_type) {
+ ucl_object_insert_key(doc_obj, ucl_object_fromstring("double"),
+ "type", 0, false);
+ }
+ }
+ else if (handler == rspamd_rcl_parse_struct_time) {
+ auto *type = "time";
+
+ if (!has_type) {
+ ucl_object_insert_key(doc_obj, ucl_object_fromstring(type),
+ "type", 0, false);
+ }
+ }
+ else if (handler == rspamd_rcl_parse_struct_string_list) {
+ if (!has_type) {
+ ucl_object_insert_key(doc_obj, ucl_object_fromstring("string list"),
+ "type", 0, false);
+ }
+ if (!has_example) {
+ ucl_object_insert_key(doc_obj,
+ ucl_object_fromstring_common("param = \"str1, str2, str3\" OR "
+ "param = [\"str1\", \"str2\", \"str3\"]",
+ 0, static_cast<ucl_string_flags>(0)),
+ "example",
+ 0,
+ false);
+ }
+ }
+ else if (handler == rspamd_rcl_parse_struct_boolean) {
+ if (!has_type) {
+ ucl_object_insert_key(doc_obj,
+ ucl_object_fromstring("bool"),
+ "type",
+ 0,
+ false);
+ }
+ }
+ else if (handler == rspamd_rcl_parse_struct_keypair) {
+ if (!has_type) {
+ ucl_object_insert_key(doc_obj,
+ ucl_object_fromstring("keypair"),
+ "type",
+ 0,
+ false);
+ }
+ if (!has_example) {
+ ucl_object_insert_key(doc_obj,
+ ucl_object_fromstring("keypair { "
+ "pubkey = <base32_string>;"
+ " privkey = <base32_string>; "
+ "}"),
+ "example",
+ 0,
+ false);
+ }
+ }
+ else if (handler == rspamd_rcl_parse_struct_addr) {
+ if (!has_type) {
+ ucl_object_insert_key(doc_obj,
+ ucl_object_fromstring("socket address"),
+ "type",
+ 0,
+ false);
+ }
+ }
+ else if (handler == rspamd_rcl_parse_struct_mime_addr) {
+ if (!has_type) {
+ ucl_object_insert_key(doc_obj,
+ ucl_object_fromstring("email address"),
+ "type",
+ 0,
+ false);
+ }
+ }
+}
+
+ucl_object_t *
+rspamd_rcl_add_doc_obj(ucl_object_t *doc_target,
+ const char *doc_string,
+ const char *doc_name,
+ ucl_type_t type,
+ rspamd_rcl_default_handler_t handler,
+ gint flags,
+ const char *default_value,
+ gboolean required)
+{
+ ucl_object_t *doc_obj;
+
+ if (doc_target == nullptr || doc_name == nullptr) {
+ return nullptr;
+ }
+
+ doc_obj = ucl_object_typed_new(UCL_OBJECT);
+
+ /* Insert doc string itself */
+ if (doc_string) {
+ ucl_object_insert_key(doc_obj,
+ ucl_object_fromstring_common(doc_string, 0, static_cast<ucl_string_flags>(0)),
+ "data", 0, false);
+ }
+ else {
+ ucl_object_insert_key(doc_obj, ucl_object_fromstring("undocumented"),
+ "data", 0, false);
+ }
+
+ if (type != UCL_NULL) {
+ ucl_object_insert_key(doc_obj,
+ ucl_object_fromstring(ucl_object_type_to_string(type)),
+ "type", 0, false);
+ }
+
+ rspamd_rcl_doc_obj_from_handler(doc_obj, handler, flags);
+
+ ucl_object_insert_key(doc_obj,
+ ucl_object_frombool(required),
+ "required", 0, false);
+
+ if (default_value) {
+ ucl_object_insert_key(doc_obj,
+ ucl_object_fromstring_common(default_value, 0, static_cast<ucl_string_flags>(0)),
+ "default", 0, false);
+ }
+
+ ucl_object_insert_key(doc_target, doc_obj, doc_name, 0, true);
+
+ return doc_obj;
+}
+
+ucl_object_t *
+rspamd_rcl_add_doc_by_path(struct rspamd_config *cfg,
+ const gchar *doc_path,
+ const char *doc_string,
+ const char *doc_name,
+ ucl_type_t type,
+ rspamd_rcl_default_handler_t handler,
+ gint flags,
+ const char *default_value,
+ gboolean required)
+{
+ const auto *cur = cfg->doc_strings;
+
+ if (doc_path == nullptr) {
+ /* Assume top object */
+ return rspamd_rcl_add_doc_obj(cfg->doc_strings,
+ doc_string,
+ doc_name,
+ type,
+ handler,
+ flags,
+ default_value,
+ required);
+ }
+ else {
+ const auto *found = ucl_object_lookup_path(cfg->doc_strings, doc_path);
+
+ if (found != nullptr) {
+ return rspamd_rcl_add_doc_obj((ucl_object_t *) found,
+ doc_string,
+ doc_name,
+ type,
+ handler,
+ flags,
+ default_value,
+ required);
+ }
+
+ /* Otherwise we need to insert all components of the path */
+ rspamd::string_foreach_delim(doc_path, ".", [&](const std::string_view &elt) {
+ if (ucl_object_type(cur) != UCL_OBJECT) {
+ msg_err_config("Bad path while lookup for '%s' at %*s",
+ doc_path, (int) elt.size(), elt.data());
+ }
+ const auto *found = ucl_object_lookup_len(cur, elt.data(), elt.size());
+ if (found == nullptr) {
+ auto *obj = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key((ucl_object_t *) cur,
+ obj,
+ elt.data(),
+ elt.size(),
+ true);
+ cur = obj;
+ }
+ else {
+ cur = found;
+ }
+ });
+ }
+
+ return rspamd_rcl_add_doc_obj(ucl_object_ref(cur),
+ doc_string,
+ doc_name,
+ type,
+ handler,
+ flags,
+ default_value,
+ required);
+}
+
+static void
+rspamd_rcl_add_doc_from_comments(struct rspamd_config *cfg,
+ ucl_object_t *top_doc, const ucl_object_t *obj,
+ const ucl_object_t *comments, gboolean is_top)
+{
+ ucl_object_iter_t it = nullptr;
+ const ucl_object_t *cur, *cmt;
+ ucl_object_t *cur_doc;
+
+ if (ucl_object_type(obj) == UCL_OBJECT) {
+ while ((cur = ucl_object_iterate(obj, &it, true)) != nullptr) {
+ cur_doc = nullptr;
+
+ if ((cmt = ucl_comments_find(comments, cur)) != nullptr) {
+ cur_doc = rspamd_rcl_add_doc_obj(top_doc,
+ ucl_object_tostring(cmt), ucl_object_key(cur),
+ ucl_object_type(cur), nullptr, 0, nullptr, FALSE);
+ }
+
+ if (ucl_object_type(cur) == UCL_OBJECT) {
+ if (cur_doc) {
+ rspamd_rcl_add_doc_from_comments(cfg, cur_doc, cur,
+ comments,
+ FALSE);
+ }
+ else {
+ rspamd_rcl_add_doc_from_comments(cfg, top_doc, cur,
+ comments,
+ FALSE);
+ }
+ }
+ }
+ }
+ else if (!is_top) {
+ if ((cmt = ucl_comments_find(comments, obj)) != nullptr) {
+ rspamd_rcl_add_doc_obj(top_doc,
+ ucl_object_tostring(cmt), ucl_object_key(obj),
+ ucl_object_type(obj), nullptr, 0, nullptr, FALSE);
+ }
+ }
+}
+
+ucl_object_t *
+rspamd_rcl_add_doc_by_example(struct rspamd_config *cfg,
+ const gchar *root_path,
+ const gchar *doc_string,
+ const gchar *doc_name,
+ const gchar *example_data, gsize example_len)
+{
+ auto parser = std::shared_ptr<ucl_parser>(ucl_parser_new(UCL_PARSER_NO_FILEVARS | UCL_PARSER_SAVE_COMMENTS), ucl_parser_free);
+
+ if (!ucl_parser_add_chunk(parser.get(), reinterpret_cast<const unsigned char *>(example_data), example_len)) {
+ msg_err_config("cannot parse example: %s",
+ ucl_parser_get_error(parser.get()));
+
+ return nullptr;
+ }
+
+ auto *top = ucl_parser_get_object(parser.get());
+ const auto *comments = ucl_parser_get_comments(parser.get());
+
+ /* Add top object */
+ auto *top_doc = rspamd_rcl_add_doc_by_path(cfg, root_path, doc_string,
+ doc_name, ucl_object_type(top), nullptr, 0, nullptr, FALSE);
+ ucl_object_insert_key(top_doc,
+ ucl_object_fromstring_common(example_data, example_len, static_cast<ucl_string_flags>(0)),
+ "example", 0, false);
+
+ rspamd_rcl_add_doc_from_comments(cfg, top_doc, top, comments, TRUE);
+
+ return top_doc;
+}
diff --git a/src/libserver/cfg_rcl.h b/src/libserver/cfg_rcl.h
new file mode 100644
index 0000000..766c55e
--- /dev/null
+++ b/src/libserver/cfg_rcl.h
@@ -0,0 +1,476 @@
+/*
+ * 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.
+ */
+#ifndef CFG_RCL_H_
+#define CFG_RCL_H_
+
+#include "config.h"
+#include "cfg_file.h"
+#include "ucl.h"
+#include "mem_pool.h"
+
+#define CFG_RCL_ERROR cfg_rcl_error_quark()
+static inline GQuark
+cfg_rcl_error_quark(void)
+{
+ return g_quark_from_static_string("cfg-rcl-error-quark");
+}
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_rcl_section;
+struct rspamd_rcl_sections_map;
+struct rspamd_config;
+struct rspamd_rcl_default_handler_data;
+
+enum rspamd_rcl_flag {
+ RSPAMD_CL_FLAG_TIME_FLOAT = 0x1 << 0,
+ RSPAMD_CL_FLAG_TIME_TIMEVAL = 0x1 << 1,
+ RSPAMD_CL_FLAG_TIME_TIMESPEC = 0x1 << 2,
+ RSPAMD_CL_FLAG_TIME_INTEGER = 0x1 << 3,
+ RSPAMD_CL_FLAG_TIME_UINT_32 = 0x1 << 4,
+ RSPAMD_CL_FLAG_INT_16 = 0x1 << 5,
+ RSPAMD_CL_FLAG_INT_32 = 0x1 << 6,
+ RSPAMD_CL_FLAG_INT_64 = 0x1 << 7,
+ RSPAMD_CL_FLAG_UINT = 0x1 << 8,
+ RSPAMD_CL_FLAG_INT_SIZE = 0x1 << 9,
+ RSPAMD_CL_FLAG_STRING_PATH = 0x1 << 10,
+ RSPAMD_CL_FLAG_BOOLEAN_INVERSE = 0x1 << 11,
+ RSPAMD_CL_FLAG_STRING_LIST_HASH = 0x1 << 12,
+ RSPAMD_CL_FLAG_MULTIPLE = 0x1 << 13,
+ RSPAMD_CL_FLAG_SIGNKEY = 0x1 << 14,
+ RSPAMD_CL_FLAG_NISTKEY = 0x1 << 15,
+};
+
+struct rspamd_rcl_struct_parser {
+ struct rspamd_config *cfg;
+ gpointer user_struct;
+ goffset offset;
+ int flags; /* enum rspamd_rcl_flag */
+};
+
+
+/**
+ * Common handler type
+ * @param cfg configuration
+ * @param obj object to parse
+ * @param ud user data (depends on section)
+ * @param err error object
+ * @return TRUE if a section has been parsed
+ */
+typedef gboolean (*rspamd_rcl_handler_t)(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ const gchar *key,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err);
+
+typedef gboolean (*rspamd_rcl_default_handler_t)(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err);
+
+/**
+ * A handler type that is called at the end of section parsing
+ * @param cfg configuration
+ * @param ud user data
+ */
+typedef void (*rspamd_rcl_section_fin_t)(rspamd_mempool_t *pool, gpointer ud);
+
+/**
+ * Add a default handler for a section
+ * @param section section pointer
+ * @param name name of param
+ * @param handler handler of param
+ * @param offset offset in a structure
+ * @param flags flags for the parser
+ * @return newly created structure
+ */
+struct rspamd_rcl_default_handler_data *rspamd_rcl_add_default_handler(
+ struct rspamd_rcl_section *section,
+ const gchar *name,
+ rspamd_rcl_default_handler_t handler,
+ goffset offset,
+ gint flags,
+ const gchar *doc_string);
+
+/**
+ * Add new section to the configuration
+ * @param top top section
+ * @param name the name of the section
+ * @param key_attr name of the attribute that should be used as key attribute
+ * @param handler handler function for all attributes
+ * @param type type of object handled by a handler
+ * @param required whether at least one of these sections is required
+ * @param strict_type turn on strict check for types for this section
+ * @return newly created structure
+ */
+struct rspamd_rcl_section *rspamd_rcl_add_section(
+ struct rspamd_rcl_sections_map **top,
+ struct rspamd_rcl_section *parent_section,
+ const gchar *name,
+ const gchar *key_attr,
+ rspamd_rcl_handler_t handler,
+ enum ucl_type type,
+ gboolean required,
+ gboolean strict_type);
+
+struct rspamd_rcl_section *rspamd_rcl_add_section_doc(
+ struct rspamd_rcl_sections_map **top,
+ struct rspamd_rcl_section *parent_section,
+ const gchar *name, const gchar *key_attr,
+ rspamd_rcl_handler_t handler,
+ enum ucl_type type, gboolean required,
+ gboolean strict_type,
+ ucl_object_t *doc_target,
+ const gchar *doc_string);
+
+/**
+ * Init common sections known to rspamd
+ * @return top section
+ */
+struct rspamd_rcl_sections_map *rspamd_rcl_config_init(struct rspamd_config *cfg,
+ GHashTable *skip_sections);
+
+/**
+ * Parse configuration
+ * @param top top section
+ * @param cfg rspamd configuration
+ * @param ptr pointer to the target
+ * @param pool pool object
+ * @param obj ucl object to parse
+ * @param err error pointer
+ * @return
+ */
+gboolean rspamd_rcl_parse(struct rspamd_rcl_sections_map *top,
+ struct rspamd_config *cfg,
+ gpointer ptr, rspamd_mempool_t *pool,
+ const ucl_object_t *obj, GError **err);
+
+/**
+ * Here is a section of common handlers that accepts rcl_struct_parser
+ * which itself contains a struct pointer and the offset of a member in a
+ * specific structure
+ */
+
+/**
+ * Parse a string field of a structure
+ * @param cfg config pointer
+ * @param obj object to parse
+ * @param ud struct_parser structure
+ * @param section the current section
+ * @param err error pointer
+ * @return TRUE if a string value has been successfully parsed
+ */
+gboolean rspamd_rcl_parse_struct_string(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err);
+
+/**
+ * Parse an integer field of a structure
+ * @param cfg config pointer
+ * @param obj object to parse
+ * @param ud struct_parser structure
+ * @param section the current section
+ * @param err error pointer
+ * @return TRUE if a value has been successfully parsed
+ */
+gboolean rspamd_rcl_parse_struct_integer(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err);
+
+
+/**
+ * Parse a float field of a structure
+ * @param cfg config pointer
+ * @param obj object to parse
+ * @param ud struct_parser structure
+ * @param section the current section
+ * @param err error pointer
+ * @return TRUE if a value has been successfully parsed
+ */
+gboolean rspamd_rcl_parse_struct_double(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err);
+
+/**
+ * Parse a time field of a structure
+ * @param cfg config pointer
+ * @param obj object to parse
+ * @param ud struct_parser structure (flags mean the exact structure used)
+ * @param section the current section
+ * @param err error pointer
+ * @return TRUE if a value has been successfully parsed
+ */
+gboolean rspamd_rcl_parse_struct_time(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err);
+
+/**
+ * Parse a string list field of a structure presented by a GList* object
+ * @param cfg config pointer
+ * @param obj object to parse
+ * @param ud struct_parser structure (flags mean the exact structure used)
+ * @param section the current section
+ * @param err error pointer
+ * @return TRUE if a value has been successfully parsed
+ */
+gboolean rspamd_rcl_parse_struct_string_list(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err);
+
+/**
+ * Parse a boolean field of a structure
+ * @param cfg config pointer
+ * @param obj object to parse
+ * @param ud struct_parser structure (flags mean the exact structure used)
+ * @param section the current section
+ * @param err error pointer
+ * @return TRUE if a value has been successfully parsed
+ */
+gboolean rspamd_rcl_parse_struct_boolean(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err);
+
+/**
+ * Parse a keypair field of a structure
+ * @param cfg config pointer
+ * @param obj object to parse
+ * @param ud struct_parser structure (flags mean the exact structure used)
+ * @param section the current section
+ * @param err error pointer
+ * @return TRUE if a value has been successfully parsed
+ */
+gboolean rspamd_rcl_parse_struct_keypair(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err);
+
+/**
+ * Parse a pubkey field of a structure
+ * @param cfg config pointer
+ * @param obj object to parse
+ * @param ud struct_parser structure (flags mean the exact structure used)
+ * @param section the current section
+ * @param err error pointer
+ * @return TRUE if a value has been successfully parsed
+ */
+gboolean rspamd_rcl_parse_struct_pubkey(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err);
+
+/**
+ * Parse a inet addr field of a structure
+ * @param cfg config pointer
+ * @param obj object to parse
+ * @param ud struct_parser structure (flags mean the exact structure used)
+ * @param section the current section
+ * @param err error pointer
+ * @return TRUE if a value has been successfully parsed
+ */
+gboolean rspamd_rcl_parse_struct_addr(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err);
+
+/**
+ * Parse a gmime inet address field of a structure
+ * @param cfg config pointer
+ * @param obj object to parse
+ * @param ud struct_parser structure (flags mean the exact structure used)
+ * @param section the current section
+ * @param err error pointer
+ * @return TRUE if a value has been successfully parsed
+ */
+gboolean rspamd_rcl_parse_struct_mime_addr(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err);
+
+/**
+ * Parse a raw ucl object
+ * @param cfg config pointer
+ * @param obj object to parse
+ * @param ud struct_parser structure (flags mean the exact structure used)
+ * @param section the current section
+ * @param err error pointer
+ * @return TRUE if a value has been successfully parsed
+ */
+gboolean rspamd_rcl_parse_struct_ucl(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err);
+
+
+/**
+ * Utility functions
+ */
+
+/**
+ * Register new parser for a worker type of an option with the specified name
+ * @param cfg config structure
+ * @param type type of worker (GQuark)
+ * @param name name of option
+ * @param handler handler of option
+ * @param target opaque target structure, note it **MUST** be worker ctx due to some reasons I don't really remember
+ * @param offset offset inside a structure
+ */
+void rspamd_rcl_register_worker_option(struct rspamd_config *cfg,
+ GQuark type,
+ const gchar *name,
+ rspamd_rcl_default_handler_t handler,
+ gpointer target,
+ glong offset,
+ gint flags,
+ const gchar *doc_string);
+
+/**
+ * Adds new documentation object to the configuration
+ * @param doc_target target object where to insert documentation (top object is used if this is NULL)
+ * @param doc_object documentation object to insert
+ */
+ucl_object_t *rspamd_rcl_add_doc_obj(ucl_object_t *doc_target,
+ const char *doc_string,
+ const char *doc_name,
+ ucl_type_t type,
+ rspamd_rcl_default_handler_t handler,
+ gint flags,
+ const char *default_value,
+ gboolean required);
+
+/**
+ * Adds new documentation option specified by path `doc_path` that should be
+ * split by dots
+ */
+ucl_object_t *rspamd_rcl_add_doc_by_path(struct rspamd_config *cfg,
+ const gchar *doc_path,
+ const char *doc_string,
+ const char *doc_name,
+ ucl_type_t type,
+ rspamd_rcl_default_handler_t handler,
+ gint flags,
+ const char *default_value,
+ gboolean required);
+
+
+/**
+ * Parses example and adds documentation according to the example:
+ *
+ * ```
+ * section {
+ * param1 = value; # explanation
+ * param2 = value; # explanation
+ * }
+ * ```
+ *
+ * will produce the following documentation strings:
+ * section ->
+ * section.param1 : explanation
+ * section.param2 : explanation
+ *
+ * @param cfg
+ * @param root_path
+ * @param example_data
+ * @param example_len
+ * @return
+ */
+ucl_object_t *rspamd_rcl_add_doc_by_example(struct rspamd_config *cfg,
+ const gchar *root_path,
+ const gchar *doc_string,
+ const gchar *doc_name,
+ const gchar *example_data, gsize example_len);
+
+/**
+ * Add lua modules path
+ * @param cfg
+ * @param path
+ * @param err
+ * @return
+ */
+gboolean rspamd_rcl_add_lua_plugins_path(struct rspamd_rcl_sections_map *sections,
+ struct rspamd_config *cfg,
+ const gchar *path,
+ gboolean main_path,
+ GError **err);
+
+
+/**
+ * Calls for an external lua function to apply potential config transformations
+ * if needed. This function can change the cfg->rcl_obj.
+ *
+ * Example of transformation function:
+ *
+ * function(obj)
+ * if obj.something == 'foo' then
+ * obj.something = "bla"
+ * return true, obj
+ * end
+ *
+ * return false, nil
+ * end
+ *
+ * If function returns 'false' then rcl_obj is not touched. Otherwise,
+ * it is changed, then rcl_obj is imported from lua. Old config is dereferenced.
+ * @param cfg
+ */
+void rspamd_rcl_maybe_apply_lua_transform(struct rspamd_config *cfg);
+void rspamd_rcl_sections_free(struct rspamd_rcl_sections_map *sections);
+
+void rspamd_config_calculate_cksum(struct rspamd_config *cfg);
+
+/*
+ * Read configuration file
+ */
+gboolean rspamd_config_parse_ucl(struct rspamd_config *cfg,
+ const gchar *filename,
+ GHashTable *vars,
+ ucl_include_trace_func_t inc_trace,
+ void *trace_data,
+ gboolean skip_jinja,
+ GError **err);
+gboolean rspamd_config_read(struct rspamd_config *cfg,
+ const gchar *filename,
+ rspamd_rcl_section_fin_t logger_fin,
+ gpointer logger_ud,
+ GHashTable *vars,
+ gboolean skip_jinja,
+ gchar **lua_env);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* CFG_RCL_H_ */
diff --git a/src/libserver/cfg_utils.cxx b/src/libserver/cfg_utils.cxx
new file mode 100644
index 0000000..3a94b47
--- /dev/null
+++ b/src/libserver/cfg_utils.cxx
@@ -0,0 +1,2955 @@
+/*
+ * 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 "lua/lua_common.h"
+#include "lua/lua_thread_pool.h"
+
+#include "cfg_file.h"
+#include "rspamd.h"
+#include "cfg_file_private.h"
+
+#include "maps/map.h"
+#include "maps/map_helpers.h"
+#include "maps/map_private.h"
+#include "dynamic_cfg.h"
+#include "utlist.h"
+#include "stat_api.h"
+#include "unix-std.h"
+#include "libutil/multipattern.h"
+#include "monitored.h"
+#include "ref.h"
+#include "cryptobox.h"
+#include "ssl_util.h"
+#include "contrib/libottery/ottery.h"
+#include "contrib/fastutf8/fastutf8.h"
+
+#ifdef SYS_ZSTD
+#include "zstd.h"
+#else
+#define ZSTD_STATIC_LINKING_ONLY
+#include "contrib/zstd/zstd.h"
+#endif
+
+#ifdef HAVE_OPENSSL
+#include <openssl/rand.h>
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/ssl.h>
+#include <openssl/conf.h>
+#endif
+#ifdef HAVE_LOCALE_H
+#include <locale.h>
+#endif
+#ifdef HAVE_SYS_RESOURCE_H
+#include <sys/resource.h>
+#endif
+#include <math.h>
+#include "libserver/composites/composites.h"
+
+#include "blas-config.h"
+
+#include <string>
+#include <string_view>
+#include <vector>
+#include "fmt/core.h"
+#include "cxx/util.hxx"
+#include "frozen/unordered_map.h"
+#include "frozen/string.h"
+#include "contrib/ankerl/unordered_dense.h"
+
+#define DEFAULT_SCORE 10.0
+
+#define DEFAULT_RLIMIT_NOFILE 2048
+#define DEFAULT_RLIMIT_MAXCORE 0
+#define DEFAULT_MAP_TIMEOUT 60.0 * 5
+#define DEFAULT_MAP_FILE_WATCH_MULTIPLIER 1
+#define DEFAULT_MIN_WORD 0
+#define DEFAULT_MAX_WORD 40
+#define DEFAULT_WORDS_DECAY 600
+#define DEFAULT_MAX_MESSAGE (50 * 1024 * 1024)
+#define DEFAULT_MAX_PIC (1 * 1024 * 1024)
+#define DEFAULT_MAX_SHOTS 100
+#define DEFAULT_MAX_SESSIONS 100
+#define DEFAULT_MAX_WORKERS 4
+#define DEFAULT_MAX_HTML_SIZE DEFAULT_MAX_MESSAGE / 5 /* 10 Mb */
+/* Timeout for task processing */
+#define DEFAULT_TASK_TIMEOUT 8.0
+#define DEFAULT_LUA_GC_STEP 200
+#define DEFAULT_LUA_GC_PAUSE 200
+#define DEFAULT_GC_MAXITERS 0
+
+struct rspamd_ucl_map_cbdata {
+ struct rspamd_config *cfg;
+ std::string buf;
+
+ explicit rspamd_ucl_map_cbdata(struct rspamd_config *cfg)
+ : cfg(cfg)
+ {
+ }
+};
+static gchar *rspamd_ucl_read_cb(gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final);
+static void rspamd_ucl_fin_cb(struct map_cb_data *data, void **target);
+static void rspamd_ucl_dtor_cb(struct map_cb_data *data);
+
+guint rspamd_config_log_id = (guint) -1;
+RSPAMD_CONSTRUCTOR(rspamd_config_log_init)
+{
+ rspamd_config_log_id = rspamd_logger_add_debug_module("config");
+}
+
+struct rspamd_actions_list {
+ using action_ptr = std::shared_ptr<rspamd_action>;
+ std::vector<action_ptr> actions;
+ ankerl::unordered_dense::map<std::string_view, action_ptr> actions_by_name;
+
+ explicit rspamd_actions_list()
+ {
+ actions.reserve(METRIC_ACTION_MAX + 2);
+ actions_by_name.reserve(METRIC_ACTION_MAX + 2);
+ }
+
+ void add_action(action_ptr action)
+ {
+ actions.push_back(action);
+ actions_by_name[action->name] = action;
+ sort();
+ }
+
+ void sort()
+ {
+ std::sort(actions.begin(), actions.end(), [](const action_ptr &a1, const action_ptr &a2) -> bool {
+ if (!isnan(a1->threshold) && !isnan(a2->threshold)) {
+ return a1->threshold < a2->threshold;
+ }
+
+ if (isnan(a1->threshold) && isnan(a2->threshold)) {
+ return false;
+ }
+ else if (isnan(a1->threshold)) {
+ return true;
+ }
+
+ return false;
+ });
+ }
+
+ void clear()
+ {
+ actions.clear();
+ actions_by_name.clear();
+ }
+};
+
+#define RSPAMD_CFG_ACTIONS(cfg) (reinterpret_cast<rspamd_actions_list *>((cfg)->actions))
+
+gboolean
+rspamd_parse_bind_line(struct rspamd_config *cfg,
+ struct rspamd_worker_conf *cf,
+ const gchar *str)
+{
+ struct rspamd_worker_bind_conf *cnf;
+ const gchar *fdname;
+ gboolean ret = TRUE;
+
+ if (str == nullptr) {
+ return FALSE;
+ }
+
+ cnf = rspamd_mempool_alloc0_type(cfg->cfg_pool, struct rspamd_worker_bind_conf);
+
+ cnf->cnt = 1024;
+ cnf->bind_line = rspamd_mempool_strdup(cfg->cfg_pool, str);
+
+ auto bind_line = std::string_view{cnf->bind_line};
+
+ if (bind_line.starts_with("systemd:")) {
+ /* The actual socket will be passed by systemd environment */
+ fdname = str + sizeof("systemd:") - 1;
+ cnf->is_systemd = TRUE;
+ cnf->addrs = g_ptr_array_new_full(1, nullptr);
+ rspamd_mempool_add_destructor(cfg->cfg_pool,
+ rspamd_ptr_array_free_hard, cnf->addrs);
+
+ if (fdname[0]) {
+ g_ptr_array_add(cnf->addrs, rspamd_mempool_strdup(cfg->cfg_pool, fdname));
+ cnf->cnt = cnf->addrs->len;
+ cnf->name = rspamd_mempool_strdup(cfg->cfg_pool, str);
+ LL_PREPEND(cf->bind_conf, cnf);
+ }
+ else {
+ msg_err_config("cannot parse bind line: %s", str);
+ ret = FALSE;
+ }
+ }
+ else {
+ if (rspamd_parse_host_port_priority(str, &cnf->addrs,
+ nullptr, &cnf->name, DEFAULT_BIND_PORT, TRUE, cfg->cfg_pool) == RSPAMD_PARSE_ADDR_FAIL) {
+ msg_err_config("cannot parse bind line: %s", str);
+ ret = FALSE;
+ }
+ else {
+ cnf->cnt = cnf->addrs->len;
+ LL_PREPEND(cf->bind_conf, cnf);
+ }
+ }
+
+ return ret;
+}
+
+struct rspamd_config *
+rspamd_config_new(enum rspamd_config_init_flags flags)
+{
+ struct rspamd_config *cfg;
+ rspamd_mempool_t *pool;
+
+ pool = rspamd_mempool_new(8 * 1024 * 1024, "cfg", 0);
+ cfg = rspamd_mempool_alloc0_type(pool, struct rspamd_config);
+ /* Allocate larger pool for cfg */
+ cfg->cfg_pool = pool;
+ cfg->dns_timeout = 1.0;
+ cfg->dns_retransmits = 5;
+ /* 16 sockets per DNS server */
+ cfg->dns_io_per_server = 16;
+ cfg->unknown_weight = NAN;
+
+ cfg->actions = (void *) new rspamd_actions_list();
+
+ /* Add all internal actions to keep compatibility */
+ for (int i = METRIC_ACTION_REJECT; i < METRIC_ACTION_MAX; i++) {
+
+ auto &&action = std::make_shared<rspamd_action>();
+ action->threshold = NAN;
+ action->name = rspamd_mempool_strdup(cfg->cfg_pool,
+ rspamd_action_to_str(static_cast<rspamd_action_type>(i)));
+ action->action_type = static_cast<rspamd_action_type>(i);
+
+ if (i == METRIC_ACTION_SOFT_REJECT) {
+ action->flags |= RSPAMD_ACTION_NO_THRESHOLD | RSPAMD_ACTION_HAM;
+ }
+ else if (i == METRIC_ACTION_GREYLIST) {
+ action->flags |= RSPAMD_ACTION_THRESHOLD_ONLY | RSPAMD_ACTION_HAM;
+ }
+ else if (i == METRIC_ACTION_NOACTION) {
+ action->flags |= RSPAMD_ACTION_HAM;
+ }
+
+ RSPAMD_CFG_ACTIONS(cfg)->add_action(std::move(action));
+ }
+
+ /* Disable timeout */
+ cfg->task_timeout = DEFAULT_TASK_TIMEOUT;
+
+
+ rspamd_config_init_metric(cfg);
+ cfg->composites_manager = rspamd_composites_manager_create(cfg);
+ cfg->classifiers_symbols = g_hash_table_new(rspamd_str_hash,
+ rspamd_str_equal);
+ cfg->cfg_params = g_hash_table_new(rspamd_str_hash, rspamd_str_equal);
+ cfg->debug_modules = g_hash_table_new(rspamd_str_hash, rspamd_str_equal);
+ cfg->explicit_modules = g_hash_table_new(rspamd_str_hash, rspamd_str_equal);
+ cfg->trusted_keys = g_hash_table_new(rspamd_str_hash,
+ rspamd_str_equal);
+
+ cfg->map_timeout = DEFAULT_MAP_TIMEOUT;
+ cfg->map_file_watch_multiplier = DEFAULT_MAP_FILE_WATCH_MULTIPLIER;
+
+ cfg->log_level = G_LOG_LEVEL_WARNING;
+ cfg->log_flags = RSPAMD_LOG_FLAG_DEFAULT;
+
+ cfg->check_text_attachements = TRUE;
+
+ cfg->dns_max_requests = 64;
+ cfg->history_rows = 200;
+ cfg->log_error_elts = 10;
+ cfg->log_error_elt_maxlen = 1000;
+ cfg->log_task_max_elts = 7;
+ cfg->cache_reload_time = 30.0;
+ cfg->max_lua_urls = 1024;
+ cfg->max_urls = cfg->max_lua_urls * 10;
+ cfg->max_recipients = 1024;
+ cfg->max_blas_threads = 1;
+ cfg->max_opts_len = 4096;
+ cfg->gtube_patterns_policy = RSPAMD_GTUBE_REJECT;
+
+ /* Default log line */
+ cfg->log_format_str = rspamd_mempool_strdup(cfg->cfg_pool,
+ "id: <$mid>,$if_qid{ qid: <$>,}$if_ip{ ip: $,}"
+ "$if_user{ user: $,}$if_smtp_from{ from: <$>,} (default: $is_spam "
+ "($action): [$scores] [$symbols_scores_params]), len: $len, time: $time_real, "
+ "dns req: $dns_req, digest: <$digest>"
+ "$if_smtp_rcpts{ rcpts: <$>, }$if_mime_rcpt{ mime_rcpt: <$>, }");
+ /* Allow non-mime input by default */
+ cfg->allow_raw_input = TRUE;
+ /* Default maximum words processed */
+ cfg->words_decay = DEFAULT_WORDS_DECAY;
+ cfg->min_word_len = DEFAULT_MIN_WORD;
+ cfg->max_word_len = DEFAULT_MAX_WORD;
+ cfg->max_html_len = DEFAULT_MAX_HTML_SIZE;
+
+ /* GC limits */
+ cfg->lua_gc_pause = DEFAULT_LUA_GC_PAUSE;
+ cfg->lua_gc_step = DEFAULT_LUA_GC_STEP;
+ cfg->full_gc_iters = DEFAULT_GC_MAXITERS;
+
+ /* Default hyperscan cache */
+ cfg->hs_cache_dir = rspamd_mempool_strdup(cfg->cfg_pool, RSPAMD_DBDIR "/");
+
+ if (!(flags & RSPAMD_CONFIG_INIT_SKIP_LUA)) {
+ cfg->lua_state = (void *) rspamd_lua_init(flags & RSPAMD_CONFIG_INIT_WIPE_LUA_MEM);
+ cfg->own_lua_state = TRUE;
+ cfg->lua_thread_pool = (void *) lua_thread_pool_new(RSPAMD_LUA_CFG_STATE(cfg));
+ }
+
+ cfg->cache = rspamd_symcache_new(cfg);
+ cfg->ups_ctx = rspamd_upstreams_library_init();
+ cfg->re_cache = rspamd_re_cache_new();
+ cfg->doc_strings = ucl_object_typed_new(UCL_OBJECT);
+ /*
+ * Unless exim is fixed
+ */
+ cfg->enable_shutdown_workaround = TRUE;
+
+ cfg->ssl_ciphers = rspamd_mempool_strdup(cfg->cfg_pool, "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4");
+ cfg->max_message = DEFAULT_MAX_MESSAGE;
+ cfg->max_pic_size = DEFAULT_MAX_PIC;
+ cfg->images_cache_size = 256;
+ cfg->monitored_ctx = rspamd_monitored_ctx_init();
+ cfg->neighbours = ucl_object_typed_new(UCL_OBJECT);
+ cfg->redis_pool = rspamd_redis_pool_init();
+ cfg->default_max_shots = DEFAULT_MAX_SHOTS;
+ cfg->max_sessions_cache = DEFAULT_MAX_SESSIONS;
+ cfg->maps_cache_dir = rspamd_mempool_strdup(cfg->cfg_pool, RSPAMD_DBDIR);
+ cfg->c_modules = g_ptr_array_new();
+ cfg->heartbeat_interval = 10.0;
+
+ cfg->enable_css_parser = true;
+ cfg->script_modules = g_ptr_array_new();
+
+ REF_INIT_RETAIN(cfg, rspamd_config_free);
+
+ return cfg;
+}
+
+void rspamd_config_free(struct rspamd_config *cfg)
+{
+ struct rspamd_config_cfg_lua_script *sc, *sctmp;
+ struct rspamd_config_settings_elt *set, *stmp;
+ struct rspamd_worker_log_pipe *lp, *ltmp;
+
+ rspamd_lua_run_config_unload(RSPAMD_LUA_CFG_STATE(cfg), cfg);
+
+ /* Scripts part */
+ DL_FOREACH_SAFE(cfg->on_term_scripts, sc, sctmp)
+ {
+ luaL_unref(RSPAMD_LUA_CFG_STATE(cfg), LUA_REGISTRYINDEX, sc->cbref);
+ }
+
+ DL_FOREACH_SAFE(cfg->on_load_scripts, sc, sctmp)
+ {
+ luaL_unref(RSPAMD_LUA_CFG_STATE(cfg), LUA_REGISTRYINDEX, sc->cbref);
+ }
+
+ DL_FOREACH_SAFE(cfg->post_init_scripts, sc, sctmp)
+ {
+ luaL_unref(RSPAMD_LUA_CFG_STATE(cfg), LUA_REGISTRYINDEX, sc->cbref);
+ }
+
+ DL_FOREACH_SAFE(cfg->config_unload_scripts, sc, sctmp)
+ {
+ luaL_unref(RSPAMD_LUA_CFG_STATE(cfg), LUA_REGISTRYINDEX, sc->cbref);
+ }
+
+ DL_FOREACH_SAFE(cfg->setting_ids, set, stmp)
+ {
+ REF_RELEASE(set);
+ }
+
+ rspamd_map_remove_all(cfg);
+ rspamd_mempool_destructors_enforce(cfg->cfg_pool);
+
+ g_list_free(cfg->classifiers);
+ g_list_free(cfg->workers);
+ rspamd_symcache_destroy(cfg->cache);
+ ucl_object_unref(cfg->cfg_ucl_obj);
+ ucl_object_unref(cfg->config_comments);
+ ucl_object_unref(cfg->doc_strings);
+ ucl_object_unref(cfg->neighbours);
+ g_hash_table_remove_all(cfg->cfg_params);
+ g_hash_table_unref(cfg->cfg_params);
+ g_hash_table_unref(cfg->classifiers_symbols);
+ g_hash_table_unref(cfg->debug_modules);
+ g_hash_table_unref(cfg->explicit_modules);
+ g_hash_table_unref(cfg->trusted_keys);
+
+ rspamd_re_cache_unref(cfg->re_cache);
+ g_ptr_array_free(cfg->c_modules, TRUE);
+ g_ptr_array_free(cfg->script_modules, TRUE);
+
+ if (cfg->monitored_ctx) {
+ rspamd_monitored_ctx_destroy(cfg->monitored_ctx);
+ }
+
+ if (RSPAMD_LUA_CFG_STATE(cfg) && cfg->own_lua_state) {
+ lua_thread_pool_free((struct lua_thread_pool *) cfg->lua_thread_pool);
+ rspamd_lua_close(RSPAMD_LUA_CFG_STATE(cfg));
+ }
+
+ if (cfg->redis_pool) {
+ rspamd_redis_pool_destroy(cfg->redis_pool);
+ }
+
+ rspamd_upstreams_library_unref(cfg->ups_ctx);
+ delete RSPAMD_CFG_ACTIONS(cfg);
+
+ rspamd_mempool_destructors_enforce(cfg->cfg_pool);
+
+ if (cfg->checksum) {
+ g_free(cfg->checksum);
+ }
+
+ REF_RELEASE(cfg->libs_ctx);
+
+ DL_FOREACH_SAFE(cfg->log_pipes, lp, ltmp)
+ {
+ close(lp->fd);
+ g_free(lp);
+ }
+
+ rspamd_mempool_delete(cfg->cfg_pool);
+}
+
+const ucl_object_t *
+rspamd_config_get_module_opt(struct rspamd_config *cfg,
+ const gchar *module_name,
+ const gchar *opt_name)
+{
+ const ucl_object_t *res = nullptr, *sec;
+
+ sec = ucl_obj_get_key(cfg->cfg_ucl_obj, module_name);
+ if (sec != nullptr) {
+ res = ucl_obj_get_key(sec, opt_name);
+ }
+
+ return res;
+}
+
+gint rspamd_config_parse_flag(const gchar *str, guint len)
+{
+ gint c;
+
+ if (!str || !*str) {
+ return -1;
+ }
+
+ if (len == 0) {
+ len = strlen(str);
+ }
+
+ switch (len) {
+ case 1:
+ c = g_ascii_tolower(*str);
+ if (c == 'y' || c == '1') {
+ return 1;
+ }
+ else if (c == 'n' || c == '0') {
+ return 0;
+ }
+ break;
+ case 2:
+ if (g_ascii_strncasecmp(str, "no", len) == 0) {
+ return 0;
+ }
+ else if (g_ascii_strncasecmp(str, "on", len) == 0) {
+ return 1;
+ }
+ break;
+ case 3:
+ if (g_ascii_strncasecmp(str, "yes", len) == 0) {
+ return 1;
+ }
+ else if (g_ascii_strncasecmp(str, "off", len) == 0) {
+ return 0;
+ }
+ break;
+ case 4:
+ if (g_ascii_strncasecmp(str, "true", len) == 0) {
+ return 1;
+ }
+ break;
+ case 5:
+ if (g_ascii_strncasecmp(str, "false", len) == 0) {
+ return 0;
+ }
+ break;
+ }
+
+ return -1;
+}
+
+// A mapping between names and log format types + flags
+constexpr const auto config_vars = frozen::make_unordered_map<frozen::string, std::pair<rspamd_log_format_type, int>>({
+ {"mid", {RSPAMD_LOG_MID, 0}},
+ {"qid", {RSPAMD_LOG_QID, 0}},
+ {"user", {RSPAMD_LOG_USER, 0}},
+ {"ip", {RSPAMD_LOG_IP, 0}},
+ {"len", {RSPAMD_LOG_LEN, 0}},
+ {"dns_req", {RSPAMD_LOG_DNS_REQ, 0}},
+ {"smtp_from", {RSPAMD_LOG_SMTP_FROM, 0}},
+ {"mime_from", {RSPAMD_LOG_MIME_FROM, 0}},
+ {"smtp_rcpt", {RSPAMD_LOG_SMTP_RCPT, 0}},
+ {"mime_rcpt", {RSPAMD_LOG_MIME_RCPT, 0}},
+ {"smtp_rcpts", {RSPAMD_LOG_SMTP_RCPTS, 0}},
+ {"mime_rcpts", {RSPAMD_LOG_MIME_RCPTS, 0}},
+ {"time_real", {RSPAMD_LOG_TIME_REAL, 0}},
+ {"time_virtual", {RSPAMD_LOG_TIME_VIRTUAL, 0}},
+ {"lua", {RSPAMD_LOG_LUA, 0}},
+ {"digest", {RSPAMD_LOG_DIGEST, 0}},
+ {"checksum", {RSPAMD_LOG_DIGEST, 0}},
+ {"filename", {RSPAMD_LOG_FILENAME, 0}},
+ {"forced_action", {RSPAMD_LOG_FORCED_ACTION, 0}},
+ {"settings_id", {RSPAMD_LOG_SETTINGS_ID, 0}},
+ {"mempool_size", {RSPAMD_LOG_MEMPOOL_SIZE, 0}},
+ {"mempool_waste", {RSPAMD_LOG_MEMPOOL_WASTE, 0}},
+ {"action", {RSPAMD_LOG_ACTION, 0}},
+ {"scores", {RSPAMD_LOG_SCORES, 0}},
+ {"symbols", {RSPAMD_LOG_SYMBOLS, 0}},
+ {"symbols_scores", {RSPAMD_LOG_SYMBOLS, RSPAMD_LOG_FMT_FLAG_SYMBOLS_SCORES}},
+ {"symbols_params", {RSPAMD_LOG_SYMBOLS, RSPAMD_LOG_FMT_FLAG_SYMBOLS_PARAMS}},
+ {"symbols_scores_params", {RSPAMD_LOG_SYMBOLS, RSPAMD_LOG_FMT_FLAG_SYMBOLS_PARAMS | RSPAMD_LOG_FMT_FLAG_SYMBOLS_SCORES}},
+ {"groups", {RSPAMD_LOG_GROUPS, 0}},
+ {"public_groups", {RSPAMD_LOG_PUBLIC_GROUPS, 0}},
+ {"is_spam", {RSPAMD_LOG_ISSPAM, 0}},
+});
+
+static gboolean
+rspamd_config_process_var(struct rspamd_config *cfg, const rspamd_ftok_t *var,
+ const rspamd_ftok_t *content)
+{
+ g_assert(var != nullptr);
+
+ auto flags = 0;
+ auto lc_var = std::string{var->begin, var->len};
+ std::transform(lc_var.begin(), lc_var.end(), lc_var.begin(), g_ascii_tolower);
+ auto tok = std::string_view{lc_var};
+
+ if (var->len > 3 && tok.starts_with("if_")) {
+ flags |= RSPAMD_LOG_FMT_FLAG_CONDITION;
+ tok = tok.substr(3);
+ }
+
+ auto maybe_fmt_var = rspamd::find_map(config_vars, tok);
+
+ if (maybe_fmt_var) {
+ auto &fmt_var = maybe_fmt_var.value().get();
+ auto *log_format = rspamd_mempool_alloc0_type(cfg->cfg_pool, rspamd_log_format);
+
+ log_format->type = fmt_var.first;
+ log_format->flags = fmt_var.second | flags;
+
+ if (log_format->type != RSPAMD_LOG_LUA) {
+ if (content && content->len > 0) {
+ log_format->data = rspamd_mempool_alloc0(cfg->cfg_pool,
+ sizeof(rspamd_ftok_t));
+ memcpy(log_format->data, content, sizeof(*content));
+ log_format->len = sizeof(*content);
+ }
+ }
+ else {
+ /* Load lua code and ensure that we have function ref returned */
+ if (!content || content->len == 0) {
+ msg_err_config("lua variable needs content: %T", &tok);
+ return FALSE;
+ }
+
+ if (luaL_loadbuffer(RSPAMD_LUA_CFG_STATE(cfg), content->begin, content->len,
+ "lua log variable") != 0) {
+ msg_err_config("error loading lua code: '%T': %s", content,
+ lua_tostring(RSPAMD_LUA_CFG_STATE(cfg), -1));
+ return FALSE;
+ }
+ if (lua_pcall(RSPAMD_LUA_CFG_STATE(cfg), 0, 1, 0) != 0) {
+ msg_err_config("error executing lua code: '%T': %s", content,
+ lua_tostring(RSPAMD_LUA_CFG_STATE(cfg), -1));
+ lua_pop(RSPAMD_LUA_CFG_STATE(cfg), 1);
+
+ return FALSE;
+ }
+
+ if (lua_type(RSPAMD_LUA_CFG_STATE(cfg), -1) != LUA_TFUNCTION) {
+ msg_err_config("lua variable should return function: %T", content);
+ lua_pop(RSPAMD_LUA_CFG_STATE(cfg), 1);
+ return FALSE;
+ }
+
+ auto id = luaL_ref(RSPAMD_LUA_CFG_STATE(cfg), LUA_REGISTRYINDEX);
+ log_format->data = GINT_TO_POINTER(id);
+ log_format->len = 0;
+ }
+
+ DL_APPEND(cfg->log_format, log_format);
+ }
+ else {
+ std::string known_formats;
+
+ for (const auto &v: config_vars) {
+ known_formats += std::string_view{v.first.data(), v.first.size()};
+ known_formats += ", ";
+ }
+
+ if (known_formats.size() > 2) {
+ // Remove last comma
+ known_formats.resize(known_formats.size() - 2);
+ }
+ msg_err_config("unknown log variable: %T, known vars are: \"%s\"", var, known_formats.c_str());
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_config_parse_log_format(struct rspamd_config *cfg)
+{
+ const gchar *p, *c, *end, *s;
+ gchar *d;
+ struct rspamd_log_format *lf = nullptr;
+ rspamd_ftok_t var, var_content;
+ enum {
+ parse_str,
+ parse_dollar,
+ parse_var_name,
+ parse_var_content,
+ } state = parse_str;
+ gint braces = 0;
+
+ g_assert(cfg != nullptr);
+ c = cfg->log_format_str;
+
+ if (c == nullptr) {
+ return FALSE;
+ }
+
+ p = c;
+ end = p + strlen(p);
+
+ while (p < end) {
+ switch (state) {
+ case parse_str:
+ if (*p == '$') {
+ state = parse_dollar;
+ }
+ else {
+ p++;
+ }
+ break;
+ case parse_dollar:
+ if (p > c) {
+ /* We have string element that we need to store */
+ lf = rspamd_mempool_alloc0_type(cfg->cfg_pool, struct rspamd_log_format);
+ lf->type = RSPAMD_LOG_STRING;
+ lf->data = rspamd_mempool_alloc(cfg->cfg_pool, p - c + 1);
+ /* Filter \r\n from the destination */
+ s = c;
+ d = (char *) lf->data;
+
+ while (s < p) {
+ if (*s != '\r' && *s != '\n') {
+ *d++ = *s++;
+ }
+ else {
+ *d++ = ' ';
+ s++;
+ }
+ }
+ *d = '\0';
+
+ lf->len = d - (char *) lf->data;
+ DL_APPEND(cfg->log_format, lf);
+ lf = nullptr;
+ }
+ p++;
+ c = p;
+ state = parse_var_name;
+ break;
+ case parse_var_name:
+ if (*p == '{') {
+ var.begin = c;
+ var.len = p - c;
+ p++;
+ c = p;
+ state = parse_var_content;
+ braces = 1;
+ }
+ else if (*p != '_' && *p != '-' && !g_ascii_isalnum(*p)) {
+ /* Variable with no content */
+ var.begin = c;
+ var.len = p - c;
+ c = p;
+
+ if (!rspamd_config_process_var(cfg, &var, nullptr)) {
+ return FALSE;
+ }
+
+ state = parse_str;
+ }
+ else {
+ p++;
+ }
+ break;
+ case parse_var_content:
+ if (*p == '}' && --braces == 0) {
+ var_content.begin = c;
+ var_content.len = p - c;
+ p++;
+ c = p;
+
+ if (!rspamd_config_process_var(cfg, &var, &var_content)) {
+ return FALSE;
+ }
+
+ state = parse_str;
+ }
+ else if (*p == '{') {
+ braces++;
+ p++;
+ }
+ else {
+ p++;
+ }
+ break;
+ }
+ }
+
+ /* Last state */
+ switch (state) {
+ case parse_str:
+ if (p > c) {
+ /* We have string element that we need to store */
+ lf = rspamd_mempool_alloc0_type(cfg->cfg_pool, struct rspamd_log_format);
+ lf->type = RSPAMD_LOG_STRING;
+ lf->data = rspamd_mempool_alloc(cfg->cfg_pool, p - c + 1);
+ /* Filter \r\n from the destination */
+ s = c;
+ d = (char *) lf->data;
+
+ while (s < p) {
+ if (*s != '\r' && *s != '\n') {
+ *d++ = *s++;
+ }
+ else {
+ *d++ = ' ';
+ s++;
+ }
+ }
+ *d = '\0';
+
+ lf->len = d - (char *) lf->data;
+ DL_APPEND(cfg->log_format, lf);
+ lf = nullptr;
+ }
+ break;
+
+ case parse_var_name:
+ var.begin = c;
+ var.len = p - c;
+
+ if (!rspamd_config_process_var(cfg, &var, nullptr)) {
+ return FALSE;
+ }
+ break;
+ case parse_dollar:
+ case parse_var_content:
+ msg_err_config("cannot parse log format %s: incomplete string",
+ cfg->log_format_str);
+ return FALSE;
+ break;
+ }
+
+ return TRUE;
+}
+
+static void
+rspamd_urls_config_dtor(gpointer _unused)
+{
+ rspamd_url_deinit();
+}
+
+static void
+rspamd_adjust_clocks_resolution(struct rspamd_config *cfg)
+{
+#ifdef HAVE_CLOCK_GETTIME
+ struct timespec ts;
+#endif
+
+#ifdef HAVE_CLOCK_GETTIME
+#ifdef HAVE_CLOCK_PROCESS_CPUTIME_ID
+ clock_getres(CLOCK_PROCESS_CPUTIME_ID, &ts);
+#elif defined(HAVE_CLOCK_VIRTUAL)
+ clock_getres(CLOCK_VIRTUAL, &ts);
+#else
+ clock_getres(CLOCK_REALTIME, &ts);
+#endif
+ cfg->clock_res = log10(1000000. / ts.tv_nsec);
+ if (cfg->clock_res < 0) {
+ cfg->clock_res = 0;
+ }
+ if (cfg->clock_res > 3) {
+ cfg->clock_res = 3;
+ }
+#else
+ /* For gettimeofday */
+ cfg->clock_res = 1;
+#endif
+}
+
+/*
+ * Perform post load actions
+ */
+gboolean
+rspamd_config_post_load(struct rspamd_config *cfg,
+ enum rspamd_post_load_options opts)
+{
+
+ auto ret = TRUE;
+
+ rspamd_adjust_clocks_resolution(cfg);
+ rspamd_logger_configure_modules(cfg->debug_modules);
+
+ if (cfg->one_shot_mode) {
+ msg_info_config("enabling one shot mode (was %d max shots)",
+ cfg->default_max_shots);
+ cfg->default_max_shots = 1;
+ }
+
+#if defined(WITH_HYPERSCAN) && !defined(__aarch64__) && !defined(__powerpc64__)
+ if (!cfg->disable_hyperscan) {
+ if (!(cfg->libs_ctx->crypto_ctx->cpu_config & CPUID_SSSE3)) {
+ msg_warn_config("CPU doesn't have SSSE3 instructions set "
+ "required for hyperscan, disable it");
+ cfg->disable_hyperscan = TRUE;
+ }
+ }
+#endif
+
+ rspamd_regexp_library_init(cfg);
+ rspamd_multipattern_library_init(cfg->hs_cache_dir);
+
+ if (opts & RSPAMD_CONFIG_INIT_URL) {
+ if (cfg->tld_file == nullptr) {
+ /* Try to guess tld file */
+ auto fpath = fmt::format("{0}{1}{2}", RSPAMD_SHAREDIR,
+ G_DIR_SEPARATOR, "effective_tld_names.dat");
+
+ if (access(fpath.c_str(), R_OK) != -1) {
+ msg_debug_config("url_tld option is not specified but %s is available,"
+ " therefore this file is assumed as TLD file for URL"
+ " extraction",
+ fpath.c_str());
+ cfg->tld_file = rspamd_mempool_strdup(cfg->cfg_pool, fpath.c_str());
+ }
+ else {
+ if (opts & RSPAMD_CONFIG_INIT_VALIDATE) {
+ msg_err_config("no url_tld option has been specified");
+ ret = FALSE;
+ }
+ }
+ }
+ else {
+ if (access(cfg->tld_file, R_OK) == -1) {
+ if (opts & RSPAMD_CONFIG_INIT_VALIDATE) {
+ ret = FALSE;
+ msg_err_config("cannot access tld file %s: %s", cfg->tld_file,
+ strerror(errno));
+ }
+ else {
+ msg_debug_config("cannot access tld file %s: %s", cfg->tld_file,
+ strerror(errno));
+ cfg->tld_file = nullptr;
+ }
+ }
+ }
+
+ if (opts & RSPAMD_CONFIG_INIT_NO_TLD) {
+ rspamd_url_init(nullptr);
+ }
+ else {
+ rspamd_url_init(cfg->tld_file);
+ }
+
+ rspamd_mempool_add_destructor(cfg->cfg_pool, rspamd_urls_config_dtor,
+ nullptr);
+ }
+
+ init_dynamic_config(cfg);
+ /* Insert classifiers symbols */
+ rspamd_config_insert_classify_symbols(cfg);
+
+ /* Parse format string that we have */
+ if (!rspamd_config_parse_log_format(cfg)) {
+ msg_err_config("cannot parse log format, task logging will not be available");
+ if (opts & RSPAMD_CONFIG_INIT_VALIDATE) {
+ ret = FALSE;
+ }
+ }
+
+ if (opts & RSPAMD_CONFIG_INIT_SYMCACHE) {
+ /* Init config cache */
+ ret = rspamd_symcache_init(cfg->cache) && ret;
+
+ /* Init re cache */
+ rspamd_re_cache_init(cfg->re_cache, cfg);
+
+ /* Try load Hypersan */
+ auto hs_ret = rspamd_re_cache_load_hyperscan(cfg->re_cache,
+ cfg->hs_cache_dir ? cfg->hs_cache_dir : RSPAMD_DBDIR "/",
+ true);
+
+ if (hs_ret == RSPAMD_HYPERSCAN_LOAD_ERROR) {
+ msg_debug_config("cannot load hyperscan database, disable it");
+ }
+ }
+
+ if (opts & RSPAMD_CONFIG_INIT_LIBS) {
+ /* Config other libraries */
+ ret = rspamd_config_libs(cfg->libs_ctx, cfg) && ret;
+
+ if (!ret) {
+ msg_err_config("cannot configure libraries, fatal error");
+ return FALSE;
+ }
+ }
+
+ /* Validate cache */
+ if (opts & RSPAMD_CONFIG_INIT_VALIDATE) {
+ /* Check for actions sanity */
+ auto seen_controller = FALSE;
+
+ auto *cur = cfg->workers;
+ while (cur) {
+ auto *wcf = (struct rspamd_worker_conf *) cur->data;
+
+ if (wcf->type == g_quark_from_static_string("controller")) {
+ seen_controller = TRUE;
+ break;
+ }
+
+ cur = g_list_next(cur);
+ }
+
+ if (!seen_controller) {
+ msg_warn_config("controller worker is unconfigured: learning,"
+ " periodic scripts, maps watching and many other"
+ " Rspamd features will be broken");
+ }
+
+ ret = rspamd_symcache_validate(cfg->cache, cfg, FALSE) && ret;
+ }
+
+ if (opts & RSPAMD_CONFIG_INIT_POST_LOAD_LUA) {
+ rspamd_lua_run_config_post_init(RSPAMD_LUA_CFG_STATE(cfg), cfg);
+ }
+
+ if (opts & RSPAMD_CONFIG_INIT_PRELOAD_MAPS) {
+ rspamd_map_preload(cfg);
+ }
+
+ return ret;
+}
+
+struct rspamd_classifier_config *
+rspamd_config_new_classifier(struct rspamd_config *cfg,
+ struct rspamd_classifier_config *c)
+{
+ if (c == nullptr) {
+ c =
+ rspamd_mempool_alloc0_type(cfg->cfg_pool,
+ struct rspamd_classifier_config);
+ c->min_prob_strength = 0.05;
+ c->min_token_hits = 2;
+ }
+
+ if (c->labels == nullptr) {
+ c->labels = g_hash_table_new_full(rspamd_str_hash,
+ rspamd_str_equal,
+ nullptr,
+ (GDestroyNotify) g_list_free);
+ rspamd_mempool_add_destructor(cfg->cfg_pool,
+ (rspamd_mempool_destruct_t) g_hash_table_destroy,
+ c->labels);
+ }
+
+ return c;
+}
+
+struct rspamd_statfile_config *
+rspamd_config_new_statfile(struct rspamd_config *cfg,
+ struct rspamd_statfile_config *c)
+{
+ if (c == nullptr) {
+ c =
+ rspamd_mempool_alloc0_type(cfg->cfg_pool, struct rspamd_statfile_config);
+ }
+
+ return c;
+}
+
+void rspamd_config_init_metric(struct rspamd_config *cfg)
+{
+ cfg->grow_factor = 1.0;
+ cfg->symbols = g_hash_table_new(rspamd_str_hash, rspamd_str_equal);
+ cfg->groups = g_hash_table_new(rspamd_strcase_hash, rspamd_strcase_equal);
+
+ cfg->subject = SPAM_SUBJECT;
+ rspamd_mempool_add_destructor(cfg->cfg_pool,
+ (rspamd_mempool_destruct_t) g_hash_table_unref,
+ cfg->symbols);
+ rspamd_mempool_add_destructor(cfg->cfg_pool,
+ (rspamd_mempool_destruct_t) g_hash_table_unref,
+ cfg->groups);
+}
+
+struct rspamd_symbols_group *
+rspamd_config_new_group(struct rspamd_config *cfg, const gchar *name)
+{
+ struct rspamd_symbols_group *gr;
+
+ gr = rspamd_mempool_alloc0_type(cfg->cfg_pool, struct rspamd_symbols_group);
+ gr->symbols = g_hash_table_new(rspamd_strcase_hash,
+ rspamd_strcase_equal);
+ rspamd_mempool_add_destructor(cfg->cfg_pool,
+ (rspamd_mempool_destruct_t) g_hash_table_unref, gr->symbols);
+ gr->name = rspamd_mempool_strdup(cfg->cfg_pool, name);
+
+ if (strcmp(gr->name, "ungrouped") == 0) {
+ gr->flags |= RSPAMD_SYMBOL_GROUP_UNGROUPED;
+ }
+
+ g_hash_table_insert(cfg->groups, gr->name, gr);
+
+ return gr;
+}
+
+static void
+rspamd_worker_conf_dtor(struct rspamd_worker_conf *wcf)
+{
+ if (wcf) {
+ ucl_object_unref(wcf->options);
+ g_queue_free(wcf->active_workers);
+ g_hash_table_unref(wcf->params);
+ g_free(wcf);
+ }
+}
+
+static void
+rspamd_worker_conf_cfg_fin(gpointer d)
+{
+ auto *wcf = (struct rspamd_worker_conf *) d;
+
+ REF_RELEASE(wcf);
+}
+
+struct rspamd_worker_conf *
+rspamd_config_new_worker(struct rspamd_config *cfg,
+ struct rspamd_worker_conf *c)
+{
+ if (c == nullptr) {
+ c = g_new0(struct rspamd_worker_conf, 1);
+ c->params = g_hash_table_new(rspamd_str_hash, rspamd_str_equal);
+ c->active_workers = g_queue_new();
+#ifdef HAVE_SC_NPROCESSORS_ONLN
+ auto nproc = sysconf(_SC_NPROCESSORS_ONLN);
+ c->count = MIN(DEFAULT_MAX_WORKERS, MAX(1, nproc - 2));
+#else
+ c->count = DEFAULT_MAX_WORKERS;
+#endif
+ c->rlimit_nofile = 0;
+ c->rlimit_maxcore = 0;
+ c->enabled = TRUE;
+
+ REF_INIT_RETAIN(c, rspamd_worker_conf_dtor);
+ rspamd_mempool_add_destructor(cfg->cfg_pool,
+ rspamd_worker_conf_cfg_fin, c);
+ }
+
+ return c;
+}
+
+
+static bool
+rspamd_include_map_handler(const guchar *data, gsize len,
+ const ucl_object_t *args, void *ud)
+{
+ auto *cfg = (struct rspamd_config *) ud;
+
+ auto ftok = rspamd_ftok_t{.len = len + 1, .begin = (char *) data};
+ auto *map_line = rspamd_mempool_ftokdup(cfg->cfg_pool, &ftok);
+
+ auto *cbdata = new rspamd_ucl_map_cbdata{cfg};
+ auto **pcbdata = new rspamd_ucl_map_cbdata *(cbdata);
+
+ return rspamd_map_add(cfg,
+ map_line,
+ "ucl include",
+ rspamd_ucl_read_cb,
+ rspamd_ucl_fin_cb,
+ rspamd_ucl_dtor_cb,
+ (void **) pcbdata,
+ nullptr, RSPAMD_MAP_DEFAULT) != nullptr;
+}
+
+/*
+ * Variables:
+ * $CONFDIR - configuration directory
+ * $LOCAL_CONFDIR - local configuration directory
+ * $RUNDIR - local states directory
+ * $DBDIR - databases dir
+ * $LOGDIR - logs dir
+ * $PLUGINSDIR - plugins dir
+ * $PREFIX - installation prefix
+ * $VERSION - rspamd version
+ */
+
+#define RSPAMD_CONFDIR_MACRO "CONFDIR"
+#define RSPAMD_LOCAL_CONFDIR_MACRO "LOCAL_CONFDIR"
+#define RSPAMD_RUNDIR_MACRO "RUNDIR"
+#define RSPAMD_DBDIR_MACRO "DBDIR"
+#define RSPAMD_LOGDIR_MACRO "LOGDIR"
+#define RSPAMD_PLUGINSDIR_MACRO "PLUGINSDIR"
+#define RSPAMD_SHAREDIR_MACRO "SHAREDIR"
+#define RSPAMD_RULESDIR_MACRO "RULESDIR"
+#define RSPAMD_WWWDIR_MACRO "WWWDIR"
+#define RSPAMD_PREFIX_MACRO "PREFIX"
+#define RSPAMD_VERSION_MACRO "VERSION"
+#define RSPAMD_VERSION_MAJOR_MACRO "VERSION_MAJOR"
+#define RSPAMD_VERSION_MINOR_MACRO "VERSION_MINOR"
+#define RSPAMD_BRANCH_VERSION_MACRO "BRANCH_VERSION"
+#define RSPAMD_HOSTNAME_MACRO "HOSTNAME"
+
+void rspamd_ucl_add_conf_variables(struct ucl_parser *parser, GHashTable *vars)
+{
+ GHashTableIter it;
+ gpointer k, v;
+
+ ucl_parser_register_variable(parser,
+ RSPAMD_CONFDIR_MACRO,
+ RSPAMD_CONFDIR);
+ ucl_parser_register_variable(parser,
+ RSPAMD_LOCAL_CONFDIR_MACRO,
+ RSPAMD_LOCAL_CONFDIR);
+ ucl_parser_register_variable(parser, RSPAMD_RUNDIR_MACRO,
+ RSPAMD_RUNDIR);
+ ucl_parser_register_variable(parser, RSPAMD_DBDIR_MACRO,
+ RSPAMD_DBDIR);
+ ucl_parser_register_variable(parser, RSPAMD_LOGDIR_MACRO,
+ RSPAMD_LOGDIR);
+ ucl_parser_register_variable(parser,
+ RSPAMD_PLUGINSDIR_MACRO,
+ RSPAMD_PLUGINSDIR);
+ ucl_parser_register_variable(parser,
+ RSPAMD_SHAREDIR_MACRO,
+ RSPAMD_SHAREDIR);
+ ucl_parser_register_variable(parser,
+ RSPAMD_RULESDIR_MACRO,
+ RSPAMD_RULESDIR);
+ ucl_parser_register_variable(parser, RSPAMD_WWWDIR_MACRO,
+ RSPAMD_WWWDIR);
+ ucl_parser_register_variable(parser, RSPAMD_PREFIX_MACRO,
+ RSPAMD_PREFIX);
+ ucl_parser_register_variable(parser, RSPAMD_VERSION_MACRO, RVERSION);
+ ucl_parser_register_variable(parser, RSPAMD_VERSION_MAJOR_MACRO,
+ RSPAMD_VERSION_MAJOR);
+ ucl_parser_register_variable(parser, RSPAMD_VERSION_MINOR_MACRO,
+ RSPAMD_VERSION_MINOR);
+ ucl_parser_register_variable(parser, RSPAMD_BRANCH_VERSION_MACRO,
+ RSPAMD_VERSION_BRANCH);
+
+ auto hostlen = sysconf(_SC_HOST_NAME_MAX);
+
+ if (hostlen <= 0) {
+ hostlen = 256;
+ }
+ else {
+ hostlen++;
+ }
+
+ auto hostbuf = std::string{};
+ hostbuf.resize(hostlen);
+
+ if (gethostname(hostbuf.data(), hostlen) != 0) {
+ hostbuf = "unknown";
+ }
+
+ /* UCL copies variables, so it is safe to pass an ephemeral buffer here */
+ ucl_parser_register_variable(parser, RSPAMD_HOSTNAME_MACRO,
+ hostbuf.c_str());
+
+ if (vars != nullptr) {
+ g_hash_table_iter_init(&it, vars);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ ucl_parser_register_variable(parser, (const char *) k, (const char *) v);
+ }
+ }
+}
+
+void rspamd_ucl_add_conf_macros(struct ucl_parser *parser,
+ struct rspamd_config *cfg)
+{
+ ucl_parser_register_macro(parser,
+ "include_map",
+ rspamd_include_map_handler,
+ cfg);
+}
+
+static void
+symbols_classifiers_callback(gpointer key, gpointer value, gpointer ud)
+{
+ auto *cfg = (struct rspamd_config *) ud;
+
+ /* Actually, statistics should act like any ordinary symbol */
+ rspamd_symcache_add_symbol(cfg->cache, (const char *) key, 0, nullptr, nullptr,
+ SYMBOL_TYPE_CLASSIFIER | SYMBOL_TYPE_NOSTAT, -1);
+}
+
+void rspamd_config_insert_classify_symbols(struct rspamd_config *cfg)
+{
+ g_hash_table_foreach(cfg->classifiers_symbols,
+ symbols_classifiers_callback,
+ cfg);
+}
+
+struct rspamd_classifier_config *
+rspamd_config_find_classifier(struct rspamd_config *cfg, const gchar *name)
+{
+ if (name == nullptr) {
+ return nullptr;
+ }
+
+ auto *cur = cfg->classifiers;
+ while (cur) {
+ auto *cf = (struct rspamd_classifier_config *) cur->data;
+
+ if (g_ascii_strcasecmp(cf->name, name) == 0) {
+ return cf;
+ }
+
+ cur = g_list_next(cur);
+ }
+
+ return nullptr;
+}
+
+gboolean
+rspamd_config_check_statfiles(struct rspamd_classifier_config *cf)
+{
+ gboolean has_other = FALSE, res = FALSE, cur_class = FALSE;
+
+ /* First check classes directly */
+ auto *cur = cf->statfiles;
+ while (cur) {
+ auto *st = (struct rspamd_statfile_config *) cur->data;
+ if (!has_other) {
+ cur_class = st->is_spam;
+ has_other = TRUE;
+ }
+ else {
+ if (cur_class != st->is_spam) {
+ return TRUE;
+ }
+ }
+
+ cur = g_list_next(cur);
+ }
+
+ if (!has_other) {
+ /* We have only one statfile */
+ return FALSE;
+ }
+ /* We have not detected any statfile that has different class, so turn on heuristic based on symbol's name */
+ has_other = FALSE;
+ cur = cf->statfiles;
+ while (cur) {
+ auto *st = (struct rspamd_statfile_config *) cur->data;
+ if (rspamd_substring_search_caseless(st->symbol,
+ strlen(st->symbol), "spam", 4) != -1) {
+ st->is_spam = TRUE;
+ }
+ else if (rspamd_substring_search_caseless(st->symbol,
+ strlen(st->symbol), "ham", 3) != -1) {
+ st->is_spam = FALSE;
+ }
+
+ if (!has_other) {
+ cur_class = st->is_spam;
+ has_other = TRUE;
+ }
+ else {
+ if (cur_class != st->is_spam) {
+ res = TRUE;
+ }
+ }
+
+ cur = g_list_next(cur);
+ }
+
+ return res;
+}
+
+static gchar *
+rspamd_ucl_read_cb(gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final)
+{
+ auto *cbdata = (struct rspamd_ucl_map_cbdata *) data->cur_data;
+ auto *prev = (struct rspamd_ucl_map_cbdata *) data->prev_data;
+
+ if (cbdata == nullptr) {
+ cbdata = new rspamd_ucl_map_cbdata{prev->cfg};
+ data->cur_data = cbdata;
+ }
+ cbdata->buf.append(chunk, len);
+
+ /* Say not to copy any part of this buffer */
+ return nullptr;
+}
+
+static void
+rspamd_ucl_fin_cb(struct map_cb_data *data, void **target)
+{
+ auto *cbdata = (struct rspamd_ucl_map_cbdata *) data->cur_data;
+ auto *prev = (struct rspamd_ucl_map_cbdata *) data->prev_data;
+ auto *cfg = data->map->cfg;
+
+ if (cbdata == nullptr) {
+ msg_err_config("map fin error: new data is nullptr");
+ return;
+ }
+
+ /* New data available */
+ auto *parser = ucl_parser_new(0);
+ if (!ucl_parser_add_chunk(parser, (unsigned char *) cbdata->buf.data(),
+ cbdata->buf.size())) {
+ msg_err_config("cannot parse map %s: %s",
+ data->map->name,
+ ucl_parser_get_error(parser));
+ ucl_parser_free(parser);
+ }
+ else {
+ auto *obj = ucl_parser_get_object(parser);
+ ucl_object_iter_t it = nullptr;
+
+ for (auto *cur = ucl_object_iterate(obj, &it, true); cur != nullptr; cur = ucl_object_iterate(obj, &it, true)) {
+ ucl_object_replace_key(cbdata->cfg->cfg_ucl_obj, (ucl_object_t *) cur,
+ cur->key, cur->keylen, false);
+ }
+
+ ucl_parser_free(parser);
+ ucl_object_unref(obj);
+ }
+
+ if (target) {
+ *target = data->cur_data;
+ }
+
+ delete prev;
+}
+
+static void
+rspamd_ucl_dtor_cb(struct map_cb_data *data)
+{
+ auto *cbdata = (struct rspamd_ucl_map_cbdata *) data->cur_data;
+
+ delete cbdata;
+}
+
+gboolean
+rspamd_check_module(struct rspamd_config *cfg, module_t *mod)
+{
+ gboolean ret = TRUE;
+
+ if (mod != nullptr) {
+ if (mod->module_version != RSPAMD_CUR_MODULE_VERSION) {
+ msg_err_config("module %s has incorrect version %xd (%xd expected)",
+ mod->name, (gint) mod->module_version, RSPAMD_CUR_MODULE_VERSION);
+ ret = FALSE;
+ }
+ if (ret && mod->rspamd_version != RSPAMD_VERSION_NUM) {
+ msg_err_config("module %s has incorrect rspamd version %xL (%xL expected)",
+ mod->name, mod->rspamd_version, RSPAMD_VERSION_NUM);
+ ret = FALSE;
+ }
+ if (ret && strcmp(mod->rspamd_features, RSPAMD_FEATURES) != 0) {
+ msg_err_config("module %s has incorrect rspamd features '%s' ('%s' expected)",
+ mod->name, mod->rspamd_features, RSPAMD_FEATURES);
+ ret = FALSE;
+ }
+ }
+ else {
+ ret = FALSE;
+ }
+
+ return ret;
+}
+
+gboolean
+rspamd_check_worker(struct rspamd_config *cfg, worker_t *wrk)
+{
+ gboolean ret = TRUE;
+
+ if (wrk != nullptr) {
+ if (wrk->worker_version != RSPAMD_CUR_WORKER_VERSION) {
+ msg_err_config("worker %s has incorrect version %xd (%xd expected)",
+ wrk->name, wrk->worker_version, RSPAMD_CUR_WORKER_VERSION);
+ ret = FALSE;
+ }
+ if (ret && wrk->rspamd_version != RSPAMD_VERSION_NUM) {
+ msg_err_config("worker %s has incorrect rspamd version %xL (%xL expected)",
+ wrk->name, wrk->rspamd_version, RSPAMD_VERSION_NUM);
+ ret = FALSE;
+ }
+ if (ret && strcmp(wrk->rspamd_features, RSPAMD_FEATURES) != 0) {
+ msg_err_config("worker %s has incorrect rspamd features '%s' ('%s' expected)",
+ wrk->name, wrk->rspamd_features, RSPAMD_FEATURES);
+ ret = FALSE;
+ }
+ }
+ else {
+ ret = FALSE;
+ }
+
+ return ret;
+}
+
+gboolean
+rspamd_init_filters(struct rspamd_config *cfg, bool reconfig, bool strict)
+{
+ GList *cur;
+ module_t *mod, **pmod;
+ guint i = 0;
+ struct module_ctx *mod_ctx, *cur_ctx;
+ gboolean ret = TRUE;
+
+ /* Init all compiled modules */
+
+ for (pmod = cfg->compiled_modules; pmod != nullptr && *pmod != nullptr; pmod++) {
+ mod = *pmod;
+ if (rspamd_check_module(cfg, mod)) {
+ if (mod->module_init_func(cfg, &mod_ctx) == 0) {
+ g_assert(mod_ctx != nullptr);
+ g_ptr_array_add(cfg->c_modules, mod_ctx);
+ mod_ctx->mod = mod;
+ mod->ctx_offset = i++;
+ }
+ }
+ }
+
+ /* Now check what's enabled */
+ cur = g_list_first(cfg->filters);
+
+ while (cur) {
+ /* Perform modules configuring */
+ mod_ctx = nullptr;
+ PTR_ARRAY_FOREACH(cfg->c_modules, i, cur_ctx)
+ {
+ if (g_ascii_strcasecmp(cur_ctx->mod->name,
+ (const gchar *) cur->data) == 0) {
+ mod_ctx = cur_ctx;
+ break;
+ }
+ }
+
+ if (mod_ctx) {
+ mod = mod_ctx->mod;
+ mod_ctx->enabled = rspamd_config_is_module_enabled(cfg, mod->name);
+
+ if (reconfig) {
+ if (!mod->module_reconfig_func(cfg)) {
+ msg_err_config("reconfig of %s failed!", mod->name);
+ }
+ else {
+ msg_info_config("reconfig of %s", mod->name);
+ }
+ }
+ else {
+ if (!mod->module_config_func(cfg, strict)) {
+ msg_err_config("config of %s failed", mod->name);
+ ret = FALSE;
+
+ if (strict) {
+ return FALSE;
+ }
+ }
+ }
+ }
+
+ if (mod_ctx == nullptr) {
+ msg_warn_config("requested unknown module %s", cur->data);
+ }
+
+ cur = g_list_next(cur);
+ }
+
+ ret = rspamd_init_lua_filters(cfg, 0, strict) && ret;
+
+ return ret;
+}
+
+static void
+rspamd_config_new_symbol(struct rspamd_config *cfg, const gchar *symbol,
+ gdouble score, const gchar *description, const gchar *group,
+ guint flags, guint priority, gint nshots)
+{
+ struct rspamd_symbols_group *sym_group;
+ struct rspamd_symbol *sym_def;
+ double *score_ptr;
+
+ sym_def =
+ rspamd_mempool_alloc0_type(cfg->cfg_pool, struct rspamd_symbol);
+ score_ptr = rspamd_mempool_alloc_type(cfg->cfg_pool, double);
+
+ if (isnan(score)) {
+ /* In fact, it could be defined later */
+ msg_debug_config("score is not defined for symbol %s, set it to zero",
+ symbol);
+ score = 0.0;
+ /* Also set priority to 0 to allow override by anything */
+ sym_def->priority = 0;
+ flags |= RSPAMD_SYMBOL_FLAG_UNSCORED;
+ }
+ else {
+ sym_def->priority = priority;
+ }
+
+ *score_ptr = score;
+ sym_def->score = score;
+ sym_def->weight_ptr = score_ptr;
+ sym_def->name = rspamd_mempool_strdup(cfg->cfg_pool, symbol);
+ sym_def->flags = flags;
+ sym_def->nshots = nshots != 0 ? nshots : cfg->default_max_shots;
+ sym_def->groups = g_ptr_array_sized_new(1);
+ rspamd_mempool_add_destructor(cfg->cfg_pool, rspamd_ptr_array_free_hard,
+ sym_def->groups);
+
+ if (description) {
+ sym_def->description = rspamd_mempool_strdup(cfg->cfg_pool, description);
+ }
+
+ msg_debug_config("registered symbol %s with weight %.2f in and group %s",
+ sym_def->name, score, group);
+
+ g_hash_table_insert(cfg->symbols, sym_def->name, sym_def);
+
+ /* Search for symbol group */
+ if (group == nullptr) {
+ group = "ungrouped";
+ sym_def->flags |= RSPAMD_SYMBOL_FLAG_UNGROUPED;
+ }
+ else {
+ if (strcmp(group, "ungrouped") == 0) {
+ sym_def->flags |= RSPAMD_SYMBOL_FLAG_UNGROUPED;
+ }
+ }
+
+ sym_group = reinterpret_cast<rspamd_symbols_group *>(g_hash_table_lookup(cfg->groups, group));
+ if (sym_group == nullptr) {
+ /* Create new group */
+ sym_group = rspamd_config_new_group(cfg, group);
+ }
+
+ sym_def->gr = sym_group;
+ g_hash_table_insert(sym_group->symbols, sym_def->name, sym_def);
+
+ if (!(sym_def->flags & RSPAMD_SYMBOL_FLAG_UNGROUPED)) {
+ g_ptr_array_add(sym_def->groups, sym_group);
+ }
+}
+
+
+gboolean
+rspamd_config_add_symbol(struct rspamd_config *cfg,
+ const gchar *symbol,
+ gdouble score,
+ const gchar *description,
+ const gchar *group,
+ guint flags,
+ guint priority,
+ gint nshots)
+{
+ struct rspamd_symbol *sym_def;
+ struct rspamd_symbols_group *sym_group;
+ guint i;
+
+ g_assert(cfg != nullptr);
+ g_assert(symbol != nullptr);
+
+ sym_def = reinterpret_cast<rspamd_symbol *>(g_hash_table_lookup(cfg->symbols, symbol));
+
+ if (sym_def != nullptr) {
+ if (group != nullptr) {
+ gboolean has_group = FALSE;
+
+ PTR_ARRAY_FOREACH(sym_def->groups, i, sym_group)
+ {
+ if (g_ascii_strcasecmp(sym_group->name, group) == 0) {
+ /* Group is already here */
+ has_group = TRUE;
+ break;
+ }
+ }
+
+ if (!has_group) {
+ /* Non-empty group has a priority over non-grouped one */
+ sym_group = reinterpret_cast<rspamd_symbols_group *>(g_hash_table_lookup(cfg->groups, group));
+
+ if (sym_group == nullptr) {
+ /* Create new group */
+ sym_group = rspamd_config_new_group(cfg, group);
+ }
+
+ if ((!sym_def->gr) || (sym_def->flags & RSPAMD_SYMBOL_FLAG_UNGROUPED)) {
+ sym_def->gr = sym_group;
+ sym_def->flags &= ~RSPAMD_SYMBOL_FLAG_UNGROUPED;
+ }
+
+ g_hash_table_insert(sym_group->symbols, sym_def->name, sym_def);
+ sym_def->flags &= ~(RSPAMD_SYMBOL_FLAG_UNGROUPED);
+ g_ptr_array_add(sym_def->groups, sym_group);
+ }
+ }
+
+ if (sym_def->priority > priority &&
+ (isnan(score) || !(sym_def->flags & RSPAMD_SYMBOL_FLAG_UNSCORED))) {
+ msg_debug_config("symbol %s has been already registered with "
+ "priority %ud, do not override (new priority: %ud)",
+ symbol,
+ sym_def->priority,
+ priority);
+ /* But we can still add description */
+ if (!sym_def->description && description) {
+ sym_def->description = rspamd_mempool_strdup(cfg->cfg_pool,
+ description);
+ }
+
+ /* Or nshots in case of non-default setting */
+ if (nshots != 0 && sym_def->nshots == cfg->default_max_shots) {
+ sym_def->nshots = nshots;
+ }
+
+ return FALSE;
+ }
+ else {
+
+ if (!isnan(score)) {
+ msg_debug_config("symbol %s has been already registered with "
+ "priority %ud, override it with new priority: %ud, "
+ "old score: %.2f, new score: %.2f",
+ symbol,
+ sym_def->priority,
+ priority,
+ sym_def->score,
+ score);
+
+ *sym_def->weight_ptr = score;
+ sym_def->score = score;
+ sym_def->priority = priority;
+ sym_def->flags &= ~RSPAMD_SYMBOL_FLAG_UNSCORED;
+ }
+
+ sym_def->flags = flags;
+
+ if (nshots != 0) {
+ sym_def->nshots = nshots;
+ }
+ else {
+ /* Do not reset unless we have exactly lower priority */
+ if (sym_def->priority < priority) {
+ sym_def->nshots = cfg->default_max_shots;
+ }
+ }
+
+ if (description) {
+ sym_def->description = rspamd_mempool_strdup(cfg->cfg_pool,
+ description);
+ }
+
+
+ /* We also check group information in this case */
+ if (group != nullptr && sym_def->gr != nullptr &&
+ strcmp(group, sym_def->gr->name) != 0) {
+
+ sym_group = reinterpret_cast<rspamd_symbols_group *>(g_hash_table_lookup(cfg->groups, group));
+
+ if (sym_group == nullptr) {
+ /* Create new group */
+ sym_group = rspamd_config_new_group(cfg, group);
+ }
+
+ if (!(sym_group->flags & RSPAMD_SYMBOL_GROUP_UNGROUPED)) {
+ msg_debug_config("move symbol %s from group %s to %s",
+ sym_def->name, sym_def->gr->name, group);
+ g_hash_table_remove(sym_def->gr->symbols, sym_def->name);
+ sym_def->gr = sym_group;
+ g_hash_table_insert(sym_group->symbols, sym_def->name, sym_def);
+ }
+ }
+
+ return TRUE;
+ }
+ }
+
+ /* This is called merely when we have an undefined symbol */
+ rspamd_config_new_symbol(cfg, symbol, score, description,
+ group, flags, priority, nshots);
+
+ return TRUE;
+}
+
+gboolean
+rspamd_config_add_symbol_group(struct rspamd_config *cfg,
+ const gchar *symbol,
+ const gchar *group)
+{
+ struct rspamd_symbol *sym_def;
+ struct rspamd_symbols_group *sym_group;
+ guint i;
+
+ g_assert(cfg != nullptr);
+ g_assert(symbol != nullptr);
+ g_assert(group != nullptr);
+
+ sym_def = reinterpret_cast<rspamd_symbol *>(g_hash_table_lookup(cfg->symbols, symbol));
+
+ if (sym_def != nullptr) {
+ gboolean has_group = FALSE;
+
+ PTR_ARRAY_FOREACH(sym_def->groups, i, sym_group)
+ {
+ if (g_ascii_strcasecmp(sym_group->name, group) == 0) {
+ /* Group is already here */
+ has_group = TRUE;
+ break;
+ }
+ }
+
+ if (!has_group) {
+ /* Non-empty group has a priority over non-grouped one */
+ sym_group = reinterpret_cast<rspamd_symbols_group *>(g_hash_table_lookup(cfg->groups, group));
+
+ if (sym_group == nullptr) {
+ /* Create new group */
+ sym_group = rspamd_config_new_group(cfg, group);
+ }
+
+ if (!sym_def->gr) {
+ sym_def->gr = sym_group;
+ }
+
+ g_hash_table_insert(sym_group->symbols, sym_def->name, sym_def);
+ sym_def->flags &= ~(RSPAMD_SYMBOL_FLAG_UNGROUPED);
+ g_ptr_array_add(sym_def->groups, sym_group);
+
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+gboolean
+rspamd_config_is_enabled_from_ucl(rspamd_mempool_t *pool,
+ const ucl_object_t *obj)
+{
+
+ const ucl_object_t *enabled;
+
+ enabled = ucl_object_lookup(obj, "enabled");
+
+ if (enabled) {
+ if (ucl_object_type(enabled) == UCL_BOOLEAN) {
+ return ucl_object_toboolean(enabled);
+ }
+ else if (ucl_object_type(enabled) == UCL_STRING) {
+ gint ret = rspamd_config_parse_flag(ucl_object_tostring(enabled), 0);
+
+ if (ret == 0) {
+ return FALSE;
+ }
+ else if (ret == -1) {
+
+ msg_info_pool_check("wrong value for the `enabled` key");
+ return FALSE;
+ }
+ /* Default return is TRUE here */
+ }
+ }
+
+
+ const ucl_object_t *disabled;
+
+ disabled = ucl_object_lookup(obj, "disabled");
+
+ if (disabled) {
+ if (ucl_object_type(disabled) == UCL_BOOLEAN) {
+ return !ucl_object_toboolean(disabled);
+ }
+ else if (ucl_object_type(disabled) == UCL_STRING) {
+ gint ret = rspamd_config_parse_flag(ucl_object_tostring(disabled), 0);
+
+ if (ret == 0) {
+ return TRUE;
+ }
+ else if (ret == -1) {
+
+ msg_info_pool_check("wrong value for the `disabled` key");
+ return FALSE;
+ }
+
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_config_is_module_enabled(struct rspamd_config *cfg,
+ const gchar *module_name)
+{
+ gboolean is_c = FALSE, enabled;
+ const ucl_object_t *conf;
+ GList *cur;
+ struct rspamd_symbols_group *gr;
+ lua_State *L = RSPAMD_LUA_CFG_STATE(cfg);
+ struct module_ctx *cur_ctx;
+ guint i;
+
+ PTR_ARRAY_FOREACH(cfg->c_modules, i, cur_ctx)
+ {
+ if (g_ascii_strcasecmp(cur_ctx->mod->name, module_name) == 0) {
+ is_c = TRUE;
+ break;
+ }
+ }
+
+ if (g_hash_table_lookup(cfg->explicit_modules, module_name) != nullptr) {
+ /* Always load module */
+ rspamd_plugins_table_push_elt(L, "enabled", module_name);
+
+ return TRUE;
+ }
+
+ if (is_c) {
+ gboolean found = FALSE;
+
+ cur = g_list_first(cfg->filters);
+
+ while (cur) {
+ if (strcmp((char *) cur->data, module_name) == 0) {
+ found = TRUE;
+ break;
+ }
+
+ cur = g_list_next(cur);
+ }
+
+ if (!found) {
+ msg_info_config("internal module %s is disable in `filters` line",
+ module_name);
+ rspamd_plugins_table_push_elt(L,
+ "disabled_explicitly", module_name);
+
+ return FALSE;
+ }
+ }
+
+ conf = ucl_object_lookup(cfg->cfg_ucl_obj, module_name);
+
+ if (conf == nullptr) {
+ rspamd_plugins_table_push_elt(L, "disabled_unconfigured", module_name);
+
+ msg_info_config("%s module %s is enabled but has not been configured",
+ is_c ? "internal" : "lua", module_name);
+
+ if (!is_c) {
+ msg_info_config("%s disabling unconfigured lua module", module_name);
+ return FALSE;
+ }
+ }
+ else {
+ enabled = rspamd_config_is_enabled_from_ucl(cfg->cfg_pool, conf);
+
+ if (!enabled) {
+ rspamd_plugins_table_push_elt(L,
+ "disabled_explicitly", module_name);
+
+ msg_info_config(
+ "%s module %s is disabled in the configuration",
+ is_c ? "internal" : "lua", module_name);
+ return FALSE;
+ }
+ }
+
+ /* Now we check symbols group */
+ gr = reinterpret_cast<rspamd_symbols_group *>(g_hash_table_lookup(cfg->groups, module_name));
+
+ if (gr) {
+ if (gr->flags & RSPAMD_SYMBOL_GROUP_DISABLED) {
+ rspamd_plugins_table_push_elt(L,
+ "disabled_explicitly", module_name);
+ msg_info_config("%s module %s is disabled in the configuration as "
+ "its group has been disabled",
+ is_c ? "internal" : "lua", module_name);
+
+ return FALSE;
+ }
+ }
+
+ rspamd_plugins_table_push_elt(L, "enabled", module_name);
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_config_action_from_ucl(struct rspamd_config *cfg,
+ struct rspamd_action *act,
+ const ucl_object_t *obj,
+ guint priority)
+{
+ auto threshold = NAN;
+ int flags = 0;
+
+ auto obj_type = ucl_object_type(obj);
+
+ if (obj_type == UCL_OBJECT) {
+ obj_type = ucl_object_type(obj);
+
+ const auto *elt = ucl_object_lookup_any(obj, "score", "threshold", nullptr);
+
+ if (elt) {
+ threshold = ucl_object_todouble(elt);
+ }
+
+ elt = ucl_object_lookup(obj, "flags");
+
+ if (elt && ucl_object_type(elt) == UCL_ARRAY) {
+ const ucl_object_t *cur;
+ ucl_object_iter_t it = nullptr;
+
+ while ((cur = ucl_object_iterate(elt, &it, true)) != nullptr) {
+ if (ucl_object_type(cur) == UCL_STRING) {
+ const gchar *fl_str = ucl_object_tostring(cur);
+
+ if (g_ascii_strcasecmp(fl_str, "no_threshold") == 0) {
+ flags |= RSPAMD_ACTION_NO_THRESHOLD;
+ }
+ else if (g_ascii_strcasecmp(fl_str, "threshold_only") == 0) {
+ flags |= RSPAMD_ACTION_THRESHOLD_ONLY;
+ }
+ else if (g_ascii_strcasecmp(fl_str, "ham") == 0) {
+ flags |= RSPAMD_ACTION_HAM;
+ }
+ else {
+ msg_warn_config("unknown action flag: %s", fl_str);
+ }
+ }
+ }
+ }
+
+ elt = ucl_object_lookup(obj, "milter");
+
+ if (elt) {
+ const gchar *milter_action = ucl_object_tostring(elt);
+
+ if (strcmp(milter_action, "discard") == 0) {
+ flags |= RSPAMD_ACTION_MILTER;
+ act->action_type = METRIC_ACTION_DISCARD;
+ }
+ else if (strcmp(milter_action, "quarantine") == 0) {
+ flags |= RSPAMD_ACTION_MILTER;
+ act->action_type = METRIC_ACTION_QUARANTINE;
+ }
+ else {
+ msg_warn_config("unknown milter action: %s", milter_action);
+ }
+ }
+ }
+ else if (obj_type == UCL_FLOAT || obj_type == UCL_INT) {
+ threshold = ucl_object_todouble(obj);
+ }
+
+ /* TODO: add lua references support */
+
+ if (isnan(threshold) && !(flags & RSPAMD_ACTION_NO_THRESHOLD)) {
+ msg_err_config("action %s has no threshold being set and it is not"
+ " a no threshold action",
+ act->name);
+
+ return FALSE;
+ }
+
+ act->threshold = threshold;
+ act->flags = flags;
+
+ enum rspamd_action_type std_act;
+
+ if (!(flags & RSPAMD_ACTION_MILTER)) {
+ if (rspamd_action_from_str(act->name, &std_act)) {
+ act->action_type = std_act;
+ }
+ else {
+ act->action_type = METRIC_ACTION_CUSTOM;
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_config_set_action_score(struct rspamd_config *cfg,
+ const gchar *action_name,
+ const ucl_object_t *obj)
+{
+ enum rspamd_action_type std_act;
+ const ucl_object_t *elt;
+ guint priority = ucl_object_get_priority(obj), obj_type;
+
+ g_assert(cfg != nullptr);
+ g_assert(action_name != nullptr);
+
+ obj_type = ucl_object_type(obj);
+
+ if (obj_type == UCL_OBJECT) {
+ elt = ucl_object_lookup(obj, "priority");
+
+ if (elt) {
+ priority = ucl_object_toint(elt);
+ }
+ }
+
+ /* Here are dragons:
+ * We have `canonical` name for actions, such as `soft reject` and
+ * configuration names for actions (used to be more convenient), such
+ * as `soft_reject`. Unfortunately, we must have heuristic for this
+ * variance of names.
+ */
+
+ if (rspamd_action_from_str(action_name, &std_act)) {
+ action_name = rspamd_action_to_str(std_act);
+ }
+
+ auto actions = RSPAMD_CFG_ACTIONS(cfg);
+ auto existing_act_it = actions->actions_by_name.find(action_name);
+
+ if (existing_act_it != actions->actions_by_name.end()) {
+ auto *act = existing_act_it->second.get();
+ /* Existing element */
+ if (act->priority <= priority) {
+ /* We can replace data */
+ auto old_pri = act->priority;
+ auto old_thr = act->threshold;
+
+ if (rspamd_config_action_from_ucl(cfg, act, obj, priority)) {
+ msg_info_config("action %s has been already registered with "
+ "priority %ud, override it with new priority: %ud, "
+ "old threshold: %.2f, new threshold: %.2f",
+ action_name,
+ old_pri,
+ priority,
+ old_thr,
+ act->threshold);
+ actions->sort();
+ }
+ else {
+ return FALSE;
+ }
+ }
+ else {
+ msg_info_config("action %s has been already registered with "
+ "priority %ud, do not override (new priority: %ud)",
+ action_name,
+ act->priority,
+ priority);
+ }
+ }
+ else {
+ /* Add new element */
+ auto act = std::make_shared<rspamd_action>();
+ act->name = rspamd_mempool_strdup(cfg->cfg_pool, action_name);
+
+ if (rspamd_config_action_from_ucl(cfg, act.get(), obj, priority)) {
+ actions->add_action(std::move(act));
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_config_maybe_disable_action(struct rspamd_config *cfg,
+ const gchar *action_name,
+ guint priority)
+{
+ auto actions = RSPAMD_CFG_ACTIONS(cfg);
+ auto maybe_act = rspamd::find_map(actions->actions_by_name, action_name);
+
+ if (maybe_act) {
+ auto *act = maybe_act.value().get().get();
+ if (priority >= act->priority) {
+ msg_info_config("disable action %s; old priority: %ud, new priority: %ud",
+ action_name,
+ act->priority,
+ priority);
+
+ act->threshold = NAN;
+ act->priority = priority;
+ act->flags |= RSPAMD_ACTION_NO_THRESHOLD;
+
+ return TRUE;
+ }
+ else {
+ msg_info_config("action %s has been already registered with "
+ "priority %ud, cannot disable it with new priority: %ud",
+ action_name,
+ act->priority,
+ priority);
+ }
+ }
+
+ return FALSE;
+}
+
+struct rspamd_action *
+rspamd_config_get_action(struct rspamd_config *cfg, const gchar *name)
+{
+ auto actions = RSPAMD_CFG_ACTIONS(cfg);
+ auto maybe_act = rspamd::find_map(actions->actions_by_name, name);
+
+ if (maybe_act) {
+ return maybe_act.value().get().get();
+ }
+
+ return nullptr;
+}
+
+struct rspamd_action *
+rspamd_config_get_action_by_type(struct rspamd_config *cfg,
+ enum rspamd_action_type type)
+{
+ for (const auto &act: RSPAMD_CFG_ACTIONS(cfg)->actions) {
+ if (act->action_type == type) {
+ return act.get();
+ }
+ }
+
+ return nullptr;
+}
+
+void rspamd_config_actions_foreach(struct rspamd_config *cfg,
+ void (*func)(struct rspamd_action *act, void *d),
+ void *data)
+{
+ for (const auto &act: RSPAMD_CFG_ACTIONS(cfg)->actions) {
+ func(act.get(), data);
+ }
+}
+
+void rspamd_config_actions_foreach_enumerate(struct rspamd_config *cfg,
+ void (*func)(int idx, struct rspamd_action *act, void *d),
+ void *data)
+{
+ for (const auto &[idx, act]: rspamd::enumerate(RSPAMD_CFG_ACTIONS(cfg)->actions)) {
+ func(idx, act.get(), data);
+ }
+}
+
+gsize rspamd_config_actions_size(struct rspamd_config *cfg)
+{
+ return RSPAMD_CFG_ACTIONS(cfg)->actions.size();
+}
+
+gboolean
+rspamd_config_radix_from_ucl(struct rspamd_config *cfg, const ucl_object_t *obj, const gchar *description,
+ struct rspamd_radix_map_helper **target, GError **err,
+ struct rspamd_worker *worker, const gchar *map_name)
+{
+ ucl_type_t type;
+ ucl_object_iter_t it = nullptr;
+ const ucl_object_t *cur, *cur_elt;
+ const gchar *str;
+
+ /* Cleanup */
+ *target = nullptr;
+
+ LL_FOREACH(obj, cur_elt)
+ {
+ type = ucl_object_type(cur_elt);
+
+ switch (type) {
+ case UCL_STRING:
+ /* Either map or a list of IPs */
+ str = ucl_object_tostring(cur_elt);
+
+ if (rspamd_map_is_map(str)) {
+ if (rspamd_map_add_from_ucl(cfg, cur_elt,
+ description,
+ rspamd_radix_read,
+ rspamd_radix_fin,
+ rspamd_radix_dtor,
+ (void **) target,
+ worker, RSPAMD_MAP_DEFAULT) == nullptr) {
+ g_set_error(err,
+ g_quark_from_static_string("rspamd-config"),
+ EINVAL, "bad map definition %s for %s", str,
+ ucl_object_key(obj));
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+ else {
+ /* Just a list */
+ if (!*target) {
+ *target = rspamd_map_helper_new_radix(
+ rspamd_map_add_fake(cfg, description, map_name));
+ }
+
+ rspamd_map_helper_insert_radix_resolve(*target, str, "");
+ }
+ break;
+ case UCL_OBJECT:
+ /* Should be a map description */
+ if (rspamd_map_add_from_ucl(cfg, cur_elt,
+ description,
+ rspamd_radix_read,
+ rspamd_radix_fin,
+ rspamd_radix_dtor,
+ (void **) target,
+ worker, RSPAMD_MAP_DEFAULT) == nullptr) {
+ g_set_error(err,
+ g_quark_from_static_string("rspamd-config"),
+ EINVAL, "bad map object for %s", ucl_object_key(obj));
+ return FALSE;
+ }
+
+ return TRUE;
+ break;
+ case UCL_ARRAY:
+ /* List of IP addresses */
+ it = ucl_object_iterate_new(cur_elt);
+
+ while ((cur = ucl_object_iterate_safe(it, true)) != nullptr) {
+
+
+ if (ucl_object_type(cur) == UCL_STRING) {
+ str = ucl_object_tostring(cur);
+ if (!*target) {
+ *target = rspamd_map_helper_new_radix(
+ rspamd_map_add_fake(cfg, description, map_name));
+ }
+
+ rspamd_map_helper_insert_radix_resolve(*target, str, "");
+ }
+ else {
+ g_set_error(err,
+ g_quark_from_static_string("rspamd-config"),
+ EINVAL, "bad element inside array object for %s: expected string, got: %s",
+ ucl_object_key(obj), ucl_object_type_to_string(ucl_object_type(cur)));
+ ucl_object_iterate_free(it);
+ return FALSE;
+ }
+ }
+
+ ucl_object_iterate_free(it);
+ break;
+ default:
+ g_set_error(err, g_quark_from_static_string("rspamd-config"),
+ EINVAL, "bad map type %s for %s",
+ ucl_object_type_to_string(type),
+ ucl_object_key(obj));
+ return FALSE;
+ }
+ }
+
+ /* Destroy on cfg cleanup */
+ rspamd_mempool_add_destructor(cfg->cfg_pool,
+ (rspamd_mempool_destruct_t) rspamd_map_helper_destroy_radix,
+ *target);
+
+ return TRUE;
+}
+
+constexpr const auto action_types = frozen::make_unordered_map<frozen::string, enum rspamd_action_type>({
+ {"reject", METRIC_ACTION_REJECT},
+ {"greylist", METRIC_ACTION_GREYLIST},
+ {"add header", METRIC_ACTION_ADD_HEADER},
+ {"add_header", METRIC_ACTION_ADD_HEADER},
+ {"rewrite subject", METRIC_ACTION_REWRITE_SUBJECT},
+ {"rewrite_subject", METRIC_ACTION_REWRITE_SUBJECT},
+ {"soft reject", METRIC_ACTION_SOFT_REJECT},
+ {"soft_reject", METRIC_ACTION_SOFT_REJECT},
+ {"no action", METRIC_ACTION_NOACTION},
+ {"no_action", METRIC_ACTION_NOACTION},
+ {"accept", METRIC_ACTION_NOACTION},
+ {"quarantine", METRIC_ACTION_QUARANTINE},
+ {"discard", METRIC_ACTION_DISCARD},
+
+});
+
+gboolean
+rspamd_action_from_str(const gchar *data, enum rspamd_action_type *result)
+{
+ auto maybe_action = rspamd::find_map(action_types, std::string_view{data});
+
+ if (maybe_action) {
+ *result = maybe_action.value().get();
+ return true;
+ }
+ else {
+ return false;
+ }
+}
+
+const gchar *
+rspamd_action_to_str(enum rspamd_action_type action)
+{
+ switch (action) {
+ case METRIC_ACTION_REJECT:
+ return "reject";
+ case METRIC_ACTION_SOFT_REJECT:
+ return "soft reject";
+ case METRIC_ACTION_REWRITE_SUBJECT:
+ return "rewrite subject";
+ case METRIC_ACTION_ADD_HEADER:
+ return "add header";
+ case METRIC_ACTION_GREYLIST:
+ return "greylist";
+ case METRIC_ACTION_NOACTION:
+ return "no action";
+ case METRIC_ACTION_MAX:
+ return "invalid max action";
+ case METRIC_ACTION_CUSTOM:
+ return "custom";
+ case METRIC_ACTION_DISCARD:
+ return "discard";
+ case METRIC_ACTION_QUARANTINE:
+ return "quarantine";
+ }
+
+ return "unknown action";
+}
+
+const gchar *
+rspamd_action_to_str_alt(enum rspamd_action_type action)
+{
+ switch (action) {
+ case METRIC_ACTION_REJECT:
+ return "reject";
+ case METRIC_ACTION_SOFT_REJECT:
+ return "soft_reject";
+ case METRIC_ACTION_REWRITE_SUBJECT:
+ return "rewrite_subject";
+ case METRIC_ACTION_ADD_HEADER:
+ return "add_header";
+ case METRIC_ACTION_GREYLIST:
+ return "greylist";
+ case METRIC_ACTION_NOACTION:
+ return "no action";
+ case METRIC_ACTION_MAX:
+ return "invalid max action";
+ case METRIC_ACTION_CUSTOM:
+ return "custom";
+ case METRIC_ACTION_DISCARD:
+ return "discard";
+ case METRIC_ACTION_QUARANTINE:
+ return "quarantine";
+ }
+
+ return "unknown action";
+}
+
+static void
+rspamd_config_settings_elt_dtor(struct rspamd_config_settings_elt *e)
+{
+ if (e->symbols_enabled) {
+ ucl_object_unref(e->symbols_enabled);
+ }
+ if (e->symbols_disabled) {
+ ucl_object_unref(e->symbols_disabled);
+ }
+}
+
+guint32
+rspamd_config_name_to_id(const gchar *name, gsize namelen)
+{
+ guint64 h;
+
+ h = rspamd_cryptobox_fast_hash_specific(RSPAMD_CRYPTOBOX_XXHASH64,
+ name, namelen, 0x0);
+ /* Take the lower part of hash as LE number */
+ return ((guint32) GUINT64_TO_LE(h));
+}
+
+struct rspamd_config_settings_elt *
+rspamd_config_find_settings_id_ref(struct rspamd_config *cfg,
+ guint32 id)
+{
+ struct rspamd_config_settings_elt *cur;
+
+ DL_FOREACH(cfg->setting_ids, cur)
+ {
+ if (cur->id == id) {
+ REF_RETAIN(cur);
+ return cur;
+ }
+ }
+
+ return nullptr;
+}
+
+struct rspamd_config_settings_elt *rspamd_config_find_settings_name_ref(
+ struct rspamd_config *cfg,
+ const gchar *name, gsize namelen)
+{
+ guint32 id;
+
+ id = rspamd_config_name_to_id(name, namelen);
+
+ return rspamd_config_find_settings_id_ref(cfg, id);
+}
+
+void rspamd_config_register_settings_id(struct rspamd_config *cfg,
+ const gchar *name,
+ ucl_object_t *symbols_enabled,
+ ucl_object_t *symbols_disabled,
+ enum rspamd_config_settings_policy policy)
+{
+ struct rspamd_config_settings_elt *elt;
+ guint32 id;
+
+ id = rspamd_config_name_to_id(name, strlen(name));
+ elt = rspamd_config_find_settings_id_ref(cfg, id);
+
+ if (elt) {
+ /* Need to replace */
+ struct rspamd_config_settings_elt *nelt;
+
+ DL_DELETE(cfg->setting_ids, elt);
+
+ nelt = rspamd_mempool_alloc0_type(cfg->cfg_pool, struct rspamd_config_settings_elt);
+
+ nelt->id = id;
+ nelt->name = rspamd_mempool_strdup(cfg->cfg_pool, name);
+
+ if (symbols_enabled) {
+ nelt->symbols_enabled = ucl_object_ref(symbols_enabled);
+ }
+
+ if (symbols_disabled) {
+ nelt->symbols_disabled = ucl_object_ref(symbols_disabled);
+ }
+
+ nelt->policy = policy;
+
+ REF_INIT_RETAIN(nelt, rspamd_config_settings_elt_dtor);
+ msg_warn_config("replace settings id %ud (%s)", id, name);
+ rspamd_symcache_process_settings_elt(cfg->cache, elt);
+ DL_APPEND(cfg->setting_ids, nelt);
+
+ /*
+ * Need to unref old element twice as there are two reference holders:
+ * 1. Config structure as we call REF_INIT_RETAIN
+ * 2. rspamd_config_find_settings_id_ref also increases refcount
+ */
+ REF_RELEASE(elt);
+ REF_RELEASE(elt);
+ }
+ else {
+ elt = rspamd_mempool_alloc0_type(cfg->cfg_pool, struct rspamd_config_settings_elt);
+
+ elt->id = id;
+ elt->name = rspamd_mempool_strdup(cfg->cfg_pool, name);
+
+ if (symbols_enabled) {
+ elt->symbols_enabled = ucl_object_ref(symbols_enabled);
+ }
+
+ if (symbols_disabled) {
+ elt->symbols_disabled = ucl_object_ref(symbols_disabled);
+ }
+
+ elt->policy = policy;
+
+ msg_info_config("register new settings id %ud (%s)", id, name);
+ REF_INIT_RETAIN(elt, rspamd_config_settings_elt_dtor);
+ rspamd_symcache_process_settings_elt(cfg->cache, elt);
+ DL_APPEND(cfg->setting_ids, elt);
+ }
+}
+
+int rspamd_config_ev_backend_get(struct rspamd_config *cfg)
+{
+#define AUTO_BACKEND (ev_supported_backends() & ~EVBACKEND_IOURING)
+ if (cfg == nullptr || cfg->events_backend == nullptr) {
+ return AUTO_BACKEND;
+ }
+
+ if (strcmp(cfg->events_backend, "auto") == 0) {
+ return AUTO_BACKEND;
+ }
+ else if (strcmp(cfg->events_backend, "epoll") == 0) {
+ if (ev_supported_backends() & EVBACKEND_EPOLL) {
+ return EVBACKEND_EPOLL;
+ }
+ else {
+ msg_warn_config("unsupported events_backend: %s; defaulting to auto",
+ cfg->events_backend);
+ return AUTO_BACKEND;
+ }
+ }
+ else if (strcmp(cfg->events_backend, "iouring") == 0) {
+ if (ev_supported_backends() & EVBACKEND_IOURING) {
+ return EVBACKEND_IOURING;
+ }
+ else {
+ msg_warn_config("unsupported events_backend: %s; defaulting to auto",
+ cfg->events_backend);
+ return AUTO_BACKEND;
+ }
+ }
+ else if (strcmp(cfg->events_backend, "kqueue") == 0) {
+ if (ev_supported_backends() & EVBACKEND_KQUEUE) {
+ return EVBACKEND_KQUEUE;
+ }
+ else {
+ msg_warn_config("unsupported events_backend: %s; defaulting to auto",
+ cfg->events_backend);
+ return AUTO_BACKEND;
+ }
+ }
+ else if (strcmp(cfg->events_backend, "poll") == 0) {
+ return EVBACKEND_POLL;
+ }
+ else if (strcmp(cfg->events_backend, "select") == 0) {
+ return EVBACKEND_SELECT;
+ }
+ else {
+ msg_warn_config("unknown events_backend: %s; defaulting to auto",
+ cfg->events_backend);
+ }
+
+ return AUTO_BACKEND;
+}
+
+const gchar *
+rspamd_config_ev_backend_to_string(int ev_backend, gboolean *effective)
+{
+#define SET_EFFECTIVE(b) \
+ do { \
+ if ((effective) != nullptr) *(effective) = b; \
+ } while (0)
+
+ if ((ev_backend & EVBACKEND_ALL) == EVBACKEND_ALL) {
+ SET_EFFECTIVE(TRUE);
+ return "auto";
+ }
+
+ if (ev_backend & EVBACKEND_IOURING) {
+ SET_EFFECTIVE(TRUE);
+ return "epoll+io_uring";
+ }
+ if (ev_backend & EVBACKEND_LINUXAIO) {
+ SET_EFFECTIVE(TRUE);
+ return "epoll+aio";
+ }
+ if (ev_backend & EVBACKEND_IOURING) {
+ SET_EFFECTIVE(TRUE);
+ return "epoll+io_uring";
+ }
+ if (ev_backend & EVBACKEND_LINUXAIO) {
+ SET_EFFECTIVE(TRUE);
+ return "epoll+aio";
+ }
+ if (ev_backend & EVBACKEND_EPOLL) {
+ SET_EFFECTIVE(TRUE);
+ return "epoll";
+ }
+ if (ev_backend & EVBACKEND_KQUEUE) {
+ SET_EFFECTIVE(TRUE);
+ return "kqueue";
+ }
+ if (ev_backend & EVBACKEND_POLL) {
+ SET_EFFECTIVE(FALSE);
+ return "poll";
+ }
+ if (ev_backend & EVBACKEND_SELECT) {
+ SET_EFFECTIVE(FALSE);
+ return "select";
+ }
+
+ SET_EFFECTIVE(FALSE);
+ return "unknown";
+#undef SET_EFFECTIVE
+}
+
+struct rspamd_external_libs_ctx *
+rspamd_init_libs(void)
+{
+ struct rlimit rlim;
+ struct ottery_config *ottery_cfg;
+
+ auto *ctx = g_new0(struct rspamd_external_libs_ctx, 1);
+ ctx->crypto_ctx = rspamd_cryptobox_init();
+ ottery_cfg = (struct ottery_config *) g_malloc0(ottery_get_sizeof_config());
+ ottery_config_init(ottery_cfg);
+ ctx->ottery_cfg = ottery_cfg;
+
+ rspamd_openssl_maybe_init();
+
+ /* Check if we have rdrand */
+ if ((ctx->crypto_ctx->cpu_config & CPUID_RDRAND) == 0) {
+ ottery_config_disable_entropy_sources(ottery_cfg,
+ OTTERY_ENTROPY_SRC_RDRAND);
+ }
+
+ g_assert(ottery_init(ottery_cfg) == 0);
+#if OPENSSL_VERSION_NUMBER >= 0x1000104fL && OPENSSL_VERSION_NUMBER < 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
+ RAND_set_rand_engine(nullptr);
+#endif
+
+ /* Configure utf8 library */
+ guint utf8_flags = 0;
+
+ if ((ctx->crypto_ctx->cpu_config & CPUID_SSE41)) {
+ utf8_flags |= RSPAMD_FAST_UTF8_FLAG_SSE41;
+ }
+ if ((ctx->crypto_ctx->cpu_config & CPUID_AVX2)) {
+ utf8_flags |= RSPAMD_FAST_UTF8_FLAG_AVX2;
+ }
+
+ rspamd_fast_utf8_library_init(utf8_flags);
+
+#ifdef HAVE_LOCALE_H
+ if (getenv("LANG") == nullptr) {
+ setlocale(LC_ALL, "C");
+ setlocale(LC_CTYPE, "C");
+ setlocale(LC_MESSAGES, "C");
+ setlocale(LC_TIME, "C");
+ }
+ else {
+ /* Just set the default locale */
+ setlocale(LC_ALL, "");
+ /* But for some issues we still want C locale */
+ setlocale(LC_NUMERIC, "C");
+ }
+#endif
+
+ ctx->ssl_ctx = rspamd_init_ssl_ctx();
+ ctx->ssl_ctx_noverify = rspamd_init_ssl_ctx_noverify();
+ rspamd_random_seed_fast();
+
+ /* Set stack size for pcre */
+ getrlimit(RLIMIT_STACK, &rlim);
+ rlim.rlim_cur = 100 * 1024 * 1024;
+ rlim.rlim_max = rlim.rlim_cur;
+ setrlimit(RLIMIT_STACK, &rlim);
+
+ ctx->local_addrs = rspamd_inet_library_init();
+ REF_INIT_RETAIN(ctx, rspamd_deinit_libs);
+
+ return ctx;
+}
+
+static struct zstd_dictionary *
+rspamd_open_zstd_dictionary(const char *path)
+{
+ struct zstd_dictionary *dict;
+
+ dict = g_new0(zstd_dictionary, 1);
+ dict->dict = rspamd_file_xmap(path, PROT_READ, &dict->size, TRUE);
+
+ if (dict->dict == nullptr) {
+ g_free(dict);
+
+ return nullptr;
+ }
+
+ dict->id = -1;
+
+ if (dict->id == 0) {
+ g_free(dict);
+
+ return nullptr;
+ }
+
+ return dict;
+}
+
+static void
+rspamd_free_zstd_dictionary(struct zstd_dictionary *dict)
+{
+ if (dict) {
+ munmap(dict->dict, dict->size);
+ g_free(dict);
+ }
+}
+
+#ifdef HAVE_OPENBLAS_SET_NUM_THREADS
+extern "C" void openblas_set_num_threads(int num_threads);
+#endif
+#ifdef HAVE_BLI_THREAD_SET_NUM_THREADS
+extern "C" void bli_thread_set_num_threads(int num_threads);
+#endif
+
+gboolean
+rspamd_config_libs(struct rspamd_external_libs_ctx *ctx,
+ struct rspamd_config *cfg)
+{
+ size_t r;
+ gboolean ret = TRUE;
+
+ g_assert(cfg != nullptr);
+
+ if (ctx != nullptr) {
+ if (cfg->local_addrs) {
+ GError *err = nullptr;
+ ret = rspamd_config_radix_from_ucl(cfg, cfg->local_addrs,
+ "Local addresses",
+ (struct rspamd_radix_map_helper **) ctx->local_addrs,
+ &err,
+ nullptr, "local addresses");
+
+ if (!ret) {
+ msg_err_config("cannot load local addresses: %e", err);
+ g_error_free(err);
+
+ return ret;
+ }
+ }
+
+ rspamd_free_zstd_dictionary(ctx->in_dict);
+ rspamd_free_zstd_dictionary(ctx->out_dict);
+
+ if (ctx->out_zstream) {
+ ZSTD_freeCStream((ZSTD_CCtx *) ctx->out_zstream);
+ ctx->out_zstream = nullptr;
+ }
+
+ if (ctx->in_zstream) {
+ ZSTD_freeDStream((ZSTD_DCtx *) ctx->in_zstream);
+ ctx->in_zstream = nullptr;
+ }
+
+ if (cfg->zstd_input_dictionary) {
+ ctx->in_dict = rspamd_open_zstd_dictionary(
+ cfg->zstd_input_dictionary);
+
+ if (ctx->in_dict == nullptr) {
+ msg_err_config("cannot open zstd dictionary in %s",
+ cfg->zstd_input_dictionary);
+ }
+ }
+ if (cfg->zstd_output_dictionary) {
+ ctx->out_dict = rspamd_open_zstd_dictionary(
+ cfg->zstd_output_dictionary);
+
+ if (ctx->out_dict == nullptr) {
+ msg_err_config("cannot open zstd dictionary in %s",
+ cfg->zstd_output_dictionary);
+ }
+ }
+
+ if (cfg->fips_mode) {
+#ifdef HAVE_FIPS_MODE
+ int mode = FIPS_mode();
+ unsigned long err = (unsigned long) -1;
+
+ /* Toggle FIPS mode */
+ if (mode == 0) {
+#if defined(OPENSSL_VERSION_MAJOR) && (OPENSSL_VERSION_MAJOR >= 3)
+ if (EVP_set_default_properties(nullptr, "fips=yes") != 1) {
+#else
+ if (FIPS_mode_set(1) != 1) {
+#endif
+ err = ERR_get_error();
+ }
+ }
+ else {
+ msg_info_config("OpenSSL FIPS mode is already enabled");
+ }
+
+ if (err != (unsigned long) -1) {
+#if defined(OPENSSL_VERSION_MAJOR) && (OPENSSL_VERSION_MAJOR >= 3)
+ msg_err_config("EVP_set_default_properties failed: %s",
+#else
+ msg_err_config("FIPS_mode_set failed: %s",
+#endif
+ ERR_error_string(err, nullptr));
+ ret = FALSE;
+ }
+ else {
+ msg_info_config("OpenSSL FIPS mode is enabled");
+ }
+#else
+ msg_warn_config("SSL FIPS mode is enabled but not supported by OpenSSL library!");
+#endif
+ }
+
+ rspamd_ssl_ctx_config(cfg, ctx->ssl_ctx);
+ rspamd_ssl_ctx_config(cfg, ctx->ssl_ctx_noverify);
+
+ /* Init decompression */
+ ctx->in_zstream = ZSTD_createDStream();
+ r = ZSTD_initDStream((ZSTD_DCtx *) ctx->in_zstream);
+
+ if (ZSTD_isError(r)) {
+ msg_err("cannot init decompression stream: %s",
+ ZSTD_getErrorName(r));
+ ZSTD_freeDStream((ZSTD_DCtx *) ctx->in_zstream);
+ ctx->in_zstream = nullptr;
+ }
+
+ /* Init compression */
+ ctx->out_zstream = ZSTD_createCStream();
+ r = ZSTD_initCStream((ZSTD_CCtx *) ctx->out_zstream, 1);
+
+ if (ZSTD_isError(r)) {
+ msg_err("cannot init compression stream: %s",
+ ZSTD_getErrorName(r));
+ ZSTD_freeCStream((ZSTD_CCtx *) ctx->out_zstream);
+ ctx->out_zstream = nullptr;
+ }
+#ifdef HAVE_OPENBLAS_SET_NUM_THREADS
+ openblas_set_num_threads(cfg->max_blas_threads);
+#endif
+#ifdef HAVE_BLI_THREAD_SET_NUM_THREADS
+ bli_thread_set_num_threads(cfg->max_blas_threads);
+#endif
+ }
+
+ return ret;
+}
+
+gboolean
+rspamd_libs_reset_decompression(struct rspamd_external_libs_ctx *ctx)
+{
+ gsize r;
+
+ if (ctx->in_zstream == nullptr) {
+ return FALSE;
+ }
+ else {
+ r = ZSTD_DCtx_reset((ZSTD_DCtx *) ctx->in_zstream, ZSTD_reset_session_only);
+
+ if (ZSTD_isError(r)) {
+ msg_err("cannot init decompression stream: %s",
+ ZSTD_getErrorName(r));
+ ZSTD_freeDStream((ZSTD_DCtx *) ctx->in_zstream);
+ ctx->in_zstream = nullptr;
+
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_libs_reset_compression(struct rspamd_external_libs_ctx *ctx)
+{
+ gsize r;
+
+ if (ctx->out_zstream == nullptr) {
+ return FALSE;
+ }
+ else {
+ /* Dictionary will be reused automatically if specified */
+ r = ZSTD_CCtx_reset((ZSTD_CCtx *) ctx->out_zstream, ZSTD_reset_session_only);
+ if (!ZSTD_isError(r)) {
+ r = ZSTD_CCtx_setPledgedSrcSize((ZSTD_CCtx *) ctx->out_zstream, ZSTD_CONTENTSIZE_UNKNOWN);
+ }
+
+ if (ZSTD_isError(r)) {
+ msg_err("cannot init compression stream: %s",
+ ZSTD_getErrorName(r));
+ ZSTD_freeCStream((ZSTD_CCtx *) ctx->out_zstream);
+ ctx->out_zstream = nullptr;
+
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+void rspamd_deinit_libs(struct rspamd_external_libs_ctx *ctx)
+{
+ if (ctx != nullptr) {
+ g_free(ctx->ottery_cfg);
+
+#ifdef HAVE_OPENSSL
+ EVP_cleanup();
+ ERR_free_strings();
+ rspamd_ssl_ctx_free(ctx->ssl_ctx);
+ rspamd_ssl_ctx_free(ctx->ssl_ctx_noverify);
+#endif
+ rspamd_inet_library_destroy();
+ rspamd_free_zstd_dictionary(ctx->in_dict);
+ rspamd_free_zstd_dictionary(ctx->out_dict);
+
+ if (ctx->out_zstream) {
+ ZSTD_freeCStream((ZSTD_CCtx *) ctx->out_zstream);
+ }
+
+ if (ctx->in_zstream) {
+ ZSTD_freeDStream((ZSTD_DCtx *) ctx->in_zstream);
+ }
+
+ rspamd_cryptobox_deinit(ctx->crypto_ctx);
+
+ g_free(ctx);
+ }
+}
+
+gboolean
+rspamd_ip_is_local_cfg(struct rspamd_config *cfg,
+ const rspamd_inet_addr_t *addr)
+{
+ struct rspamd_radix_map_helper *local_addrs = nullptr;
+
+ if (cfg && cfg->libs_ctx) {
+ local_addrs = *(struct rspamd_radix_map_helper **) cfg->libs_ctx->local_addrs;
+ }
+
+ if (rspamd_inet_address_is_local(addr)) {
+ return TRUE;
+ }
+
+ if (local_addrs) {
+ if (rspamd_match_radix_map_addr(local_addrs, addr) != nullptr) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
diff --git a/src/libserver/composites/composites.cxx b/src/libserver/composites/composites.cxx
new file mode 100644
index 0000000..aa231a3
--- /dev/null
+++ b/src/libserver/composites/composites.cxx
@@ -0,0 +1,989 @@
+/*-
+ * Copyright 2021 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 "logger.h"
+#include "expression.h"
+#include "task.h"
+#include "utlist.h"
+#include "scan_result.h"
+#include "composites.h"
+
+#include <cmath>
+#include <vector>
+#include <variant>
+#include "libutil/cxx/util.hxx"
+#include "contrib/ankerl/unordered_dense.h"
+
+#include "composites_internal.hxx"
+
+#define msg_err_composites(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "composites", task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_composites(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ "composites", task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_composites(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ "composites", task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+#define msg_debug_composites(...) rspamd_conditional_debug_fast(NULL, task->from_addr, \
+ rspamd_composites_log_id, "composites", task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+INIT_LOG_MODULE(composites)
+
+
+namespace rspamd::composites {
+static rspamd_expression_atom_t *rspamd_composite_expr_parse(const gchar *line, gsize len,
+ rspamd_mempool_t *pool,
+ gpointer ud, GError **err);
+static gdouble rspamd_composite_expr_process(void *ud, rspamd_expression_atom_t *atom);
+static gint rspamd_composite_expr_priority(rspamd_expression_atom_t *atom);
+static void rspamd_composite_expr_destroy(rspamd_expression_atom_t *atom);
+static void composites_foreach_callback(gpointer key, gpointer value, void *data);
+
+const struct rspamd_atom_subr composite_expr_subr = {
+ .parse = rspamd::composites::rspamd_composite_expr_parse,
+ .process = rspamd::composites::rspamd_composite_expr_process,
+ .priority = rspamd::composites::rspamd_composite_expr_priority,
+ .destroy = rspamd::composites::rspamd_composite_expr_destroy};
+}// namespace rspamd::composites
+
+namespace rspamd::composites {
+
+static constexpr const double epsilon = 0.00001;
+
+struct symbol_remove_data {
+ const char *sym;
+ struct rspamd_composite *comp;
+ GNode *parent;
+ std::uint8_t action;
+};
+
+struct composites_data {
+ struct rspamd_task *task;
+ struct rspamd_composite *composite;
+ struct rspamd_scan_result *metric_res;
+ ankerl::unordered_dense::map<std::string_view,
+ std::vector<symbol_remove_data>>
+ symbols_to_remove;
+ std::vector<bool> checked;
+
+ explicit composites_data(struct rspamd_task *task, struct rspamd_scan_result *mres)
+ : task(task), composite(nullptr), metric_res(mres)
+ {
+ checked.resize(rspamd_composites_manager_nelts(task->cfg->composites_manager) * 2,
+ false);
+ }
+};
+
+struct rspamd_composite_option_match {
+ rspamd_regexp_t *re;
+ std::string match;
+
+ explicit rspamd_composite_option_match(const char *start, std::size_t len) noexcept
+ : re(nullptr), match(start, len)
+ {
+ }
+
+ explicit rspamd_composite_option_match(rspamd_regexp_t *re) noexcept
+ : re(rspamd_regexp_ref(re))
+ {
+ }
+
+ rspamd_composite_option_match(const rspamd_composite_option_match &other) noexcept
+ {
+ if (other.re) {
+ re = rspamd_regexp_ref(other.re);
+ }
+ else {
+ match = other.match;
+ re = nullptr;
+ }
+ }
+ rspamd_composite_option_match &operator=(const rspamd_composite_option_match &other) noexcept
+ {
+ if (other.re) {
+ if (re) {
+ rspamd_regexp_unref(re);
+ }
+ re = rspamd_regexp_ref(other.re);
+ }
+ else {
+ if (re) {
+ rspamd_regexp_unref(re);
+ }
+ re = nullptr;
+ match = other.match;
+ }
+
+ return *this;
+ }
+
+ rspamd_composite_option_match(rspamd_composite_option_match &&other) noexcept
+ {
+ if (other.re) {
+ re = other.re;
+ other.re = nullptr;
+ }
+ else {
+ re = nullptr;
+ match = std::move(other.match);
+ }
+ }
+ rspamd_composite_option_match &operator=(rspamd_composite_option_match &&other) noexcept
+ {
+ if (other.re) {
+ if (re) {
+ rspamd_regexp_unref(re);
+ }
+ re = other.re;
+ other.re = nullptr;
+ }
+ else {
+ if (re) {
+ rspamd_regexp_unref(re);
+ }
+ re = nullptr;
+ match = std::move(other.match);
+ }
+
+ return *this;
+ }
+
+ ~rspamd_composite_option_match()
+ {
+ if (re) {
+ rspamd_regexp_unref(re);
+ }
+ }
+
+ auto match_opt(const std::string_view &data) const -> bool
+ {
+ if (re) {
+ return rspamd_regexp_search(re,
+ data.data(), data.size(),
+ nullptr, nullptr, false, nullptr);
+ }
+ else {
+ return data == match;
+ }
+ }
+
+ auto get_pat() const -> std::string_view
+ {
+ if (re) {
+ return std::string_view(rspamd_regexp_get_pattern(re));
+ }
+ else {
+ return match;
+ }
+ }
+};
+
+enum class rspamd_composite_atom_type {
+ ATOM_UNKNOWN,
+ ATOM_COMPOSITE,
+ ATOM_PLAIN
+};
+
+struct rspamd_composite_atom {
+ std::string symbol;
+ std::string_view norm_symbol;
+ rspamd_composite_atom_type comp_type = rspamd_composite_atom_type::ATOM_UNKNOWN;
+ const struct rspamd_composite *ncomp; /* underlying composite */
+ std::vector<rspamd_composite_option_match> opts;
+};
+
+enum rspamd_composite_action : std::uint8_t {
+ RSPAMD_COMPOSITE_UNTOUCH = 0,
+ RSPAMD_COMPOSITE_REMOVE_SYMBOL = (1u << 0),
+ RSPAMD_COMPOSITE_REMOVE_WEIGHT = (1u << 1),
+ RSPAMD_COMPOSITE_REMOVE_FORCED = (1u << 2)
+};
+
+static GQuark
+rspamd_composites_quark(void)
+{
+ return g_quark_from_static_string("composites");
+}
+
+static auto
+rspamd_composite_atom_dtor(void *ptr)
+{
+ auto *atom = reinterpret_cast<rspamd_composite_atom *>(ptr);
+
+ delete atom;
+}
+
+static rspamd_expression_atom_t *
+rspamd_composite_expr_parse(const gchar *line, gsize len,
+ rspamd_mempool_t *pool,
+ gpointer ud, GError **err)
+{
+ gsize clen = 0;
+ const gchar *p, *end;
+ enum composite_expr_state {
+ comp_state_read_symbol = 0,
+ comp_state_read_obrace,
+ comp_state_read_option,
+ comp_state_read_regexp,
+ comp_state_read_regexp_end,
+ comp_state_read_comma,
+ comp_state_read_ebrace,
+ comp_state_read_end
+ } state = comp_state_read_symbol;
+
+ end = line + len;
+ p = line;
+
+ /* Find length of the atom using a reduced state machine */
+ while (p < end) {
+ if (state == comp_state_read_end) {
+ break;
+ }
+
+ switch (state) {
+ case comp_state_read_symbol:
+ clen = rspamd_memcspn(p, "[; \t()><!|&\n", len);
+ p += clen;
+
+ if (*p == '[') {
+ state = comp_state_read_obrace;
+ }
+ else {
+ state = comp_state_read_end;
+ }
+ break;
+ case comp_state_read_obrace:
+ p++;
+
+ if (*p == '/') {
+ p++;
+ state = comp_state_read_regexp;
+ }
+ else {
+ state = comp_state_read_option;
+ }
+ break;
+ case comp_state_read_regexp:
+ if (*p == '\\' && p + 1 < end) {
+ /* Escaping */
+ p++;
+ }
+ else if (*p == '/') {
+ /* End of regexp, possible flags */
+ state = comp_state_read_regexp_end;
+ }
+ p++;
+ break;
+ case comp_state_read_option:
+ case comp_state_read_regexp_end:
+ if (*p == ',') {
+ p++;
+ state = comp_state_read_comma;
+ }
+ else if (*p == ']') {
+ state = comp_state_read_ebrace;
+ }
+ else {
+ p++;
+ }
+ break;
+ case comp_state_read_comma:
+ if (!g_ascii_isspace(*p)) {
+ if (*p == '/') {
+ state = comp_state_read_regexp;
+ }
+ else if (*p == ']') {
+ state = comp_state_read_ebrace;
+ }
+ else {
+ state = comp_state_read_option;
+ }
+ }
+ else {
+ /* Skip spaces after comma */
+ p++;
+ }
+ break;
+ case comp_state_read_ebrace:
+ p++;
+ state = comp_state_read_end;
+ break;
+ case comp_state_read_end:
+ g_assert_not_reached();
+ }
+ }
+
+ if (state != comp_state_read_end) {
+ g_set_error(err, rspamd_composites_quark(), 100, "invalid composite: %s;"
+ "parser stopped in state %d",
+ line, state);
+ return NULL;
+ }
+
+ clen = p - line;
+ p = line;
+ state = comp_state_read_symbol;
+
+ auto *atom = new rspamd_composite_atom;
+ auto *res = rspamd_mempool_alloc0_type(pool, rspamd_expression_atom_t);
+ res->len = clen;
+ res->str = line;
+
+ /* Full state machine to fill a composite atom */
+ const gchar *opt_start = nullptr;
+
+ while (p < end) {
+ if (state == comp_state_read_end) {
+ break;
+ }
+
+ switch (state) {
+ case comp_state_read_symbol: {
+ clen = rspamd_memcspn(p, "[; \t()><!|&\n", len);
+ p += clen;
+
+ if (*p == '[') {
+ state = comp_state_read_obrace;
+ }
+ else {
+ state = comp_state_read_end;
+ }
+
+ atom->symbol = std::string{line, clen};
+ auto norm_start = std::find_if(atom->symbol.begin(), atom->symbol.end(),
+ [](char c) { return g_ascii_isalnum(c); });
+ if (norm_start == atom->symbol.end()) {
+ msg_err_pool("invalid composite atom: %s", atom->symbol.c_str());
+ }
+ atom->norm_symbol = make_string_view_from_it(norm_start, atom->symbol.end());
+ break;
+ }
+ case comp_state_read_obrace:
+ p++;
+
+ if (*p == '/') {
+ opt_start = p;
+ p++; /* Starting slash */
+ state = comp_state_read_regexp;
+ }
+ else {
+ state = comp_state_read_option;
+ opt_start = p;
+ }
+
+ break;
+ case comp_state_read_regexp:
+ if (*p == '\\' && p + 1 < end) {
+ /* Escaping */
+ p++;
+ }
+ else if (*p == '/') {
+ /* End of regexp, possible flags */
+ state = comp_state_read_regexp_end;
+ }
+ p++;
+ break;
+ case comp_state_read_option:
+ if (*p == ',' || *p == ']') {
+ /* Plain match, copy option to ensure string_view validity */
+ gint opt_len = p - opt_start;
+ auto *opt_buf = rspamd_mempool_alloc_buffer(pool, opt_len + 1);
+ rspamd_strlcpy(opt_buf, opt_start, opt_len + 1);
+ opt_buf = g_strstrip(opt_buf);
+ atom->opts.emplace_back(opt_buf, strlen(opt_buf));
+
+ if (*p == ',') {
+ p++;
+ state = comp_state_read_comma;
+ }
+ else {
+ state = comp_state_read_ebrace;
+ }
+ }
+ else {
+ p++;
+ }
+ break;
+ case comp_state_read_regexp_end:
+ if (*p == ',' || *p == ']') {
+ auto opt_len = p - opt_start;
+ rspamd_regexp_t *re;
+ GError *re_err = nullptr;
+
+ re = rspamd_regexp_new_len(opt_start, opt_len, nullptr, &re_err);
+
+ if (re == nullptr) {
+ msg_err_pool("cannot create regexp from string %*s: %e",
+ opt_len, opt_start, re_err);
+
+ g_error_free(re_err);
+ }
+ else {
+ atom->opts.emplace_back(re);
+ rspamd_regexp_unref(re);
+ }
+
+ if (*p == ',') {
+ p++;
+ state = comp_state_read_comma;
+ }
+ else {
+ state = comp_state_read_ebrace;
+ }
+ }
+ else {
+ p++;
+ }
+ break;
+ case comp_state_read_comma:
+ if (!g_ascii_isspace(*p)) {
+ if (*p == '/') {
+ state = comp_state_read_regexp;
+ opt_start = p;
+ }
+ else if (*p == ']') {
+ state = comp_state_read_ebrace;
+ }
+ else {
+ opt_start = p;
+ state = comp_state_read_option;
+ }
+ }
+ else {
+ /* Skip spaces after comma */
+ p++;
+ }
+ break;
+ case comp_state_read_ebrace:
+ p++;
+ state = comp_state_read_end;
+ break;
+ case comp_state_read_end:
+ g_assert_not_reached();
+ }
+ }
+
+ res->data = atom;
+
+ return res;
+}
+
+static auto
+process_symbol_removal(rspamd_expression_atom_t *atom,
+ struct composites_data *cd,
+ struct rspamd_symbol_result *ms,
+ const std::string &beg) -> void
+{
+ struct rspamd_task *task = cd->task;
+
+ if (ms == nullptr) {
+ return;
+ }
+
+ /*
+ * At this point we know that we need to do something about this symbol,
+ * however, we don't know whether we need to delete it unfortunately,
+ * that depends on the later decisions when the complete expression is
+ * evaluated.
+ */
+ auto rd_it = cd->symbols_to_remove.find(ms->name);
+
+ auto fill_removal_structure = [&](symbol_remove_data &nrd) {
+ nrd.sym = ms->name;
+
+ /* By default remove symbols */
+ switch (cd->composite->policy) {
+ case rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_REMOVE_ALL:
+ default:
+ nrd.action = (RSPAMD_COMPOSITE_REMOVE_SYMBOL | RSPAMD_COMPOSITE_REMOVE_WEIGHT);
+ break;
+ case rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_REMOVE_SYMBOL:
+ nrd.action = RSPAMD_COMPOSITE_REMOVE_SYMBOL;
+ break;
+ case rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_REMOVE_WEIGHT:
+ nrd.action = RSPAMD_COMPOSITE_REMOVE_WEIGHT;
+ break;
+ case rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_LEAVE:
+ nrd.action = 0;
+ break;
+ }
+
+ for (auto t: beg) {
+ if (t == '~') {
+ nrd.action &= ~RSPAMD_COMPOSITE_REMOVE_SYMBOL;
+ }
+ else if (t == '-') {
+ nrd.action &= ~(RSPAMD_COMPOSITE_REMOVE_WEIGHT |
+ RSPAMD_COMPOSITE_REMOVE_SYMBOL);
+ }
+ else if (t == '^') {
+ nrd.action |= RSPAMD_COMPOSITE_REMOVE_FORCED;
+ }
+ else {
+ break;
+ }
+ }
+
+ nrd.comp = cd->composite;
+ nrd.parent = atom->parent;
+ };
+
+ if (rd_it != cd->symbols_to_remove.end()) {
+ fill_removal_structure(rd_it->second.emplace_back());
+ msg_debug_composites("%s: added symbol %s to removal: %d policy, from composite %s",
+ cd->metric_res->name,
+ ms->name, rd_it->second.back().action,
+ cd->composite->sym.c_str());
+ }
+ else {
+ std::vector<symbol_remove_data> nrd;
+ fill_removal_structure(nrd.emplace_back());
+ msg_debug_composites("%s: added symbol %s to removal: %d policy, from composite %s",
+ cd->metric_res->name,
+ ms->name, nrd.front().action,
+ cd->composite->sym.c_str());
+ cd->symbols_to_remove[ms->name] = std::move(nrd);
+ }
+}
+
+static auto
+process_single_symbol(struct composites_data *cd,
+ std::string_view sym,
+ struct rspamd_symbol_result **pms,
+ struct rspamd_composite_atom *atom) -> double
+{
+ struct rspamd_symbol_result *ms = nullptr;
+ gdouble rc = 0;
+ struct rspamd_task *task = cd->task;
+
+ if ((ms = rspamd_task_find_symbol_result(cd->task, sym.data(), cd->metric_res)) == nullptr) {
+ msg_debug_composites("not found symbol %s in composite %s", sym.data(),
+ cd->composite->sym.c_str());
+
+ if (G_UNLIKELY(atom->comp_type == rspamd_composite_atom_type::ATOM_UNKNOWN)) {
+ const struct rspamd_composite *ncomp;
+
+ if ((ncomp = COMPOSITE_MANAGER_FROM_PTR(task->cfg->composites_manager)->find(sym)) != NULL) {
+ atom->comp_type = rspamd_composite_atom_type::ATOM_COMPOSITE;
+ atom->ncomp = ncomp;
+ }
+ else {
+ atom->comp_type = rspamd_composite_atom_type::ATOM_PLAIN;
+ }
+ }
+
+ if (atom->comp_type == rspamd_composite_atom_type::ATOM_COMPOSITE) {
+ msg_debug_composites("symbol %s for composite %s is another composite",
+ sym.data(), cd->composite->sym.c_str());
+
+ if (!cd->checked[atom->ncomp->id * 2]) {
+ msg_debug_composites("composite dependency %s for %s is not checked",
+ sym.data(), cd->composite->sym.c_str());
+ /* Set checked for this symbol to avoid cyclic references */
+ cd->checked[cd->composite->id * 2] = true;
+ auto *saved = cd->composite; /* Save the current composite */
+ composites_foreach_callback((gpointer) atom->ncomp->sym.c_str(),
+ (gpointer) atom->ncomp, (gpointer) cd);
+ /* Restore state */
+ cd->composite = saved;
+ cd->checked[cd->composite->id * 2] = false;
+
+ ms = rspamd_task_find_symbol_result(cd->task, sym.data(),
+ cd->metric_res);
+ }
+ else {
+ /*
+ * XXX: in case of cyclic references this would return 0
+ */
+ if (cd->checked[atom->ncomp->id * 2 + 1]) {
+ ms = rspamd_task_find_symbol_result(cd->task, sym.data(),
+ cd->metric_res);
+ }
+ }
+ }
+ }
+
+ if (ms) {
+ msg_debug_composites("found symbol %s in composite %s, weight: %.3f",
+ sym.data(), cd->composite->sym.c_str(), ms->score);
+
+ /* Now check options */
+ for (const auto &cur_opt: atom->opts) {
+ struct rspamd_symbol_option *opt;
+ auto found = false;
+
+ DL_FOREACH(ms->opts_head, opt)
+ {
+ if (cur_opt.match_opt({opt->option, opt->optlen})) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ auto pat = cur_opt.get_pat();
+ msg_debug_composites("symbol %s in composite %s misses required option %*s",
+ sym.data(),
+ cd->composite->sym.c_str(),
+ (int) pat.size(), pat.data());
+ ms = nullptr;
+
+ break;
+ }
+ }
+
+ if (ms) {
+ if (ms->score == 0) {
+ rc = epsilon * 16.0; /* Distinguish from 0 */
+ }
+ else {
+ rc = ms->score;
+ }
+ }
+ }
+
+ *pms = ms;
+ return rc;
+}
+
+static auto
+rspamd_composite_expr_process(void *ud, rspamd_expression_atom_t *atom) -> double
+{
+ struct composites_data *cd = (struct composites_data *) ud;
+ struct rspamd_composite_atom *comp_atom = (struct rspamd_composite_atom *) atom->data;
+
+ struct rspamd_symbol_result *ms = NULL;
+ struct rspamd_task *task = cd->task;
+ gdouble rc = 0;
+
+ if (cd->checked[cd->composite->id * 2]) {
+ /* We have already checked this composite, so just return its value */
+ if (cd->checked[cd->composite->id * 2 + 1]) {
+ ms = rspamd_task_find_symbol_result(cd->task,
+ comp_atom->norm_symbol.data(),
+ cd->metric_res);
+ }
+
+ if (ms) {
+ if (ms->score == 0) {
+ rc = epsilon; /* Distinguish from 0 */
+ }
+ else {
+ /* Treat negative and positive scores equally... */
+ rc = fabs(ms->score);
+ }
+ }
+
+ msg_debug_composites("composite %s is already checked, result: %.2f",
+ cd->composite->sym.c_str(), rc);
+
+ return rc;
+ }
+
+ /* Note: sym is zero terminated as it is a view on std::string */
+ auto sym = comp_atom->norm_symbol;
+ auto group_process_functor = [&](auto cond, int sub_start) -> double {
+ auto max = 0.;
+ GHashTableIter it;
+ gpointer k, v;
+ struct rspamd_symbols_group *gr;
+
+ gr = (struct rspamd_symbols_group *) g_hash_table_lookup(cd->task->cfg->groups,
+ sym.substr(sub_start).data());
+
+ if (gr != nullptr) {
+ g_hash_table_iter_init(&it, gr->symbols);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ auto *sdef = (rspamd_symbol *) v;
+
+ if (cond(sdef->score)) {
+ rc = process_single_symbol(cd,
+ std::string_view(sdef->name),
+ &ms,
+ comp_atom);
+
+ if (fabs(rc) > epsilon) {
+ process_symbol_removal(atom,
+ cd,
+ ms,
+ comp_atom->symbol);
+
+ if (fabs(rc) > max) {
+ max = fabs(rc);
+ }
+ }
+ }
+ }
+ }
+
+ return max;
+ };
+
+ if (sym.size() > 2) {
+ if (sym.substr(0, 2) == "g:") {
+ rc = group_process_functor([](auto _) { return true; }, 2);
+ }
+ else if (sym.substr(0, 3) == "g+:") {
+ /* Group, positive symbols only */
+ rc = group_process_functor([](auto sc) { return sc > 0.; }, 3);
+ }
+ else if (sym.substr(0, 3) == "g-:") {
+ rc = group_process_functor([](auto sc) { return sc < 0.; }, 3);
+ }
+ else {
+ rc = process_single_symbol(cd, sym, &ms, comp_atom);
+
+ if (fabs(rc) > epsilon) {
+ process_symbol_removal(atom,
+ cd,
+ ms,
+ comp_atom->symbol);
+ }
+ }
+ }
+ else {
+ rc = process_single_symbol(cd, sym, &ms, comp_atom);
+
+ if (fabs(rc) > epsilon) {
+ process_symbol_removal(atom,
+ cd,
+ ms,
+ comp_atom->symbol);
+ }
+ }
+
+ msg_debug_composites("%s: result for atom %s in composite %s is %.4f",
+ cd->metric_res->name,
+ comp_atom->norm_symbol.data(),
+ cd->composite->sym.c_str(), rc);
+
+ return rc;
+}
+
+/*
+ * We don't have preferences for composites
+ */
+static gint
+rspamd_composite_expr_priority(rspamd_expression_atom_t *atom)
+{
+ return 0;
+}
+
+static void
+rspamd_composite_expr_destroy(rspamd_expression_atom_t *atom)
+{
+ rspamd_composite_atom_dtor(atom->data);
+}
+
+static void
+composites_foreach_callback(gpointer key, gpointer value, void *data)
+{
+ auto *cd = (struct composites_data *) data;
+ auto *comp = (struct rspamd_composite *) value;
+ auto *str_key = (const gchar *) key;
+ struct rspamd_task *task;
+ gdouble rc;
+
+ cd->composite = comp;
+ task = cd->task;
+
+ msg_debug_composites("process composite %s", str_key);
+
+ if (!cd->checked[cd->composite->id * 2]) {
+ if (rspamd_symcache_is_checked(cd->task, cd->task->cfg->cache,
+ str_key)) {
+ msg_debug_composites("composite %s is checked in symcache but not "
+ "in composites bitfield",
+ cd->composite->sym.c_str());
+ cd->checked[comp->id * 2] = true;
+ cd->checked[comp->id * 2 + 1] = false;
+ }
+ else {
+ if (rspamd_task_find_symbol_result(cd->task, str_key,
+ cd->metric_res) != nullptr) {
+ /* Already set, no need to check */
+ msg_debug_composites("composite %s is already in metric "
+ "in composites bitfield",
+ cd->composite->sym.c_str());
+ cd->checked[comp->id * 2] = true;
+ cd->checked[comp->id * 2 + 1] = true;
+
+ return;
+ }
+
+ msg_debug_composites("%s: start processing composite %s",
+ cd->metric_res->name,
+ cd->composite->sym.c_str());
+
+ rc = rspamd_process_expression(comp->expr, RSPAMD_EXPRESSION_FLAG_NOOPT,
+ cd);
+
+ /* Checked bit */
+ cd->checked[comp->id * 2] = true;
+
+ msg_debug_composites("%s: final result for composite %s is %.4f",
+ cd->metric_res->name,
+ cd->composite->sym.c_str(), rc);
+
+ /* Result bit */
+ if (fabs(rc) > epsilon) {
+ cd->checked[comp->id * 2 + 1] = true;
+ rspamd_task_insert_result_full(cd->task, str_key, 1.0, NULL,
+ RSPAMD_SYMBOL_INSERT_SINGLE, cd->metric_res);
+ }
+ else {
+ cd->checked[comp->id * 2 + 1] = false;
+ }
+ }
+ }
+}
+
+
+static auto
+remove_symbols(const composites_data &cd, const std::vector<symbol_remove_data> &rd) -> void
+{
+ struct rspamd_task *task = cd.task;
+ gboolean skip = FALSE,
+ has_valid_op = FALSE,
+ want_remove_score = TRUE,
+ want_remove_symbol = TRUE,
+ want_forced = FALSE;
+ const gchar *disable_score_reason = "no policy",
+ *disable_symbol_reason = "no policy";
+
+ task = cd.task;
+
+ for (const auto &cur: rd) {
+ if (!cd.checked[cur.comp->id * 2 + 1]) {
+ continue;
+ }
+ /*
+ * First of all exclude all elements with any parent that is negation:
+ * !A || B -> here we can have both !A and B matched, but we do *NOT*
+ * want to remove symbol in that case
+ */
+ auto *par = cur.parent;
+ skip = FALSE;
+
+ while (par) {
+ if (rspamd_expression_node_is_op(par, OP_NOT)) {
+ skip = TRUE;
+ break;
+ }
+
+ par = par->parent;
+ }
+
+ if (skip) {
+ continue;
+ }
+
+ has_valid_op = TRUE;
+ /*
+ * Now we can try to remove symbols/scores
+ *
+ * We apply the following logic here:
+ * - if no composites would like to save score then we remove score
+ * - if no composites would like to save symbol then we remove symbol
+ */
+ if (!want_forced) {
+ if (!(cur.action & RSPAMD_COMPOSITE_REMOVE_SYMBOL)) {
+ want_remove_symbol = FALSE;
+ disable_symbol_reason = cur.comp->sym.c_str();
+ }
+
+ if (!(cur.action & RSPAMD_COMPOSITE_REMOVE_WEIGHT)) {
+ want_remove_score = FALSE;
+ disable_score_reason = cur.comp->sym.c_str();
+ }
+
+ if (cur.action & RSPAMD_COMPOSITE_REMOVE_FORCED) {
+ want_forced = TRUE;
+ disable_symbol_reason = cur.comp->sym.c_str();
+ disable_score_reason = cur.comp->sym.c_str();
+ }
+ }
+ }
+
+ auto *ms = rspamd_task_find_symbol_result(task, rd.front().sym, cd.metric_res);
+
+ if (has_valid_op && ms && !(ms->flags & RSPAMD_SYMBOL_RESULT_IGNORED)) {
+
+ if (want_remove_score || want_forced) {
+ msg_debug_composites("%s: %s remove symbol weight for %s (was %.2f), "
+ "score removal affected by %s, symbol removal affected by %s",
+ cd.metric_res->name,
+ (want_forced ? "forced" : "normal"), rd.front().sym, ms->score,
+ disable_score_reason, disable_symbol_reason);
+ cd.metric_res->score -= ms->score;
+ ms->score = 0.0;
+ }
+
+ if (want_remove_symbol || want_forced) {
+ ms->flags |= RSPAMD_SYMBOL_RESULT_IGNORED;
+ msg_debug_composites("%s: %s remove symbol %s (score %.2f), "
+ "score removal affected by %s, symbol removal affected by %s",
+ cd.metric_res->name,
+ (want_forced ? "forced" : "normal"), rd.front().sym, ms->score,
+ disable_score_reason, disable_symbol_reason);
+ }
+ }
+}
+
+static void
+composites_metric_callback(struct rspamd_task *task)
+{
+ std::vector<composites_data> comp_data_vec;
+ struct rspamd_scan_result *mres;
+
+ comp_data_vec.reserve(1);
+
+ DL_FOREACH(task->result, mres)
+ {
+ auto &cd = comp_data_vec.emplace_back(task, mres);
+
+ /* Process metric result */
+ rspamd_symcache_composites_foreach(task,
+ task->cfg->cache,
+ composites_foreach_callback,
+ &cd);
+ }
+
+ for (const auto &cd: comp_data_vec) {
+ /* Remove symbols that are in composites */
+ for (const auto &srd_it: cd.symbols_to_remove) {
+ remove_symbols(cd, srd_it.second);
+ }
+ }
+}
+
+}// namespace rspamd::composites
+
+
+void rspamd_composites_process_task(struct rspamd_task *task)
+{
+ if (task->result && !RSPAMD_TASK_IS_SKIPPED(task)) {
+ rspamd::composites::composites_metric_callback(task);
+ }
+}
diff --git a/src/libserver/composites/composites.h b/src/libserver/composites/composites.h
new file mode 100644
index 0000000..5d58029
--- /dev/null
+++ b/src/libserver/composites/composites.h
@@ -0,0 +1,64 @@
+/*-
+ * 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 SRC_LIBSERVER_COMPOSITES_H_
+#define SRC_LIBSERVER_COMPOSITES_H_
+
+#include "config.h"
+#include "contrib/libucl/ucl.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_task;
+struct rspamd_config;
+
+/**
+ * Process all results and form composite metrics from existent metrics as it is defined in config
+ * @param task worker's task that present message from user
+ */
+void rspamd_composites_process_task(struct rspamd_task *task);
+
+/**
+ * Creates a composites manager
+ * @param cfg
+ * @return
+ */
+void *rspamd_composites_manager_create(struct rspamd_config *cfg);
+/**
+ * Returns number of elements in a composite manager
+ * @return
+ */
+gsize rspamd_composites_manager_nelts(void *);
+/**
+ * Adds a composite from config
+ * @return
+ */
+void *rspamd_composites_manager_add_from_ucl(void *, const char *, const ucl_object_t *);
+void *rspamd_composites_manager_add_from_ucl_silent(void *, const char *, const ucl_object_t *);
+
+/**
+ * Adds a composite from config
+ * @return
+ */
+void *rspamd_composites_manager_add_from_string(void *, const char *, const char *);
+void *rspamd_composites_manager_add_from_string_silent(void *, const char *, const char *);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SRC_LIBSERVER_COMPOSITES_H_ */
diff --git a/src/libserver/composites/composites_internal.hxx b/src/libserver/composites/composites_internal.hxx
new file mode 100644
index 0000000..038e217
--- /dev/null
+++ b/src/libserver/composites/composites_internal.hxx
@@ -0,0 +1,112 @@
+/*-
+ * Copyright 2021 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_COMPOSITES_INTERNAL_HXX
+#define RSPAMD_COMPOSITES_INTERNAL_HXX
+#pragma once
+
+#include <string>
+#include "libutil/expression.h"
+#include "libutil/cxx/hash_util.hxx"
+#include "libserver/cfg_file.h"
+
+namespace rspamd::composites {
+
+/**
+ * Subr for composite expressions
+ */
+extern const struct rspamd_atom_subr composite_expr_subr;
+
+enum class rspamd_composite_policy {
+ RSPAMD_COMPOSITE_POLICY_REMOVE_ALL = 0,
+ RSPAMD_COMPOSITE_POLICY_REMOVE_SYMBOL,
+ RSPAMD_COMPOSITE_POLICY_REMOVE_WEIGHT,
+ RSPAMD_COMPOSITE_POLICY_LEAVE,
+ RSPAMD_COMPOSITE_POLICY_UNKNOWN
+};
+
+/**
+ * Static composites structure
+ */
+struct rspamd_composite {
+ std::string str_expr;
+ std::string sym;
+ struct rspamd_expression *expr;
+ gint id;
+ rspamd_composite_policy policy;
+};
+
+#define COMPOSITE_MANAGER_FROM_PTR(ptr) (reinterpret_cast<rspamd::composites::composites_manager *>(ptr))
+
+class composites_manager {
+public:
+ composites_manager(struct rspamd_config *_cfg)
+ : cfg(_cfg)
+ {
+ rspamd_mempool_add_destructor(_cfg->cfg_pool, composites_manager_dtor, this);
+ }
+
+ auto size(void) const -> std::size_t
+ {
+ return all_composites.size();
+ }
+
+ auto find(std::string_view name) const -> const rspamd_composite *
+ {
+ auto found = composites.find(std::string(name));
+
+ if (found != composites.end()) {
+ return found->second.get();
+ }
+
+ return nullptr;
+ }
+
+ auto add_composite(std::string_view, const ucl_object_t *, bool silent_duplicate) -> rspamd_composite *;
+ auto add_composite(std::string_view name, std::string_view expression, bool silent_duplicate, double score = NAN) -> rspamd_composite *;
+
+private:
+ ~composites_manager() = default;
+ static void composites_manager_dtor(void *ptr)
+ {
+ delete COMPOSITE_MANAGER_FROM_PTR(ptr);
+ }
+
+ auto new_composite(std::string_view composite_name, rspamd_expression *expr,
+ std::string_view composite_expression) -> auto
+ {
+ auto &composite = all_composites.emplace_back(std::make_shared<rspamd_composite>());
+ composite->expr = expr;
+ composite->id = all_composites.size() - 1;
+ composite->str_expr = composite_expression;
+ composite->sym = composite_name;
+
+ composites[composite->sym] = composite;
+
+ return composite;
+ }
+
+ ankerl::unordered_dense::map<std::string,
+ std::shared_ptr<rspamd_composite>, rspamd::smart_str_hash, rspamd::smart_str_equal>
+ composites;
+ /* Store all composites here, even if we have duplicates */
+ std::vector<std::shared_ptr<rspamd_composite>> all_composites;
+ struct rspamd_config *cfg;
+};
+
+}// namespace rspamd::composites
+
+#endif//RSPAMD_COMPOSITES_INTERNAL_HXX
diff --git a/src/libserver/composites/composites_manager.cxx b/src/libserver/composites/composites_manager.cxx
new file mode 100644
index 0000000..1ee5c40
--- /dev/null
+++ b/src/libserver/composites/composites_manager.cxx
@@ -0,0 +1,330 @@
+/*-
+ * Copyright 2021 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 <memory>
+#include <vector>
+#include <cmath>
+#include "contrib/ankerl/unordered_dense.h"
+
+#include "composites.h"
+#include "composites_internal.hxx"
+#include "libserver/cfg_file.h"
+#include "libserver/logger.h"
+#include "libserver/maps/map.h"
+#include "libutil/cxx/util.hxx"
+
+namespace rspamd::composites {
+
+static auto
+composite_policy_from_str(const std::string_view &inp) -> enum rspamd_composite_policy {
+ const static ankerl::unordered_dense::map<std::string_view,
+ enum rspamd_composite_policy>
+ names{
+ {"remove", rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_REMOVE_ALL},
+ {"remove_all", rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_REMOVE_ALL},
+ {"default", rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_REMOVE_ALL},
+ {"remove_symbol", rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_REMOVE_SYMBOL},
+ {"remove_weight", rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_REMOVE_WEIGHT},
+ {"leave", rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_LEAVE},
+ {"remove_none", rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_LEAVE},
+ };
+
+ auto found = names.find(inp);
+ if (found != names.end()){
+ return found->second;}
+
+return rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_UNKNOWN;
+}// namespace rspamd::composites
+
+auto composites_manager::add_composite(std::string_view composite_name, const ucl_object_t *obj, bool silent_duplicate) -> rspamd_composite *
+{
+
+ const auto *val = ucl_object_lookup(obj, "enabled");
+ if (val != nullptr && !ucl_object_toboolean(val)) {
+ msg_info_config("composite %s is disabled", composite_name.data());
+ return nullptr;
+ }
+
+ if (composites.contains(composite_name)) {
+ if (silent_duplicate) {
+ msg_debug_config("composite %s is redefined", composite_name.data());
+ return nullptr;
+ }
+ else {
+ msg_warn_config("composite %s is redefined", composite_name.data());
+ }
+ }
+
+ const char *composite_expression = nullptr;
+ val = ucl_object_lookup(obj, "expression");
+
+ if (val == NULL || !ucl_object_tostring_safe(val, &composite_expression)) {
+ msg_err_config("composite must have an expression defined in %s",
+ composite_name.data());
+ return nullptr;
+ }
+
+ GError *err = nullptr;
+ rspamd_expression *expr = nullptr;
+
+ if (!rspamd_parse_expression(composite_expression, 0, &composite_expr_subr,
+ NULL, cfg->cfg_pool, &err, &expr)) {
+ msg_err_config("cannot parse composite expression for %s: %e",
+ composite_name.data(), err);
+
+ if (err) {
+ g_error_free(err);
+ }
+
+ return nullptr;
+ }
+
+ const auto &composite = new_composite(composite_name, expr, composite_expression);
+
+ auto score = std::isnan(cfg->unknown_weight) ? 0.0 : cfg->unknown_weight;
+ val = ucl_object_lookup(obj, "score");
+
+ if (val != nullptr) {
+ ucl_object_todouble_safe(val, &score);
+ }
+
+ /* Also set score in the metric */
+ const auto *group = "composite";
+ val = ucl_object_lookup(obj, "group");
+ if (val != nullptr) {
+ group = ucl_object_tostring(val);
+ }
+
+ const auto *description = composite_expression;
+ val = ucl_object_lookup(obj, "description");
+ if (val != nullptr) {
+ description = ucl_object_tostring(val);
+ }
+
+ rspamd_config_add_symbol(cfg, composite_name.data(), score,
+ description, group,
+ 0,
+ ucl_object_get_priority(obj), /* No +1 as it is default... */
+ 1);
+
+ const auto *elt = ucl_object_lookup(obj, "groups");
+ if (elt && ucl_object_type(elt) == UCL_ARRAY) {
+ const ucl_object_t *cur_gr;
+ auto *gr_it = ucl_object_iterate_new(elt);
+
+ while ((cur_gr = ucl_object_iterate_safe(gr_it, true)) != nullptr) {
+ rspamd_config_add_symbol_group(cfg, composite_name.data(),
+ ucl_object_tostring(cur_gr));
+ }
+
+ ucl_object_iterate_free(gr_it);
+ }
+
+ val = ucl_object_lookup(obj, "policy");
+ if (val) {
+ composite->policy = composite_policy_from_str(ucl_object_tostring(val));
+
+ if (composite->policy == rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_UNKNOWN) {
+ msg_err_config("composite %s has incorrect policy", composite_name.data());
+ return nullptr;
+ }
+ }
+
+ return composite.get();
+}
+
+auto composites_manager::add_composite(std::string_view composite_name,
+ std::string_view composite_expression,
+ bool silent_duplicate, double score) -> rspamd_composite *
+{
+ GError *err = nullptr;
+ rspamd_expression *expr = nullptr;
+
+ if (composites.contains(composite_name)) {
+ /* Duplicate composite - refuse to add */
+ if (silent_duplicate) {
+ msg_debug_config("composite %s is redefined", composite_name.data());
+ return nullptr;
+ }
+ else {
+ msg_warn_config("composite %s is redefined", composite_name.data());
+ }
+ }
+
+ if (!rspamd_parse_expression(composite_expression.data(),
+ composite_expression.size(), &composite_expr_subr,
+ nullptr, cfg->cfg_pool, &err, &expr)) {
+ msg_err_config("cannot parse composite expression for %s: %e",
+ composite_name.data(), err);
+
+ if (err) {
+ g_error_free(err);
+ }
+
+ return nullptr;
+ }
+
+ auto final_score = std::isnan(score) ? (std::isnan(cfg->unknown_weight) ? 0.0 : cfg->unknown_weight) : score;
+ rspamd_config_add_symbol(cfg, composite_name.data(), final_score,
+ composite_name.data(), "composite",
+ 0,
+ 0,
+ 1);
+
+ return new_composite(composite_name, expr, composite_expression).get();
+}
+
+struct map_cbdata {
+ composites_manager *cm;
+ struct rspamd_config *cfg;
+ std::string buf;
+
+ explicit map_cbdata(struct rspamd_config *cfg)
+ : cfg(cfg)
+ {
+ cm = COMPOSITE_MANAGER_FROM_PTR(cfg->composites_manager);
+ }
+
+ static char *map_read(char *chunk, int len,
+ struct map_cb_data *data,
+ gboolean _final)
+ {
+
+ if (data->cur_data == nullptr) {
+ data->cur_data = data->prev_data;
+ reinterpret_cast<map_cbdata *>(data->cur_data)->buf.clear();
+ }
+
+ auto *cbd = reinterpret_cast<map_cbdata *>(data->cur_data);
+
+ cbd->buf.append(chunk, len);
+ return nullptr;
+ }
+
+ static void
+ map_fin(struct map_cb_data *data, void **target)
+ {
+ auto *cbd = reinterpret_cast<map_cbdata *>(data->cur_data);
+
+ if (data->errored) {
+ if (cbd) {
+ cbd->buf.clear();
+ }
+ }
+ else if (cbd != nullptr) {
+ if (target) {
+ *target = data->cur_data;
+ }
+
+ rspamd::string_foreach_line(cbd->buf, [&](std::string_view line) {
+ auto [name_and_score, expr] = rspamd::string_split_on(line, ' ');
+ auto [name, score] = rspamd::string_split_on(name_and_score, ':');
+
+ if (!score.empty()) {
+ /* I wish it was supported properly */
+ //auto conv_res = std::from_chars(value->data(), value->size(), num);
+ char numbuf[128], *endptr = nullptr;
+ rspamd_strlcpy(numbuf, score.data(), MIN(score.size(), sizeof(numbuf)));
+ auto num = g_ascii_strtod(numbuf, &endptr);
+
+ if (fabs(num) >= G_MAXFLOAT || std::isnan(num)) {
+ msg_err("invalid score for %*s", (int) name_and_score.size(), name_and_score.data());
+ return;
+ }
+
+ auto ret = cbd->cm->add_composite(name, expr, true, num);
+
+ if (ret == nullptr) {
+ msg_err("cannot add composite %*s", (int) name_and_score.size(), name_and_score.data());
+ return;
+ }
+ }
+ else {
+ msg_err("missing score for %*s", (int) name_and_score.size(), name_and_score.data());
+ return;
+ }
+ });
+ }
+ else {
+ msg_err("no data read for composites map");
+ }
+ }
+
+ static void
+ map_dtor(struct map_cb_data *data)
+ {
+ auto *cbd = reinterpret_cast<map_cbdata *>(data->cur_data);
+ delete cbd;
+ }
+};
+}
+
+
+void *
+rspamd_composites_manager_create(struct rspamd_config *cfg)
+{
+ auto *cm = new rspamd::composites::composites_manager(cfg);
+
+ return reinterpret_cast<void *>(cm);
+}
+
+
+gsize rspamd_composites_manager_nelts(void *ptr)
+{
+ return COMPOSITE_MANAGER_FROM_PTR(ptr)->size();
+}
+
+void *
+rspamd_composites_manager_add_from_ucl(void *cm, const char *sym, const ucl_object_t *obj)
+{
+ return reinterpret_cast<void *>(COMPOSITE_MANAGER_FROM_PTR(cm)->add_composite(sym, obj, false));
+}
+
+void *
+rspamd_composites_manager_add_from_string(void *cm, const char *sym, const char *expr)
+{
+ return reinterpret_cast<void *>(COMPOSITE_MANAGER_FROM_PTR(cm)->add_composite(sym, expr, false));
+}
+
+void *
+rspamd_composites_manager_add_from_ucl_silent(void *cm, const char *sym, const ucl_object_t *obj)
+{
+ return reinterpret_cast<void *>(COMPOSITE_MANAGER_FROM_PTR(cm)->add_composite(sym, obj, true));
+}
+
+void *
+rspamd_composites_manager_add_from_string_silent(void *cm, const char *sym, const char *expr)
+{
+ return reinterpret_cast<void *>(COMPOSITE_MANAGER_FROM_PTR(cm)->add_composite(sym, expr, true));
+}
+
+
+bool rspamd_composites_add_map_handlers(const ucl_object_t *obj, struct rspamd_config *cfg)
+{
+ auto **pcbdata = rspamd_mempool_alloc_type(cfg->cfg_pool, rspamd::composites::map_cbdata *);
+ auto *cbdata = new rspamd::composites::map_cbdata{cfg};
+ *pcbdata = cbdata;
+
+ if (struct rspamd_map * m; (m = rspamd_map_add_from_ucl(cfg, obj, "composites map",
+ rspamd::composites::map_cbdata::map_read, rspamd::composites::map_cbdata::map_fin,
+ rspamd::composites::map_cbdata::map_dtor, (void **) pcbdata,
+ nullptr, RSPAMD_MAP_DEFAULT)) == nullptr) {
+ msg_err_config("cannot load composites map from %s", ucl_object_key(obj));
+ return false;
+ }
+
+ return true;
+} \ No newline at end of file
diff --git a/src/libserver/css/CMakeLists.txt b/src/libserver/css/CMakeLists.txt
new file mode 100644
index 0000000..c0c9d51
--- /dev/null
+++ b/src/libserver/css/CMakeLists.txt
@@ -0,0 +1,9 @@
+SET(LIBCSSSRC "${CMAKE_CURRENT_SOURCE_DIR}/css.cxx"
+ "${CMAKE_CURRENT_SOURCE_DIR}/css_property.cxx"
+ "${CMAKE_CURRENT_SOURCE_DIR}/css_value.cxx"
+ "${CMAKE_CURRENT_SOURCE_DIR}/css_selector.cxx"
+ "${CMAKE_CURRENT_SOURCE_DIR}/css_tokeniser.cxx"
+ "${CMAKE_CURRENT_SOURCE_DIR}/css_util.cxx"
+ "${CMAKE_CURRENT_SOURCE_DIR}/css_rule.cxx"
+ "${CMAKE_CURRENT_SOURCE_DIR}/css_parser.cxx"
+ PARENT_SCOPE) \ No newline at end of file
diff --git a/src/libserver/css/css.cxx b/src/libserver/css/css.cxx
new file mode 100644
index 0000000..1b369ed
--- /dev/null
+++ b/src/libserver/css/css.cxx
@@ -0,0 +1,227 @@
+/*-
+ * Copyright 2021 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 "css.hxx"
+#include "contrib/ankerl/unordered_dense.h"
+#include "css_parser.hxx"
+#include "libserver/html/html_tag.hxx"
+#include "libserver/html/html_block.hxx"
+
+/* Keep unit tests implementation here (it'll possibly be moved outside one day) */
+#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
+#define DOCTEST_CONFIG_IMPLEMENT
+#include "doctest/doctest.h"
+
+namespace rspamd::css {
+
+INIT_LOG_MODULE_PUBLIC(css);
+
+class css_style_sheet::impl {
+public:
+ using sel_shared_hash = smart_ptr_hash<css_selector>;
+ using sel_shared_eq = smart_ptr_equal<css_selector>;
+ using selector_ptr = std::unique_ptr<css_selector>;
+ using selectors_hash = ankerl::unordered_dense::map<selector_ptr, css_declarations_block_ptr,
+ sel_shared_hash, sel_shared_eq>;
+ using universal_selector_t = std::pair<selector_ptr, css_declarations_block_ptr>;
+ selectors_hash tags_selector;
+ selectors_hash class_selectors;
+ selectors_hash id_selectors;
+ std::optional<universal_selector_t> universal_selector;
+};
+
+css_style_sheet::css_style_sheet(rspamd_mempool_t *pool)
+ : pool(pool), pimpl(new impl)
+{
+}
+css_style_sheet::~css_style_sheet()
+{
+}
+
+auto css_style_sheet::add_selector_rule(std::unique_ptr<css_selector> &&selector,
+ css_declarations_block_ptr decls) -> void
+{
+ impl::selectors_hash *target_hash = nullptr;
+
+ switch (selector->type) {
+ case css_selector::selector_type::SELECTOR_ALL:
+ if (pimpl->universal_selector) {
+ /* Another universal selector */
+ msg_debug_css("redefined universal selector, merging rules");
+ pimpl->universal_selector->second->merge_block(*decls);
+ }
+ else {
+ msg_debug_css("added universal selector");
+ pimpl->universal_selector = std::make_pair(std::move(selector),
+ decls);
+ }
+ break;
+ case css_selector::selector_type::SELECTOR_CLASS:
+ target_hash = &pimpl->class_selectors;
+ break;
+ case css_selector::selector_type::SELECTOR_ID:
+ target_hash = &pimpl->id_selectors;
+ break;
+ case css_selector::selector_type::SELECTOR_TAG:
+ target_hash = &pimpl->tags_selector;
+ break;
+ }
+
+ if (target_hash) {
+ auto found_it = target_hash->find(selector);
+
+ if (found_it == target_hash->end()) {
+ /* Easy case, new element */
+ target_hash->insert({std::move(selector), decls});
+ }
+ else {
+ /* The problem with merging is actually in how to handle selectors chains
+ * For example, we have 2 selectors:
+ * 1. class id tag -> meaning that we first match class, then we ensure that
+ * id is also the same and finally we check the tag
+ * 2. tag class id -> it means that we check first tag, then class and then id
+ * So we have somehow equal path in the xpath terms.
+ * I suppose now, that we merely check parent stuff and handle duplicates
+ * merging when finally resolving paths.
+ */
+ auto sel_str = selector->to_string().value_or("unknown");
+ msg_debug_css("found duplicate selector: %*s", (int) sel_str.size(),
+ sel_str.data());
+ found_it->second->merge_block(*decls);
+ }
+ }
+}
+
+auto css_style_sheet::check_tag_block(const rspamd::html::html_tag *tag) -> rspamd::html::html_block *
+{
+ std::optional<std::string_view> id_comp, class_comp;
+ rspamd::html::html_block *res = nullptr;
+
+ if (!tag) {
+ return nullptr;
+ }
+
+ /* First, find id in a tag and a class */
+ for (const auto &param: tag->components) {
+ if (param.type == html::html_component_type::RSPAMD_HTML_COMPONENT_ID) {
+ id_comp = param.value;
+ }
+ else if (param.type == html::html_component_type::RSPAMD_HTML_COMPONENT_CLASS) {
+ class_comp = param.value;
+ }
+ }
+
+ /* ID part */
+ if (id_comp && !pimpl->id_selectors.empty()) {
+ auto found_id_sel = pimpl->id_selectors.find(css_selector{id_comp.value()});
+
+ if (found_id_sel != pimpl->id_selectors.end()) {
+ const auto &decl = *(found_id_sel->second);
+ res = decl.compile_to_block(pool);
+ }
+ }
+
+ /* Class part */
+ if (class_comp && !pimpl->class_selectors.empty()) {
+ auto sv_split = [](auto strv, std::string_view delims = " ") -> std::vector<std::string_view> {
+ std::vector<decltype(strv)> ret;
+ std::size_t start = 0;
+
+ while (start < strv.size()) {
+ const auto last = strv.find_first_of(delims, start);
+ if (start != last) {
+ ret.emplace_back(strv.substr(start, last - start));
+ }
+
+ if (last == std::string_view::npos) {
+ break;
+ }
+
+ start = last + 1;
+ }
+
+ return ret;
+ };
+
+ auto elts = sv_split(class_comp.value());
+
+ for (const auto &e: elts) {
+ auto found_class_sel = pimpl->class_selectors.find(
+ css_selector{e, css_selector::selector_type::SELECTOR_CLASS});
+
+ if (found_class_sel != pimpl->class_selectors.end()) {
+ const auto &decl = *(found_class_sel->second);
+ auto *tmp = decl.compile_to_block(pool);
+
+ if (res == nullptr) {
+ res = tmp;
+ }
+ else {
+ res->propagate_block(*tmp);
+ }
+ }
+ }
+ }
+
+ /* Tags part */
+ if (!pimpl->tags_selector.empty()) {
+ auto found_tag_sel = pimpl->tags_selector.find(
+ css_selector{static_cast<tag_id_t>(tag->id)});
+
+ if (found_tag_sel != pimpl->tags_selector.end()) {
+ const auto &decl = *(found_tag_sel->second);
+ auto *tmp = decl.compile_to_block(pool);
+
+ if (res == nullptr) {
+ res = tmp;
+ }
+ else {
+ res->propagate_block(*tmp);
+ }
+ }
+ }
+
+ /* Finally, universal selector */
+ if (pimpl->universal_selector) {
+ auto *tmp = pimpl->universal_selector->second->compile_to_block(pool);
+
+ if (res == nullptr) {
+ res = tmp;
+ }
+ else {
+ res->propagate_block(*tmp);
+ }
+ }
+
+ return res;
+}
+
+auto css_parse_style(rspamd_mempool_t *pool,
+ std::string_view input,
+ std::shared_ptr<css_style_sheet> &&existing)
+ -> css_return_pair
+{
+ auto parse_res = rspamd::css::parse_css(pool, input,
+ std::forward<std::shared_ptr<css_style_sheet>>(existing));
+
+ if (parse_res.has_value()) {
+ return std::make_pair(parse_res.value(), css_parse_error());
+ }
+
+ return std::make_pair(nullptr, parse_res.error());
+}
+
+}// namespace rspamd::css \ No newline at end of file
diff --git a/src/libserver/css/css.hxx b/src/libserver/css/css.hxx
new file mode 100644
index 0000000..f0f8120
--- /dev/null
+++ b/src/libserver/css/css.hxx
@@ -0,0 +1,68 @@
+/*-
+ * Copyright 2021 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.
+ */
+#pragma once
+
+#ifndef RSPAMD_CSS_HXX
+#define RSPAMD_CSS_HXX
+
+#include <string>
+#include <memory>
+#include "logger.h"
+#include "css_rule.hxx"
+#include "css_selector.hxx"
+
+namespace rspamd::html {
+/* Forward declaration */
+struct html_tag;
+struct html_block;
+}// namespace rspamd::html
+
+namespace rspamd::css {
+
+extern int rspamd_css_log_id;
+
+#define msg_debug_css(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_css_log_id, "css", pool->tag.uid, \
+ __FUNCTION__, \
+ __VA_ARGS__)
+#define msg_err_css(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "css", pool->tag.uid, \
+ __FUNCTION__, \
+ __VA_ARGS__)
+
+class css_style_sheet {
+public:
+ css_style_sheet(rspamd_mempool_t *pool);
+ ~css_style_sheet(); /* must be declared separately due to pimpl */
+ auto add_selector_rule(std::unique_ptr<css_selector> &&selector,
+ css_declarations_block_ptr decls) -> void;
+
+ auto check_tag_block(const rspamd::html::html_tag *tag) -> rspamd::html::html_block *;
+
+private:
+ class impl;
+ rspamd_mempool_t *pool;
+ std::unique_ptr<impl> pimpl;
+};
+
+using css_return_pair = std::pair<std::shared_ptr<css_style_sheet>, css_parse_error>;
+auto css_parse_style(rspamd_mempool_t *pool,
+ std::string_view input,
+ std::shared_ptr<css_style_sheet> &&existing) -> css_return_pair;
+
+}// namespace rspamd::css
+
+#endif//RSPAMD_CSS_H \ No newline at end of file
diff --git a/src/libserver/css/css_colors_list.hxx b/src/libserver/css/css_colors_list.hxx
new file mode 100644
index 0000000..6dfe54f
--- /dev/null
+++ b/src/libserver/css/css_colors_list.hxx
@@ -0,0 +1,738 @@
+/*-
+ * Copyright 2021 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_CSS_COLORS_LIST_HXX
+#define RSPAMD_CSS_COLORS_LIST_HXX
+
+#pragma once
+
+#include <string_view>
+#include "contrib/ankerl/unordered_dense.h"
+#include "css_value.hxx"
+
+namespace rspamd::css {
+
+/*
+ * List of all colors, intended to use with hashes/sets
+ * TODO: think about frozen structs when we can deal with 700 values without
+ * compiler limits...
+ */
+static const ankerl::unordered_dense::map<std::string_view, css_color> css_colors_map{
+ {"aliceblue", {240, 248, 255}},
+ {"antiquewhite", {250, 235, 215}},
+ {"antiquewhite1", {255, 239, 219}},
+ {"antiquewhite2", {238, 223, 204}},
+ {"antiquewhite3", {205, 192, 176}},
+ {"antiquewhite4", {139, 131, 120}},
+ {"aqua", {0, 255, 255}},
+ {"aquamarine", {127, 255, 212}},
+ {"aquamarine1", {127, 255, 212}},
+ {"aquamarine2", {118, 238, 198}},
+ {"aquamarine3", {102, 205, 170}},
+ {"aquamarine4", {69, 139, 116}},
+ {"azure", {240, 255, 255}},
+ {"azure1", {240, 255, 255}},
+ {"azure2", {224, 238, 238}},
+ {"azure3", {193, 205, 205}},
+ {"azure4", {131, 139, 139}},
+ {"beige", {245, 245, 220}},
+ {"bisque", {255, 228, 196}},
+ {"bisque1", {255, 228, 196}},
+ {"bisque2", {238, 213, 183}},
+ {"bisque3", {205, 183, 158}},
+ {"bisque4", {139, 125, 107}},
+ {"black", {0, 0, 0}},
+ {"blanchedalmond", {255, 235, 205}},
+ {"blue", {0, 0, 255}},
+ {"blue1", {0, 0, 255}},
+ {"blue2", {0, 0, 238}},
+ {"blue3", {0, 0, 205}},
+ {"blue4", {0, 0, 139}},
+ {"blueviolet", {138, 43, 226}},
+ {"brown", {165, 42, 42}},
+ {"brown1", {255, 64, 64}},
+ {"brown2", {238, 59, 59}},
+ {"brown3", {205, 51, 51}},
+ {"brown4", {139, 35, 35}},
+ {"burlywood", {222, 184, 135}},
+ {"burlywood1", {255, 211, 155}},
+ {"burlywood2", {238, 197, 145}},
+ {"burlywood3", {205, 170, 125}},
+ {"burlywood4", {139, 115, 85}},
+ {"cadetblue", {95, 158, 160}},
+ {"cadetblue1", {152, 245, 255}},
+ {"cadetblue2", {142, 229, 238}},
+ {"cadetblue3", {122, 197, 205}},
+ {"cadetblue4", {83, 134, 139}},
+ {"chartreuse", {127, 255, 0}},
+ {"chartreuse1", {127, 255, 0}},
+ {"chartreuse2", {118, 238, 0}},
+ {"chartreuse3", {102, 205, 0}},
+ {"chartreuse4", {69, 139, 0}},
+ {"chocolate", {210, 105, 30}},
+ {"chocolate1", {255, 127, 36}},
+ {"chocolate2", {238, 118, 33}},
+ {"chocolate3", {205, 102, 29}},
+ {"chocolate4", {139, 69, 19}},
+ {"coral", {255, 127, 80}},
+ {"coral1", {255, 114, 86}},
+ {"coral2", {238, 106, 80}},
+ {"coral3", {205, 91, 69}},
+ {"coral4", {139, 62, 47}},
+ {"cornflowerblue", {100, 149, 237}},
+ {"cornsilk", {255, 248, 220}},
+ {"cornsilk1", {255, 248, 220}},
+ {"cornsilk2", {238, 232, 205}},
+ {"cornsilk3", {205, 200, 177}},
+ {"cornsilk4", {139, 136, 120}},
+ {"crimson", {220, 20, 60}},
+ {"cyan", {0, 255, 255}},
+ {"cyan1", {0, 255, 255}},
+ {"cyan2", {0, 238, 238}},
+ {"cyan3", {0, 205, 205}},
+ {"cyan4", {0, 139, 139}},
+ {"darkblue", {0, 0, 139}},
+ {"darkcyan", {0, 139, 139}},
+ {"darkgoldenrod", {184, 134, 11}},
+ {"darkgoldenrod1", {255, 185, 15}},
+ {"darkgoldenrod2", {238, 173, 14}},
+ {"darkgoldenrod3", {205, 149, 12}},
+ {"darkgoldenrod4", {139, 101, 8}},
+ {"darkgray", {169, 169, 169}},
+ {"darkgreen", {0, 100, 0}},
+ {"darkgrey", {169, 169, 169}},
+ {"darkkhaki", {189, 183, 107}},
+ {"darkmagenta", {139, 0, 139}},
+ {"darkolivegreen", {85, 107, 47}},
+ {"darkolivegreen1", {202, 255, 112}},
+ {"darkolivegreen2", {188, 238, 104}},
+ {"darkolivegreen3", {162, 205, 90}},
+ {"darkolivegreen4", {110, 139, 61}},
+ {"darkorange", {255, 140, 0}},
+ {"darkorange1", {255, 127, 0}},
+ {"darkorange2", {238, 118, 0}},
+ {"darkorange3", {205, 102, 0}},
+ {"darkorange4", {139, 69, 0}},
+ {"darkorchid", {153, 50, 204}},
+ {"darkorchid1", {191, 62, 255}},
+ {"darkorchid2", {178, 58, 238}},
+ {"darkorchid3", {154, 50, 205}},
+ {"darkorchid4", {104, 34, 139}},
+ {"darkred", {139, 0, 0}},
+ {"darksalmon", {233, 150, 122}},
+ {"darkseagreen", {143, 188, 143}},
+ {"darkseagreen1", {193, 255, 193}},
+ {"darkseagreen2", {180, 238, 180}},
+ {"darkseagreen3", {155, 205, 155}},
+ {"darkseagreen4", {105, 139, 105}},
+ {"darkslateblue", {72, 61, 139}},
+ {"darkslategray", {47, 79, 79}},
+ {"darkslategray1", {151, 255, 255}},
+ {"darkslategray2", {141, 238, 238}},
+ {"darkslategray3", {121, 205, 205}},
+ {"darkslategray4", {82, 139, 139}},
+ {"darkslategrey", {47, 79, 79}},
+ {"darkturquoise", {0, 206, 209}},
+ {"darkviolet", {148, 0, 211}},
+ {"deeppink", {255, 20, 147}},
+ {"deeppink1", {255, 20, 147}},
+ {"deeppink2", {238, 18, 137}},
+ {"deeppink3", {205, 16, 118}},
+ {"deeppink4", {139, 10, 80}},
+ {"deepskyblue", {0, 191, 255}},
+ {"deepskyblue1", {0, 191, 255}},
+ {"deepskyblue2", {0, 178, 238}},
+ {"deepskyblue3", {0, 154, 205}},
+ {"deepskyblue4", {0, 104, 139}},
+ {"dimgray", {105, 105, 105}},
+ {"dimgrey", {105, 105, 105}},
+ {"dodgerblue", {30, 144, 255}},
+ {"dodgerblue1", {30, 144, 255}},
+ {"dodgerblue2", {28, 134, 238}},
+ {"dodgerblue3", {24, 116, 205}},
+ {"dodgerblue4", {16, 78, 139}},
+ {"firebrick", {178, 34, 34}},
+ {"firebrick1", {255, 48, 48}},
+ {"firebrick2", {238, 44, 44}},
+ {"firebrick3", {205, 38, 38}},
+ {"firebrick4", {139, 26, 26}},
+ {"floralwhite", {255, 250, 240}},
+ {"forestgreen", {34, 139, 34}},
+ {"fuchsia", {255, 0, 255}},
+ {"gainsboro", {220, 220, 220}},
+ {"ghostwhite", {248, 248, 255}},
+ {"gold", {255, 215, 0}},
+ {"gold1", {255, 215, 0}},
+ {"gold2", {238, 201, 0}},
+ {"gold3", {205, 173, 0}},
+ {"gold4", {139, 117, 0}},
+ {"goldenrod", {218, 165, 32}},
+ {"goldenrod1", {255, 193, 37}},
+ {"goldenrod2", {238, 180, 34}},
+ {"goldenrod3", {205, 155, 29}},
+ {"goldenrod4", {139, 105, 20}},
+ {"gray", {190, 190, 190}},
+ {"gray0", {0, 0, 0}},
+ {"gray1", {3, 3, 3}},
+ {"gray10", {26, 26, 26}},
+ {"gray100", {255, 255, 255}},
+ {"gray11", {28, 28, 28}},
+ {"gray12", {31, 31, 31}},
+ {"gray13", {33, 33, 33}},
+ {"gray14", {36, 36, 36}},
+ {"gray15", {38, 38, 38}},
+ {"gray16", {41, 41, 41}},
+ {"gray17", {43, 43, 43}},
+ {"gray18", {46, 46, 46}},
+ {"gray19", {48, 48, 48}},
+ {"gray2", {5, 5, 5}},
+ {"gray20", {51, 51, 51}},
+ {"gray21", {54, 54, 54}},
+ {"gray22", {56, 56, 56}},
+ {"gray23", {59, 59, 59}},
+ {"gray24", {61, 61, 61}},
+ {"gray25", {64, 64, 64}},
+ {"gray26", {66, 66, 66}},
+ {"gray27", {69, 69, 69}},
+ {"gray28", {71, 71, 71}},
+ {"gray29", {74, 74, 74}},
+ {"gray3", {8, 8, 8}},
+ {"gray30", {77, 77, 77}},
+ {"gray31", {79, 79, 79}},
+ {"gray32", {82, 82, 82}},
+ {"gray33", {84, 84, 84}},
+ {"gray34", {87, 87, 87}},
+ {"gray35", {89, 89, 89}},
+ {"gray36", {92, 92, 92}},
+ {"gray37", {94, 94, 94}},
+ {"gray38", {97, 97, 97}},
+ {"gray39", {99, 99, 99}},
+ {"gray4", {10, 10, 10}},
+ {"gray40", {102, 102, 102}},
+ {"gray41", {105, 105, 105}},
+ {"gray42", {107, 107, 107}},
+ {"gray43", {110, 110, 110}},
+ {"gray44", {112, 112, 112}},
+ {"gray45", {115, 115, 115}},
+ {"gray46", {117, 117, 117}},
+ {"gray47", {120, 120, 120}},
+ {"gray48", {122, 122, 122}},
+ {"gray49", {125, 125, 125}},
+ {"gray5", {13, 13, 13}},
+ {"gray50", {127, 127, 127}},
+ {"gray51", {130, 130, 130}},
+ {"gray52", {133, 133, 133}},
+ {"gray53", {135, 135, 135}},
+ {"gray54", {138, 138, 138}},
+ {"gray55", {140, 140, 140}},
+ {"gray56", {143, 143, 143}},
+ {"gray57", {145, 145, 145}},
+ {"gray58", {148, 148, 148}},
+ {"gray59", {150, 150, 150}},
+ {"gray6", {15, 15, 15}},
+ {"gray60", {153, 153, 153}},
+ {"gray61", {156, 156, 156}},
+ {"gray62", {158, 158, 158}},
+ {"gray63", {161, 161, 161}},
+ {"gray64", {163, 163, 163}},
+ {"gray65", {166, 166, 166}},
+ {"gray66", {168, 168, 168}},
+ {"gray67", {171, 171, 171}},
+ {"gray68", {173, 173, 173}},
+ {"gray69", {176, 176, 176}},
+ {"gray7", {18, 18, 18}},
+ {"gray70", {179, 179, 179}},
+ {"gray71", {181, 181, 181}},
+ {"gray72", {184, 184, 184}},
+ {"gray73", {186, 186, 186}},
+ {"gray74", {189, 189, 189}},
+ {"gray75", {191, 191, 191}},
+ {"gray76", {194, 194, 194}},
+ {"gray77", {196, 196, 196}},
+ {"gray78", {199, 199, 199}},
+ {"gray79", {201, 201, 201}},
+ {"gray8", {20, 20, 20}},
+ {"gray80", {204, 204, 204}},
+ {"gray81", {207, 207, 207}},
+ {"gray82", {209, 209, 209}},
+ {"gray83", {212, 212, 212}},
+ {"gray84", {214, 214, 214}},
+ {"gray85", {217, 217, 217}},
+ {"gray86", {219, 219, 219}},
+ {"gray87", {222, 222, 222}},
+ {"gray88", {224, 224, 224}},
+ {"gray89", {227, 227, 227}},
+ {"gray9", {23, 23, 23}},
+ {"gray90", {229, 229, 229}},
+ {"gray91", {232, 232, 232}},
+ {"gray92", {235, 235, 235}},
+ {"gray93", {237, 237, 237}},
+ {"gray94", {240, 240, 240}},
+ {"gray95", {242, 242, 242}},
+ {"gray96", {245, 245, 245}},
+ {"gray97", {247, 247, 247}},
+ {"gray98", {250, 250, 250}},
+ {"gray99", {252, 252, 252}},
+ {"green", {0, 255, 0}},
+ {"green1", {0, 255, 0}},
+ {"green2", {0, 238, 0}},
+ {"green3", {0, 205, 0}},
+ {"green4", {0, 139, 0}},
+ {"greenyellow", {173, 255, 47}},
+ {"grey", {190, 190, 190}},
+ {"grey0", {0, 0, 0}},
+ {"grey1", {3, 3, 3}},
+ {"grey10", {26, 26, 26}},
+ {"grey100", {255, 255, 255}},
+ {"grey11", {28, 28, 28}},
+ {"grey12", {31, 31, 31}},
+ {"grey13", {33, 33, 33}},
+ {"grey14", {36, 36, 36}},
+ {"grey15", {38, 38, 38}},
+ {"grey16", {41, 41, 41}},
+ {"grey17", {43, 43, 43}},
+ {"grey18", {46, 46, 46}},
+ {"grey19", {48, 48, 48}},
+ {"grey2", {5, 5, 5}},
+ {"grey20", {51, 51, 51}},
+ {"grey21", {54, 54, 54}},
+ {"grey22", {56, 56, 56}},
+ {"grey23", {59, 59, 59}},
+ {"grey24", {61, 61, 61}},
+ {"grey25", {64, 64, 64}},
+ {"grey26", {66, 66, 66}},
+ {"grey27", {69, 69, 69}},
+ {"grey28", {71, 71, 71}},
+ {"grey29", {74, 74, 74}},
+ {"grey3", {8, 8, 8}},
+ {"grey30", {77, 77, 77}},
+ {"grey31", {79, 79, 79}},
+ {"grey32", {82, 82, 82}},
+ {"grey33", {84, 84, 84}},
+ {"grey34", {87, 87, 87}},
+ {"grey35", {89, 89, 89}},
+ {"grey36", {92, 92, 92}},
+ {"grey37", {94, 94, 94}},
+ {"grey38", {97, 97, 97}},
+ {"grey39", {99, 99, 99}},
+ {"grey4", {10, 10, 10}},
+ {"grey40", {102, 102, 102}},
+ {"grey41", {105, 105, 105}},
+ {"grey42", {107, 107, 107}},
+ {"grey43", {110, 110, 110}},
+ {"grey44", {112, 112, 112}},
+ {"grey45", {115, 115, 115}},
+ {"grey46", {117, 117, 117}},
+ {"grey47", {120, 120, 120}},
+ {"grey48", {122, 122, 122}},
+ {"grey49", {125, 125, 125}},
+ {"grey5", {13, 13, 13}},
+ {"grey50", {127, 127, 127}},
+ {"grey51", {130, 130, 130}},
+ {"grey52", {133, 133, 133}},
+ {"grey53", {135, 135, 135}},
+ {"grey54", {138, 138, 138}},
+ {"grey55", {140, 140, 140}},
+ {"grey56", {143, 143, 143}},
+ {"grey57", {145, 145, 145}},
+ {"grey58", {148, 148, 148}},
+ {"grey59", {150, 150, 150}},
+ {"grey6", {15, 15, 15}},
+ {"grey60", {153, 153, 153}},
+ {"grey61", {156, 156, 156}},
+ {"grey62", {158, 158, 158}},
+ {"grey63", {161, 161, 161}},
+ {"grey64", {163, 163, 163}},
+ {"grey65", {166, 166, 166}},
+ {"grey66", {168, 168, 168}},
+ {"grey67", {171, 171, 171}},
+ {"grey68", {173, 173, 173}},
+ {"grey69", {176, 176, 176}},
+ {"grey7", {18, 18, 18}},
+ {"grey70", {179, 179, 179}},
+ {"grey71", {181, 181, 181}},
+ {"grey72", {184, 184, 184}},
+ {"grey73", {186, 186, 186}},
+ {"grey74", {189, 189, 189}},
+ {"grey75", {191, 191, 191}},
+ {"grey76", {194, 194, 194}},
+ {"grey77", {196, 196, 196}},
+ {"grey78", {199, 199, 199}},
+ {"grey79", {201, 201, 201}},
+ {"grey8", {20, 20, 20}},
+ {"grey80", {204, 204, 204}},
+ {"grey81", {207, 207, 207}},
+ {"grey82", {209, 209, 209}},
+ {"grey83", {212, 212, 212}},
+ {"grey84", {214, 214, 214}},
+ {"grey85", {217, 217, 217}},
+ {"grey86", {219, 219, 219}},
+ {"grey87", {222, 222, 222}},
+ {"grey88", {224, 224, 224}},
+ {"grey89", {227, 227, 227}},
+ {"grey9", {23, 23, 23}},
+ {"grey90", {229, 229, 229}},
+ {"grey91", {232, 232, 232}},
+ {"grey92", {235, 235, 235}},
+ {"grey93", {237, 237, 237}},
+ {"grey94", {240, 240, 240}},
+ {"grey95", {242, 242, 242}},
+ {"grey96", {245, 245, 245}},
+ {"grey97", {247, 247, 247}},
+ {"grey98", {250, 250, 250}},
+ {"grey99", {252, 252, 252}},
+ {"honeydew", {240, 255, 240}},
+ {"honeydew1", {240, 255, 240}},
+ {"honeydew2", {224, 238, 224}},
+ {"honeydew3", {193, 205, 193}},
+ {"honeydew4", {131, 139, 131}},
+ {"hotpink", {255, 105, 180}},
+ {"hotpink1", {255, 110, 180}},
+ {"hotpink2", {238, 106, 167}},
+ {"hotpink3", {205, 96, 144}},
+ {"hotpink4", {139, 58, 98}},
+ {"indianred", {205, 92, 92}},
+ {"indianred1", {255, 106, 106}},
+ {"indianred2", {238, 99, 99}},
+ {"indianred3", {205, 85, 85}},
+ {"indianred4", {139, 58, 58}},
+ {"indigo", {75, 0, 130}},
+ {"ivory", {255, 255, 240}},
+ {"ivory1", {255, 255, 240}},
+ {"ivory2", {238, 238, 224}},
+ {"ivory3", {205, 205, 193}},
+ {"ivory4", {139, 139, 131}},
+ {"khaki", {240, 230, 140}},
+ {"khaki1", {255, 246, 143}},
+ {"khaki2", {238, 230, 133}},
+ {"khaki3", {205, 198, 115}},
+ {"khaki4", {139, 134, 78}},
+ {"lavender", {230, 230, 250}},
+ {"lavenderblush", {255, 240, 245}},
+ {"lavenderblush1", {255, 240, 245}},
+ {"lavenderblush2", {238, 224, 229}},
+ {"lavenderblush3", {205, 193, 197}},
+ {"lavenderblush4", {139, 131, 134}},
+ {"lawngreen", {124, 252, 0}},
+ {"lemonchiffon", {255, 250, 205}},
+ {"lemonchiffon1", {255, 250, 205}},
+ {"lemonchiffon2", {238, 233, 191}},
+ {"lemonchiffon3", {205, 201, 165}},
+ {"lemonchiffon4", {139, 137, 112}},
+ {"lightblue", {173, 216, 230}},
+ {"lightblue1", {191, 239, 255}},
+ {"lightblue2", {178, 223, 238}},
+ {"lightblue3", {154, 192, 205}},
+ {"lightblue4", {104, 131, 139}},
+ {"lightcoral", {240, 128, 128}},
+ {"lightcyan", {224, 255, 255}},
+ {"lightcyan1", {224, 255, 255}},
+ {"lightcyan2", {209, 238, 238}},
+ {"lightcyan3", {180, 205, 205}},
+ {"lightcyan4", {122, 139, 139}},
+ {"lightgoldenrod", {238, 221, 130}},
+ {"lightgoldenrod1", {255, 236, 139}},
+ {"lightgoldenrod2", {238, 220, 130}},
+ {"lightgoldenrod3", {205, 190, 112}},
+ {"lightgoldenrod4", {139, 129, 76}},
+ {"lightgoldenrodyellow", {250, 250, 210}},
+ {"lightgray", {211, 211, 211}},
+ {"lightgreen", {144, 238, 144}},
+ {"lightgrey", {211, 211, 211}},
+ {"lightpink", {255, 182, 193}},
+ {"lightpink1", {255, 174, 185}},
+ {"lightpink2", {238, 162, 173}},
+ {"lightpink3", {205, 140, 149}},
+ {"lightpink4", {139, 95, 101}},
+ {"lightsalmon", {255, 160, 122}},
+ {"lightsalmon1", {255, 160, 122}},
+ {"lightsalmon2", {238, 149, 114}},
+ {"lightsalmon3", {205, 129, 98}},
+ {"lightsalmon4", {139, 87, 66}},
+ {"lightseagreen", {32, 178, 170}},
+ {"lightskyblue", {135, 206, 250}},
+ {"lightskyblue1", {176, 226, 255}},
+ {"lightskyblue2", {164, 211, 238}},
+ {"lightskyblue3", {141, 182, 205}},
+ {"lightskyblue4", {96, 123, 139}},
+ {"lightslateblue", {132, 112, 255}},
+ {"lightslategray", {119, 136, 153}},
+ {"lightslategrey", {119, 136, 153}},
+ {"lightsteelblue", {176, 196, 222}},
+ {"lightsteelblue1", {202, 225, 255}},
+ {"lightsteelblue2", {188, 210, 238}},
+ {"lightsteelblue3", {162, 181, 205}},
+ {"lightsteelblue4", {110, 123, 139}},
+ {"lightyellow", {255, 255, 224}},
+ {"lightyellow1", {255, 255, 224}},
+ {"lightyellow2", {238, 238, 209}},
+ {"lightyellow3", {205, 205, 180}},
+ {"lightyellow4", {139, 139, 122}},
+ {"lime", {0, 255, 0}},
+ {"limegreen", {50, 205, 50}},
+ {"linen", {250, 240, 230}},
+ {"magenta", {255, 0, 255}},
+ {"magenta1", {255, 0, 255}},
+ {"magenta2", {238, 0, 238}},
+ {"magenta3", {205, 0, 205}},
+ {"magenta4", {139, 0, 139}},
+ {"maroon", {176, 48, 96}},
+ {"maroon1", {255, 52, 179}},
+ {"maroon2", {238, 48, 167}},
+ {"maroon3", {205, 41, 144}},
+ {"maroon4", {139, 28, 98}},
+ {"mediumaquamarine", {102, 205, 170}},
+ {"mediumblue", {0, 0, 205}},
+ {"mediumorchid", {186, 85, 211}},
+ {"mediumorchid1", {224, 102, 255}},
+ {"mediumorchid2", {209, 95, 238}},
+ {"mediumorchid3", {180, 82, 205}},
+ {"mediumorchid4", {122, 55, 139}},
+ {"mediumpurple", {147, 112, 219}},
+ {"mediumpurple1", {171, 130, 255}},
+ {"mediumpurple2", {159, 121, 238}},
+ {"mediumpurple3", {137, 104, 205}},
+ {"mediumpurple4", {93, 71, 139}},
+ {"mediumseagreen", {60, 179, 113}},
+ {"mediumslateblue", {123, 104, 238}},
+ {"mediumspringgreen", {0, 250, 154}},
+ {"mediumturquoise", {72, 209, 204}},
+ {"mediumvioletred", {199, 21, 133}},
+ {"midnightblue", {25, 25, 112}},
+ {"mintcream", {245, 255, 250}},
+ {"mistyrose", {255, 228, 225}},
+ {"mistyrose1", {255, 228, 225}},
+ {"mistyrose2", {238, 213, 210}},
+ {"mistyrose3", {205, 183, 181}},
+ {"mistyrose4", {139, 125, 123}},
+ {"moccasin", {255, 228, 181}},
+ {"navajowhite", {255, 222, 173}},
+ {"navajowhite1", {255, 222, 173}},
+ {"navajowhite2", {238, 207, 161}},
+ {"navajowhite3", {205, 179, 139}},
+ {"navajowhite4", {139, 121, 94}},
+ {"navy", {0, 0, 128}},
+ {"navyblue", {0, 0, 128}},
+ {"oldlace", {253, 245, 230}},
+ {"olive", {128, 128, 0}},
+ {"olivedrab", {107, 142, 35}},
+ {"olivedrab1", {192, 255, 62}},
+ {"olivedrab2", {179, 238, 58}},
+ {"olivedrab3", {154, 205, 50}},
+ {"olivedrab4", {105, 139, 34}},
+ {"orange", {255, 165, 0}},
+ {"orange1", {255, 165, 0}},
+ {"orange2", {238, 154, 0}},
+ {"orange3", {205, 133, 0}},
+ {"orange4", {139, 90, 0}},
+ {"orangered", {255, 69, 0}},
+ {"orangered1", {255, 69, 0}},
+ {"orangered2", {238, 64, 0}},
+ {"orangered3", {205, 55, 0}},
+ {"orangered4", {139, 37, 0}},
+ {"orchid", {218, 112, 214}},
+ {"orchid1", {255, 131, 250}},
+ {"orchid2", {238, 122, 233}},
+ {"orchid3", {205, 105, 201}},
+ {"orchid4", {139, 71, 137}},
+ {"palegoldenrod", {238, 232, 170}},
+ {"palegreen", {152, 251, 152}},
+ {"palegreen1", {154, 255, 154}},
+ {"palegreen2", {144, 238, 144}},
+ {"palegreen3", {124, 205, 124}},
+ {"palegreen4", {84, 139, 84}},
+ {"paleturquoise", {175, 238, 238}},
+ {"paleturquoise1", {187, 255, 255}},
+ {"paleturquoise2", {174, 238, 238}},
+ {"paleturquoise3", {150, 205, 205}},
+ {"paleturquoise4", {102, 139, 139}},
+ {"palevioletred", {219, 112, 147}},
+ {"palevioletred1", {255, 130, 171}},
+ {"palevioletred2", {238, 121, 159}},
+ {"palevioletred3", {205, 104, 137}},
+ {"palevioletred4", {139, 71, 93}},
+ {"papayawhip", {255, 239, 213}},
+ {"peachpuff", {255, 218, 185}},
+ {"peachpuff1", {255, 218, 185}},
+ {"peachpuff2", {238, 203, 173}},
+ {"peachpuff3", {205, 175, 149}},
+ {"peachpuff4", {139, 119, 101}},
+ {"peru", {205, 133, 63}},
+ {"pink", {255, 192, 203}},
+ {"pink1", {255, 181, 197}},
+ {"pink2", {238, 169, 184}},
+ {"pink3", {205, 145, 158}},
+ {"pink4", {139, 99, 108}},
+ {"plum", {221, 160, 221}},
+ {"plum1", {255, 187, 255}},
+ {"plum2", {238, 174, 238}},
+ {"plum3", {205, 150, 205}},
+ {"plum4", {139, 102, 139}},
+ {"powderblue", {176, 224, 230}},
+ {"purple", {160, 32, 240}},
+ {"purple1", {155, 48, 255}},
+ {"purple2", {145, 44, 238}},
+ {"purple3", {125, 38, 205}},
+ {"purple4", {85, 26, 139}},
+ {"rebeccapurple", {102, 51, 153}},
+ {"red", {255, 0, 0}},
+ {"red1", {255, 0, 0}},
+ {"red2", {238, 0, 0}},
+ {"red3", {205, 0, 0}},
+ {"red4", {139, 0, 0}},
+ {"rosybrown", {188, 143, 143}},
+ {"rosybrown1", {255, 193, 193}},
+ {"rosybrown2", {238, 180, 180}},
+ {"rosybrown3", {205, 155, 155}},
+ {"rosybrown4", {139, 105, 105}},
+ {"royalblue", {65, 105, 225}},
+ {"royalblue1", {72, 118, 255}},
+ {"royalblue2", {67, 110, 238}},
+ {"royalblue3", {58, 95, 205}},
+ {"royalblue4", {39, 64, 139}},
+ {"saddlebrown", {139, 69, 19}},
+ {"salmon", {250, 128, 114}},
+ {"salmon1", {255, 140, 105}},
+ {"salmon2", {238, 130, 98}},
+ {"salmon3", {205, 112, 84}},
+ {"salmon4", {139, 76, 57}},
+ {"sandybrown", {244, 164, 96}},
+ {"seagreen", {46, 139, 87}},
+ {"seagreen1", {84, 255, 159}},
+ {"seagreen2", {78, 238, 148}},
+ {"seagreen3", {67, 205, 128}},
+ {"seagreen4", {46, 139, 87}},
+ {"seashell", {255, 245, 238}},
+ {"seashell1", {255, 245, 238}},
+ {"seashell2", {238, 229, 222}},
+ {"seashell3", {205, 197, 191}},
+ {"seashell4", {139, 134, 130}},
+ {"sienna", {160, 82, 45}},
+ {"sienna1", {255, 130, 71}},
+ {"sienna2", {238, 121, 66}},
+ {"sienna3", {205, 104, 57}},
+ {"sienna4", {139, 71, 38}},
+ {"silver", {192, 192, 192}},
+ {"skyblue", {135, 206, 235}},
+ {"skyblue1", {135, 206, 255}},
+ {"skyblue2", {126, 192, 238}},
+ {"skyblue3", {108, 166, 205}},
+ {"skyblue4", {74, 112, 139}},
+ {"slateblue", {106, 90, 205}},
+ {"slateblue1", {131, 111, 255}},
+ {"slateblue2", {122, 103, 238}},
+ {"slateblue3", {105, 89, 205}},
+ {"slateblue4", {71, 60, 139}},
+ {"slategray", {112, 128, 144}},
+ {"slategray1", {198, 226, 255}},
+ {"slategray2", {185, 211, 238}},
+ {"slategray3", {159, 182, 205}},
+ {"slategray4", {108, 123, 139}},
+ {"slategrey", {112, 128, 144}},
+ {"snow", {255, 250, 250}},
+ {"snow1", {255, 250, 250}},
+ {"snow2", {238, 233, 233}},
+ {"snow3", {205, 201, 201}},
+ {"snow4", {139, 137, 137}},
+ {"springgreen", {0, 255, 127}},
+ {"springgreen1", {0, 255, 127}},
+ {"springgreen2", {0, 238, 118}},
+ {"springgreen3", {0, 205, 102}},
+ {"springgreen4", {0, 139, 69}},
+ {"steelblue", {70, 130, 180}},
+ {"steelblue1", {99, 184, 255}},
+ {"steelblue2", {92, 172, 238}},
+ {"steelblue3", {79, 148, 205}},
+ {"steelblue4", {54, 100, 139}},
+ {"tan", {210, 180, 140}},
+ {"tan1", {255, 165, 79}},
+ {"tan2", {238, 154, 73}},
+ {"tan3", {205, 133, 63}},
+ {"tan4", {139, 90, 43}},
+ {"teal", {0, 128, 128}},
+ {"thistle", {216, 191, 216}},
+ {"thistle1", {255, 225, 255}},
+ {"thistle2", {238, 210, 238}},
+ {"thistle3", {205, 181, 205}},
+ {"thistle4", {139, 123, 139}},
+ {"tomato", {255, 99, 71}},
+ {"tomato1", {255, 99, 71}},
+ {"tomato2", {238, 92, 66}},
+ {"tomato3", {205, 79, 57}},
+ {"tomato4", {139, 54, 38}},
+ {"turquoise", {64, 224, 208}},
+ {"turquoise1", {0, 245, 255}},
+ {"turquoise2", {0, 229, 238}},
+ {"turquoise3", {0, 197, 205}},
+ {"turquoise4", {0, 134, 139}},
+ {"violet", {238, 130, 238}},
+ {"violetred", {208, 32, 144}},
+ {"violetred1", {255, 62, 150}},
+ {"violetred2", {238, 58, 140}},
+ {"violetred3", {205, 50, 120}},
+ {"violetred4", {139, 34, 82}},
+ {"webgray", {128, 128, 128}},
+ {"webgreen", {0, 128, 0}},
+ {"webgrey", {128, 128, 128}},
+ {"webmaroon", {128, 0, 0}},
+ {"webpurple", {128, 0, 128}},
+ {"wheat", {245, 222, 179}},
+ {"wheat1", {255, 231, 186}},
+ {"wheat2", {238, 216, 174}},
+ {"wheat3", {205, 186, 150}},
+ {"wheat4", {139, 126, 102}},
+ {"white", {255, 255, 255}},
+ {"whitesmoke", {245, 245, 245}},
+ {"x11gray", {190, 190, 190}},
+ {"x11green", {0, 255, 0}},
+ {"x11grey", {190, 190, 190}},
+ {"x11maroon", {176, 48, 96}},
+ {"x11purple", {160, 32, 240}},
+ {"yellow", {255, 255, 0}},
+ {"yellow1", {255, 255, 0}},
+ {"yellow2", {238, 238, 0}},
+ {"yellow3", {205, 205, 0}},
+ {"yellow4", {139, 139, 0}},
+ {"yellowgreen", {154, 205, 50}},
+ {"activeborder", {180, 180, 180}},
+ {"activecaption", {153, 180, 209}},
+ {"appworkspace", {171, 171, 171}},
+ {"background", {0, 0, 0}},
+ {"buttonhighlight", {255, 255, 255}},
+ {"buttonshadow", {160, 160, 160}},
+ {"captiontext", {0, 0, 0}},
+ {"inactiveborder", {244, 247, 252}},
+ {"inactivecaption", {191, 205, 219}},
+ {"inactivecaptiontext", {0, 0, 0}},
+ {"infobackground", {255, 255, 225}},
+ {"infotext", {0, 0, 0}},
+ {"menu", {240, 240, 240}},
+ {"menutext", {0, 0, 0}},
+ {"scrollbar", {200, 200, 200}},
+ {"threeddarkshadow", {0, 0, 0}},
+ {"threedface", {0, 0, 0}},
+ {"threedhighlight", {0, 0, 0}},
+ {"threedlightshadow", {0, 0, 0}},
+ {"threedshadow", {0, 0, 0}},
+ {"transparent", {0, 0, 0, 0}},
+ {"window", {255, 255, 255}},
+ {"windowframe", {100, 100, 100}},
+ {"windowtext", {0, 0, 0}},
+};
+
+}// namespace rspamd::css
+
+#endif//RSPAMD_CSS_COLORS_LIST_HXX
diff --git a/src/libserver/css/css_parser.cxx b/src/libserver/css/css_parser.cxx
new file mode 100644
index 0000000..aed035a
--- /dev/null
+++ b/src/libserver/css/css_parser.cxx
@@ -0,0 +1,892 @@
+/*-
+ * Copyright 2021 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 "css_parser.hxx"
+#include "css_tokeniser.hxx"
+#include "css_selector.hxx"
+#include "css_rule.hxx"
+#include "css_util.hxx"
+#include "css.hxx"
+#include "fmt/core.h"
+
+#include <vector>
+#include <unicode/utf8.h>
+
+#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
+#include "doctest/doctest.h"
+
+namespace rspamd::css {
+
+const css_consumed_block css_parser_eof_block{};
+
+auto css_consumed_block::attach_block(consumed_block_ptr &&block) -> bool
+{
+ if (std::holds_alternative<std::monostate>(content)) {
+ /* Switch from monostate */
+ content = std::vector<consumed_block_ptr>();
+ }
+ else if (!std::holds_alternative<std::vector<consumed_block_ptr>>(content)) {
+ /* A single component, cannot attach a block ! */
+ return false;
+ }
+
+ auto &value_vec = std::get<std::vector<consumed_block_ptr>>(content);
+ value_vec.push_back(std::move(block));
+
+ return true;
+}
+
+auto css_consumed_block::add_function_argument(consumed_block_ptr &&block) -> bool
+{
+ if (!std::holds_alternative<css_function_block>(content)) {
+ return false;
+ }
+
+ auto &&func_bloc = std::get<css_function_block>(content);
+ func_bloc.args.push_back(std::move(block));
+
+ return true;
+}
+
+auto css_consumed_block::token_type_str(void) const -> const char *
+{
+ const auto *ret = "";
+
+ switch (tag) {
+ case parser_tag_type::css_top_block:
+ ret = "top";
+ break;
+ case parser_tag_type::css_qualified_rule:
+ ret = "qualified rule";
+ break;
+ case parser_tag_type::css_at_rule:
+ ret = "at rule";
+ break;
+ case parser_tag_type::css_simple_block:
+ ret = "simple block";
+ break;
+ case parser_tag_type::css_function:
+ ret = "function";
+ break;
+ case parser_tag_type::css_function_arg:
+ ret = "function arg";
+ break;
+ case parser_tag_type::css_component:
+ ret = "component";
+ break;
+ case parser_tag_type::css_eof_block:
+ ret = "eof";
+ break;
+ }
+
+ return ret;
+}
+
+auto css_consumed_block::debug_str(void) -> std::string
+{
+ std::string ret = fmt::format(R"("type": "{}", "value": )", token_type_str());
+
+ std::visit([&](auto &arg) {
+ using T = std::decay_t<decltype(arg)>;
+
+ if constexpr (std::is_same_v<T, std::vector<consumed_block_ptr>>) {
+ /* Array of blocks */
+ ret += "[";
+ for (const auto &block: arg) {
+ ret += "{";
+ ret += block->debug_str();
+ ret += "}, ";
+ }
+
+ if (*(--ret.end()) == ' ') {
+ ret.pop_back();
+ ret.pop_back(); /* Last ',' */
+ }
+ ret += "]";
+ }
+ else if constexpr (std::is_same_v<T, std::monostate>) {
+ /* Empty block */
+ ret += R"("empty")";
+ }
+ else if constexpr (std::is_same_v<T, css_function_block>) {
+ ret += R"({ "content": {"token": )";
+ ret += "\"" + arg.function.debug_token_str() + "\", ";
+ ret += R"("arguments": [)";
+
+ for (const auto &block: arg.args) {
+ ret += "{";
+ ret += block->debug_str();
+ ret += "}, ";
+ }
+ if (*(--ret.end()) == ' ') {
+ ret.pop_back();
+ ret.pop_back(); /* Last ',' */
+ }
+ ret += "]}}";
+ }
+ else {
+ /* Single element block */
+ ret += "\"" + arg.debug_token_str() + "\"";
+ }
+ },
+ content);
+
+ return ret;
+}
+
+class css_parser {
+public:
+ css_parser(void) = delete; /* Require mempool to be set for logging */
+ explicit css_parser(rspamd_mempool_t *pool)
+ : pool(pool)
+ {
+ style_object.reset();
+ error.type = css_parse_error_type::PARSE_ERROR_NO_ERROR;
+ }
+
+ /*
+ * This constructor captures existing via unique_ptr, but it does not
+ * destruct it on errors (we assume that it is owned somewhere else)
+ */
+ explicit css_parser(std::shared_ptr<css_style_sheet> &&existing, rspamd_mempool_t *pool)
+ : style_object(existing), pool(pool)
+ {
+ error.type = css_parse_error_type::PARSE_ERROR_NO_ERROR;
+ }
+
+ /*
+ * Process input css blocks
+ */
+ std::unique_ptr<css_consumed_block> consume_css_blocks(const std::string_view &sv);
+ /*
+ * Process a single css rule
+ */
+ std::unique_ptr<css_consumed_block> consume_css_rule(const std::string_view &sv);
+ std::optional<css_parse_error> consume_input(const std::string_view &sv);
+
+ auto get_object_maybe(void) -> tl::expected<std::shared_ptr<css_style_sheet>, css_parse_error>
+ {
+ if (style_object) {
+ return style_object;
+ }
+
+ return tl::make_unexpected(error);
+ }
+
+ /* Helper parser methods */
+ static bool need_unescape(const std::string_view &sv);
+
+private:
+ std::shared_ptr<css_style_sheet> style_object;
+ std::unique_ptr<css_tokeniser> tokeniser;
+
+ css_parse_error error;
+ rspamd_mempool_t *pool;
+
+ int rec_level = 0;
+ const int max_rec = 20;
+ bool eof = false;
+
+ /* Consumers */
+ auto component_value_consumer(std::unique_ptr<css_consumed_block> &top) -> bool;
+ auto function_consumer(std::unique_ptr<css_consumed_block> &top) -> bool;
+ auto simple_block_consumer(std::unique_ptr<css_consumed_block> &top,
+ css_parser_token::token_type expected_end,
+ bool consume_current) -> bool;
+ auto qualified_rule_consumer(std::unique_ptr<css_consumed_block> &top) -> bool;
+ auto at_rule_consumer(std::unique_ptr<css_consumed_block> &top) -> bool;
+};
+
+/*
+ * Find if we need to unescape css
+ */
+bool css_parser::need_unescape(const std::string_view &sv)
+{
+ bool in_quote = false;
+ char quote_char, prev_c = 0;
+
+ for (const auto c: sv) {
+ if (!in_quote) {
+ if (c == '"' || c == '\'') {
+ in_quote = true;
+ quote_char = c;
+ }
+ else if (c == '\\') {
+ return true;
+ }
+ }
+ else {
+ if (c == quote_char) {
+ if (prev_c != '\\') {
+ in_quote = false;
+ }
+ }
+ prev_c = c;
+ }
+ }
+
+ return false;
+}
+
+auto css_parser::function_consumer(std::unique_ptr<css_consumed_block> &top) -> bool
+{
+ auto ret = true, want_more = true;
+
+ msg_debug_css("consume function block; top block: %s, recursion level %d",
+ top->token_type_str(), rec_level);
+
+ if (++rec_level > max_rec) {
+ msg_err_css("max nesting reached, ignore style");
+ error = css_parse_error(css_parse_error_type::PARSE_ERROR_BAD_NESTING,
+ "maximum nesting has reached when parsing function value");
+ return false;
+ }
+
+ while (ret && want_more && !eof) {
+ auto next_token = tokeniser->next_token();
+
+ switch (next_token.type) {
+ case css_parser_token::token_type::eof_token:
+ eof = true;
+ break;
+ case css_parser_token::token_type::whitespace_token:
+ /* Ignore whitespaces */
+ break;
+ case css_parser_token::token_type::ebrace_token:
+ ret = true;
+ want_more = false;
+ break;
+ case css_parser_token::token_type::comma_token:
+ case css_parser_token::token_type::delim_token:
+ case css_parser_token::token_type::obrace_token:
+ break;
+ default:
+ /* Attach everything to the function block */
+ top->add_function_argument(std::make_unique<css_consumed_block>(
+ css::css_consumed_block::parser_tag_type::css_function_arg,
+ std::move(next_token)));
+ break;
+ }
+ }
+
+ --rec_level;
+
+ return ret;
+}
+
+auto css_parser::simple_block_consumer(std::unique_ptr<css_consumed_block> &top,
+ css_parser_token::token_type expected_end,
+ bool consume_current) -> bool
+{
+ auto ret = true;
+ std::unique_ptr<css_consumed_block> block;
+
+ msg_debug_css("consume simple block; top block: %s, recursion level %d",
+ top->token_type_str(), rec_level);
+
+ if (!consume_current && ++rec_level > max_rec) {
+ msg_err_css("max nesting reached, ignore style");
+ error = css_parse_error(css_parse_error_type::PARSE_ERROR_BAD_NESTING,
+ "maximum nesting has reached when parsing simple block value");
+ return false;
+ }
+
+ if (!consume_current) {
+ block = std::make_unique<css_consumed_block>(
+ css_consumed_block::parser_tag_type::css_simple_block);
+ }
+
+
+ while (ret && !eof) {
+ auto next_token = tokeniser->next_token();
+
+ if (next_token.type == expected_end) {
+ break;
+ }
+
+ switch (next_token.type) {
+ case css_parser_token::token_type::eof_token:
+ eof = true;
+ break;
+ case css_parser_token::token_type::whitespace_token:
+ /* Ignore whitespaces */
+ break;
+ default:
+ tokeniser->pushback_token(next_token);
+ ret = component_value_consumer(consume_current ? top : block);
+ break;
+ }
+ }
+
+ if (!consume_current && ret) {
+ msg_debug_css("attached node 'simple block' rule %s; length=%d",
+ block->token_type_str(), (int) block->size());
+ top->attach_block(std::move(block));
+ }
+
+ if (!consume_current) {
+ --rec_level;
+ }
+
+ return ret;
+}
+
+auto css_parser::qualified_rule_consumer(std::unique_ptr<css_consumed_block> &top) -> bool
+{
+ msg_debug_css("consume qualified block; top block: %s, recursion level %d",
+ top->token_type_str(), rec_level);
+
+ if (++rec_level > max_rec) {
+ msg_err_css("max nesting reached, ignore style");
+ error = css_parse_error(css_parse_error_type::PARSE_ERROR_BAD_NESTING,
+ "maximum nesting has reached when parsing qualified rule value");
+ return false;
+ }
+
+ auto ret = true, want_more = true;
+ auto block = std::make_unique<css_consumed_block>(
+ css_consumed_block::parser_tag_type::css_qualified_rule);
+
+ while (ret && want_more && !eof) {
+ auto next_token = tokeniser->next_token();
+ switch (next_token.type) {
+ case css_parser_token::token_type::eof_token:
+ eof = true;
+ break;
+ case css_parser_token::token_type::cdo_token:
+ case css_parser_token::token_type::cdc_token:
+ if (top->tag == css_consumed_block::parser_tag_type::css_top_block) {
+ /* Ignore */
+ ret = true;
+ }
+ else {
+ }
+ break;
+ case css_parser_token::token_type::ocurlbrace_token:
+ ret = simple_block_consumer(block,
+ css_parser_token::token_type::ecurlbrace_token, false);
+ want_more = false;
+ break;
+ case css_parser_token::token_type::whitespace_token:
+ /* Ignore whitespaces */
+ break;
+ default:
+ tokeniser->pushback_token(next_token);
+ ret = component_value_consumer(block);
+ break;
+ };
+ }
+
+ if (ret) {
+ if (top->tag == css_consumed_block::parser_tag_type::css_top_block) {
+ msg_debug_css("attached node qualified rule %s; length=%d",
+ block->token_type_str(), (int) block->size());
+ top->attach_block(std::move(block));
+ }
+ }
+
+ --rec_level;
+
+ return ret;
+}
+
+auto css_parser::at_rule_consumer(std::unique_ptr<css_consumed_block> &top) -> bool
+{
+ msg_debug_css("consume at-rule block; top block: %s, recursion level %d",
+ top->token_type_str(), rec_level);
+
+ if (++rec_level > max_rec) {
+ msg_err_css("max nesting reached, ignore style");
+ error = css_parse_error(css_parse_error_type::PARSE_ERROR_BAD_NESTING,
+ "maximum nesting has reached when parsing at keyword");
+ return false;
+ }
+
+ auto ret = true, want_more = true;
+ auto block = std::make_unique<css_consumed_block>(
+ css_consumed_block::parser_tag_type::css_at_rule);
+
+ while (ret && want_more && !eof) {
+ auto next_token = tokeniser->next_token();
+ switch (next_token.type) {
+ case css_parser_token::token_type::eof_token:
+ eof = true;
+ break;
+ case css_parser_token::token_type::cdo_token:
+ case css_parser_token::token_type::cdc_token:
+ if (top->tag == css_consumed_block::parser_tag_type::css_top_block) {
+ /* Ignore */
+ ret = true;
+ }
+ else {
+ }
+ break;
+ case css_parser_token::token_type::ocurlbrace_token:
+ ret = simple_block_consumer(block,
+ css_parser_token::token_type::ecurlbrace_token, false);
+ want_more = false;
+ break;
+ case css_parser_token::token_type::whitespace_token:
+ /* Ignore whitespaces */
+ break;
+ case css_parser_token::token_type::semicolon_token:
+ want_more = false;
+ break;
+ default:
+ tokeniser->pushback_token(next_token);
+ ret = component_value_consumer(block);
+ break;
+ };
+ }
+
+ if (ret) {
+ if (top->tag == css_consumed_block::parser_tag_type::css_top_block) {
+ msg_debug_css("attached node qualified rule %s; length=%d",
+ block->token_type_str(), (int) block->size());
+ top->attach_block(std::move(block));
+ }
+ }
+
+ --rec_level;
+
+ return ret;
+}
+
+auto css_parser::component_value_consumer(std::unique_ptr<css_consumed_block> &top) -> bool
+{
+ auto ret = true, need_more = true;
+ std::unique_ptr<css_consumed_block> block;
+
+ msg_debug_css("consume component block; top block: %s, recursion level %d",
+ top->token_type_str(), rec_level);
+
+ if (++rec_level > max_rec) {
+ error = css_parse_error(css_parse_error_type::PARSE_ERROR_BAD_NESTING,
+ "maximum nesting has reached when parsing component value");
+ return false;
+ }
+
+ while (ret && need_more && !eof) {
+ auto next_token = tokeniser->next_token();
+
+ switch (next_token.type) {
+ case css_parser_token::token_type::eof_token:
+ eof = true;
+ break;
+ case css_parser_token::token_type::ocurlbrace_token:
+ block = std::make_unique<css_consumed_block>(
+ css_consumed_block::parser_tag_type::css_simple_block);
+ ret = simple_block_consumer(block,
+ css_parser_token::token_type::ecurlbrace_token,
+ true);
+ need_more = false;
+ break;
+ case css_parser_token::token_type::obrace_token:
+ block = std::make_unique<css_consumed_block>(
+ css_consumed_block::parser_tag_type::css_simple_block);
+ ret = simple_block_consumer(block,
+ css_parser_token::token_type::ebrace_token,
+ true);
+ need_more = false;
+ break;
+ case css_parser_token::token_type::osqbrace_token:
+ block = std::make_unique<css_consumed_block>(
+ css_consumed_block::parser_tag_type::css_simple_block);
+ ret = simple_block_consumer(block,
+ css_parser_token::token_type::esqbrace_token,
+ true);
+ need_more = false;
+ break;
+ case css_parser_token::token_type::whitespace_token:
+ /* Ignore whitespaces */
+ break;
+ case css_parser_token::token_type::function_token: {
+ need_more = false;
+ block = std::make_unique<css_consumed_block>(
+ css_consumed_block::parser_tag_type::css_function,
+ std::move(next_token));
+
+ /* Consume the rest */
+ ret = function_consumer(block);
+ break;
+ }
+ default:
+ block = std::make_unique<css_consumed_block>(
+ css_consumed_block::parser_tag_type::css_component,
+ std::move(next_token));
+ need_more = false;
+ break;
+ }
+ }
+
+ if (ret && block) {
+ msg_debug_css("attached node component rule %s; length=%d",
+ block->token_type_str(), (int) block->size());
+ top->attach_block(std::move(block));
+ }
+
+ --rec_level;
+
+ return ret;
+}
+
+auto css_parser::consume_css_blocks(const std::string_view &sv) -> std::unique_ptr<css_consumed_block>
+{
+ tokeniser = std::make_unique<css_tokeniser>(pool, sv);
+ auto ret = true;
+
+ auto consumed_blocks =
+ std::make_unique<css_consumed_block>(css_consumed_block::parser_tag_type::css_top_block);
+
+ while (!eof && ret) {
+ auto next_token = tokeniser->next_token();
+
+ switch (next_token.type) {
+ case css_parser_token::token_type::whitespace_token:
+ /* Ignore whitespaces */
+ break;
+ case css_parser_token::token_type::eof_token:
+ eof = true;
+ break;
+ case css_parser_token::token_type::at_keyword_token:
+ tokeniser->pushback_token(next_token);
+ ret = at_rule_consumer(consumed_blocks);
+ break;
+ default:
+ tokeniser->pushback_token(next_token);
+ ret = qualified_rule_consumer(consumed_blocks);
+ break;
+ }
+ }
+
+ tokeniser.reset(nullptr); /* No longer needed */
+
+ return consumed_blocks;
+}
+
+auto css_parser::consume_css_rule(const std::string_view &sv) -> std::unique_ptr<css_consumed_block>
+{
+ tokeniser = std::make_unique<css_tokeniser>(pool, sv);
+ auto ret = true;
+
+ auto rule_block =
+ std::make_unique<css_consumed_block>(css_consumed_block::parser_tag_type::css_simple_block);
+
+ while (!eof && ret) {
+ auto next_token = tokeniser->next_token();
+
+ switch (next_token.type) {
+ case css_parser_token::token_type::eof_token:
+ eof = true;
+ break;
+ case css_parser_token::token_type::whitespace_token:
+ /* Ignore whitespaces */
+ break;
+ default:
+ tokeniser->pushback_token(next_token);
+ ret = component_value_consumer(rule_block);
+ break;
+ }
+ }
+
+ tokeniser.reset(nullptr); /* No longer needed */
+
+ return rule_block;
+}
+
+std::optional<css_parse_error>
+css_parser::consume_input(const std::string_view &sv)
+{
+ auto &&consumed_blocks = consume_css_blocks(sv);
+ const auto &rules = consumed_blocks->get_blocks_or_empty();
+
+ if (rules.empty()) {
+ if (error.type == css_parse_error_type::PARSE_ERROR_NO_ERROR) {
+ return css_parse_error(css_parse_error_type::PARSE_ERROR_EMPTY,
+ "no css rules consumed");
+ }
+ else {
+ return error;
+ }
+ }
+
+ if (!style_object) {
+ style_object = std::make_shared<css_style_sheet>(pool);
+ }
+
+ for (auto &&rule: rules) {
+ /*
+ * For now, we do not need any of the at rules, so we can safely ignore them
+ */
+ auto &&children = rule->get_blocks_or_empty();
+
+ if (children.size() > 1 &&
+ children[0]->tag == css_consumed_block::parser_tag_type::css_component) {
+ auto simple_block = std::find_if(children.begin(), children.end(),
+ [](auto &bl) {
+ return bl->tag == css_consumed_block::parser_tag_type::css_simple_block;
+ });
+
+ if (simple_block != children.end()) {
+ /*
+ * We have a component and a simple block,
+ * so we can parse a selector and then extract
+ * declarations from a simple block
+ */
+
+ /* First, tag all components as preamble */
+ auto selector_it = children.cbegin();
+
+ auto selector_token_functor = [&selector_it, &simple_block](void)
+ -> const css_consumed_block & {
+ for (;;) {
+ if (selector_it == simple_block) {
+ return css_parser_eof_block;
+ }
+
+ const auto &ret = (*selector_it);
+
+ ++selector_it;
+
+ return *ret;
+ }
+ };
+
+ auto selectors_vec = process_selector_tokens(pool, selector_token_functor);
+
+ if (selectors_vec.size() > 0) {
+ msg_debug_css("processed %d selectors", (int) selectors_vec.size());
+ auto decls_it = (*simple_block)->get_blocks_or_empty().cbegin();
+ auto decls_end = (*simple_block)->get_blocks_or_empty().cend();
+ auto declaration_token_functor = [&decls_it, &decls_end](void)
+ -> const css_consumed_block & {
+ for (;;) {
+ if (decls_it == decls_end) {
+ return css_parser_eof_block;
+ }
+
+ const auto &ret = (*decls_it);
+
+ ++decls_it;
+
+ return *ret;
+ }
+ };
+
+ auto declarations_vec = process_declaration_tokens(pool,
+ declaration_token_functor);
+
+ if (declarations_vec && !declarations_vec->get_rules().empty()) {
+ msg_debug_css("processed %d rules",
+ (int) declarations_vec->get_rules().size());
+
+ for (auto &&selector: selectors_vec) {
+ style_object->add_selector_rule(std::move(selector),
+ declarations_vec);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ auto debug_str = consumed_blocks->debug_str();
+ msg_debug_css("consumed css: {%*s}", (int) debug_str.size(), debug_str.data());
+
+ return std::nullopt;
+}
+
+auto get_selectors_parser_functor(rspamd_mempool_t *pool,
+ const std::string_view &st) -> blocks_gen_functor
+{
+ css_parser parser(pool);
+
+ auto &&consumed_blocks = parser.consume_css_blocks(st);
+ const auto &rules = consumed_blocks->get_blocks_or_empty();
+
+ auto rules_it = rules.begin();
+ auto &&children = (*rules_it)->get_blocks_or_empty();
+ auto cur = children.begin();
+ auto last = children.end();
+
+ /*
+ * We use move only wrapper to state the fact that the cosumed blocks
+ * are moved into the closure, not copied.
+ * It prevents us from thinking about copies of the blocks and
+ * functors.
+ * Mutable lambda is required to copy iterators inside of the closure,
+ * as, again, it is C++ where lifetime of the objects must be explicitly
+ * transferred. On the other hand, we could move all stuff inside and remove
+ * mutable.
+ */
+ return [cur, consumed_blocks = std::move(consumed_blocks), last](void) mutable
+ -> const css_consumed_block & {
+ if (cur != last) {
+ const auto &ret = (*cur);
+
+ ++cur;
+
+ return *ret;
+ }
+
+ return css_parser_eof_block;
+ };
+}
+
+auto get_rules_parser_functor(rspamd_mempool_t *pool,
+ const std::string_view &st) -> blocks_gen_functor
+{
+ css_parser parser(pool);
+
+ auto &&consumed_blocks = parser.consume_css_rule(st);
+ const auto &rules = consumed_blocks->get_blocks_or_empty();
+
+ auto cur = rules.begin();
+ auto last = rules.end();
+
+ return [cur, consumed_blocks = std::move(consumed_blocks), last](void) mutable
+ -> const css_consumed_block & {
+ if (cur != last) {
+ const auto &ret = (*cur);
+
+ ++cur;
+
+ return *ret;
+ }
+
+ return css_parser_eof_block;
+ };
+}
+
+
+/*
+ * Wrapper for the parser
+ */
+auto parse_css(rspamd_mempool_t *pool, const std::string_view &st,
+ std::shared_ptr<css_style_sheet> &&other)
+ -> tl::expected<std::shared_ptr<css_style_sheet>, css_parse_error>
+{
+ css_parser parser(std::forward<std::shared_ptr<css_style_sheet>>(other), pool);
+ std::string_view processed_input;
+
+ if (css_parser::need_unescape(st)) {
+ processed_input = rspamd::css::unescape_css(pool, st);
+ }
+ else {
+ /* Lowercase inplace */
+ auto *nspace = rspamd_mempool_alloc_buffer(pool, st.size());
+ rspamd_str_copy_lc(st.data(), nspace, st.size());
+ processed_input = std::string_view{nspace, st.size()};
+ }
+
+ auto maybe_error = parser.consume_input(processed_input);
+ if (!maybe_error) {
+ return parser.get_object_maybe();
+ }
+
+ return tl::make_unexpected(maybe_error.value());
+}
+
+auto parse_css_declaration(rspamd_mempool_t *pool, const std::string_view &st)
+ -> rspamd::html::html_block *
+{
+ std::string_view processed_input;
+
+ if (css_parser::need_unescape(st)) {
+ processed_input = rspamd::css::unescape_css(pool, st);
+ }
+ else {
+ auto *nspace = reinterpret_cast<char *>(rspamd_mempool_alloc(pool, st.size()));
+ auto nlen = rspamd_str_copy_lc(st.data(), nspace, st.size());
+ processed_input = std::string_view{nspace, nlen};
+ }
+ auto &&res = process_declaration_tokens(pool,
+ get_rules_parser_functor(pool, processed_input));
+
+ if (res) {
+ return res->compile_to_block(pool);
+ }
+
+ return nullptr;
+}
+
+TEST_SUITE("css")
+{
+ TEST_CASE("parse colors")
+ {
+ const std::vector<const char *> cases{
+ "P { CoLoR: rgb(100%, 50%, 0%); opacity: -1; width: 1em; display: none; } /* very transparent solid orange тест */",
+ "p { color: rgb(100%, 50%, 0%); opacity: 2; display: inline; } /* very transparent solid orange */",
+ "p { color: rgb(100%, 50%, 0%); opacity: 0.5; } /* very transparent solid orange */\n",
+ "p { color: rgb(100%, 50%, 0%); opacity: 1; width: 99%; } /* very transparent solid orange */\n",
+ "p { color: rgb(100%, 50%, 0%); opacity: 10%; width: 99%; } /* very transparent solid orange */\n",
+ "p { color: rgb(100%, 50%, 0%); opacity: 10%; width: 100px; } /* very transparent solid orange */\n",
+ "p { color: rgb(100%, 50%, 0%); opacity: 10% } /* very transparent solid orange */\n",
+ "* { color: hsl(0, 100%, 50%) !important } /* red */\n",
+ "* { color: hsl(120, 100%, 50%) important } /* lime */\n",
+ "* { color: hsl(120, 100%, 25%) } /* dark green */\n",
+ "* { color: hsl(120, 100%, 75%) } /* light green */\n",
+ "* { color: hsl(120, 75%, 75%) } /* pastel green, and so on */\n",
+ "em { color: #f00 } /* #rgb */\n",
+ "em { color: #ff0000 } /* #rrggbb */\n",
+ "em { color: rgb(255,0,0) }\n",
+ "em { color: rgb(100%, 0%, 0%) }\n",
+ "body {color: black; background: white }\n",
+ "h1 { color: maroon }\n",
+ "h2 { color: olive }\n",
+ "em { color: rgb(255,0,0) } /* integer range 0 - 255 */\n",
+ "em { color: rgb(300,0,0) } /* clipped to rgb(255,0,0) */\n",
+ "em { color: rgb(255,-10,0) } /* clipped to rgb(255,0,0) */\n",
+ "em { color: rgb(110%, 0%, 0%) } /* clipped to rgb(100%,0%,0%) */\n",
+ "em { color: rgb(255,0,0) } /* integer range 0 - 255 */\n",
+ "em { color: rgba(255,0,0,1) /* the same, with explicit opacity of 1 */\n",
+ "em { color: rgb(100%,0%,0%) } /* float range 0.0% - 100.0% */\n",
+ "em { color: rgba(100%,0%,0%,1) } /* the same, with explicit opacity of 1 */\n",
+ "p { color: rgba(0,0,255,0.5) } /* semi-transparent solid blue */\n",
+ "p { color: rgba(100%, 50%, 0%, 0.1) } /* very transparent solid orange */",
+ ".chat-icon[_ng-cnj-c0]::before{content:url(group-2.63e87cd21fbf8c966dd.svg);width:60px;height:60px;display:block}",
+ "tt{color:#1e3482}",
+ "tt{unicode-range: u+0049-u+004a,u+0020;}",
+ "@import url(https://fonts.googleapis.com/css?family=arial:300,400,7000;",
+ "tt{color:black;\v}",
+ "tt{color:black;\f}",
+ };
+
+ rspamd_mempool_t *pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ "css", 0);
+ for (const auto &c: cases) {
+ SUBCASE((std::string("parse css: ") + c).c_str())
+ {
+ CHECK(parse_css(pool, c, nullptr).value().get() != nullptr);
+ }
+ }
+
+ /* We now merge all styles together */
+ SUBCASE("merged css parse")
+ {
+ std::shared_ptr<css_style_sheet> merged;
+ for (const auto &c: cases) {
+ auto ret = parse_css(pool, c, std::move(merged));
+ merged.swap(ret.value());
+ }
+
+ CHECK(merged.get() != nullptr);
+ }
+
+ rspamd_mempool_delete(pool);
+ }
+}
+}// namespace rspamd::css
diff --git a/src/libserver/css/css_parser.hxx b/src/libserver/css/css_parser.hxx
new file mode 100644
index 0000000..d5a9671
--- /dev/null
+++ b/src/libserver/css/css_parser.hxx
@@ -0,0 +1,244 @@
+/*-
+ * Copyright 2021 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.
+ */
+
+#pragma once
+
+#ifndef RSPAMD_CSS_PARSER_HXX
+#define RSPAMD_CSS_PARSER_HXX
+
+#include <variant>
+#include <vector>
+#include <memory>
+#include <string>
+
+#include "function2/function2.hpp"
+#include "css_tokeniser.hxx"
+#include "parse_error.hxx"
+#include "contrib/expected/expected.hpp"
+#include "logger.h"
+
+/* Forward declaration */
+namespace rspamd::html {
+struct html_block;
+}
+
+namespace rspamd::css {
+
+/*
+ * Represents a consumed token by a parser
+ */
+class css_consumed_block {
+public:
+ enum class parser_tag_type : std::uint8_t {
+ css_top_block = 0,
+ css_qualified_rule,
+ css_at_rule,
+ css_simple_block,
+ css_function,
+ css_function_arg,
+ css_component,
+ css_eof_block,
+ };
+ using consumed_block_ptr = std::unique_ptr<css_consumed_block>;
+
+ struct css_function_block {
+ css_parser_token function;
+ std::vector<consumed_block_ptr> args;
+
+ css_function_block(css_parser_token &&tok)
+ : function(std::forward<css_parser_token>(tok))
+ {
+ }
+
+ auto as_string() const -> std::string_view
+ {
+ return function.get_string_or_default("");
+ }
+
+ static auto empty_function() -> const css_function_block &
+ {
+ static const css_function_block invalid(
+ css_parser_token(css_parser_token::token_type::eof_token,
+ css_parser_token_placeholder()));
+ return invalid;
+ }
+ };
+
+ css_consumed_block()
+ : tag(parser_tag_type::css_eof_block)
+ {
+ }
+ css_consumed_block(parser_tag_type tag)
+ : tag(tag)
+ {
+ if (tag == parser_tag_type::css_top_block ||
+ tag == parser_tag_type::css_qualified_rule ||
+ tag == parser_tag_type::css_simple_block) {
+ /* Pre-allocate content for known vector blocks */
+ std::vector<consumed_block_ptr> vec;
+ vec.reserve(4);
+ content = std::move(vec);
+ }
+ }
+ /* Construct a block from a single lexer token (for trivial blocks) */
+ explicit css_consumed_block(parser_tag_type tag, css_parser_token &&tok)
+ : tag(tag)
+ {
+ if (tag == parser_tag_type::css_function) {
+ content = css_function_block{std::move(tok)};
+ }
+ else {
+ content = std::move(tok);
+ }
+ }
+
+ /* Attach a new block to the compound block, consuming block inside */
+ auto attach_block(consumed_block_ptr &&block) -> bool;
+ /* Attach a new argument to the compound function block, consuming block inside */
+ auto add_function_argument(consumed_block_ptr &&block) -> bool;
+
+ auto assign_token(css_parser_token &&tok) -> void
+ {
+ content = std::move(tok);
+ }
+
+ /* Empty blocks used to avoid type checks in loops */
+ const inline static std::vector<consumed_block_ptr> empty_block_vec{};
+
+ auto is_blocks_vec() const -> bool
+ {
+ return (std::holds_alternative<std::vector<consumed_block_ptr>>(content));
+ }
+
+ auto get_blocks_or_empty() const -> const std::vector<consumed_block_ptr> &
+ {
+ if (is_blocks_vec()) {
+ return std::get<std::vector<consumed_block_ptr>>(content);
+ }
+
+ return empty_block_vec;
+ }
+
+ auto is_token() const -> bool
+ {
+ return (std::holds_alternative<css_parser_token>(content));
+ }
+
+ auto get_token_or_empty() const -> const css_parser_token &
+ {
+ if (is_token()) {
+ return std::get<css_parser_token>(content);
+ }
+
+ return css_parser_eof_token();
+ }
+
+ auto is_function() const -> bool
+ {
+ return (std::holds_alternative<css_function_block>(content));
+ }
+
+ auto get_function_or_invalid() const -> const css_function_block &
+ {
+ if (is_function()) {
+ return std::get<css_function_block>(content);
+ }
+
+ return css_function_block::empty_function();
+ }
+
+ auto size() const -> std::size_t
+ {
+ auto ret = 0;
+
+ std::visit([&](auto &arg) {
+ using T = std::decay_t<decltype(arg)>;
+
+ if constexpr (std::is_same_v<T, std::vector<consumed_block_ptr>>) {
+ /* Array of blocks */
+ ret = arg.size();
+ }
+ else if constexpr (std::is_same_v<T, std::monostate>) {
+ /* Empty block */
+ ret = 0;
+ }
+ else {
+ /* Single element block */
+ ret = 1;
+ }
+ },
+ content);
+
+ return ret;
+ }
+
+ auto is_eof() -> bool
+ {
+ return tag == parser_tag_type::css_eof_block;
+ }
+
+ /* Debug methods */
+ auto token_type_str(void) const -> const char *;
+ auto debug_str(void) -> std::string;
+
+public:
+ parser_tag_type tag;
+
+private:
+ std::variant<std::monostate,
+ std::vector<consumed_block_ptr>,
+ css_parser_token,
+ css_function_block>
+ content;
+};
+
+extern const css_consumed_block css_parser_eof_block;
+
+using blocks_gen_functor = fu2::unique_function<const css_consumed_block &(void)>;
+
+class css_style_sheet;
+/*
+ * Update the existing stylesheet with another stylesheet
+ */
+auto parse_css(rspamd_mempool_t *pool, const std::string_view &st,
+ std::shared_ptr<css_style_sheet> &&other)
+ -> tl::expected<std::shared_ptr<css_style_sheet>, css_parse_error>;
+
+/*
+ * Creates a functor to consume css selectors sequence
+ */
+auto get_selectors_parser_functor(rspamd_mempool_t *pool,
+ const std::string_view &st) -> blocks_gen_functor;
+
+/*
+ * Creates a functor to process a rule definition (e.g. from embedded style tag for
+ * an element)
+ */
+auto get_rules_parser_functor(rspamd_mempool_t *pool,
+ const std::string_view &st) -> blocks_gen_functor;
+
+/**
+ * Parses a css declaration (e.g. embedded css and returns a completed html block)
+ * @param pool
+ * @param st
+ * @return
+ */
+auto parse_css_declaration(rspamd_mempool_t *pool, const std::string_view &st)
+ -> rspamd::html::html_block *;
+
+}// namespace rspamd::css
+
+#endif//RSPAMD_CSS_PARSER_HXX
diff --git a/src/libserver/css/css_property.cxx b/src/libserver/css/css_property.cxx
new file mode 100644
index 0000000..1557109
--- /dev/null
+++ b/src/libserver/css/css_property.cxx
@@ -0,0 +1,69 @@
+/*-
+ * Copyright 2021 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 "css_property.hxx"
+#include "frozen/unordered_map.h"
+#include "frozen/string.h"
+#include "libutil/cxx/util.hxx"
+
+namespace rspamd::css {
+
+constexpr const auto prop_names_map = frozen::make_unordered_map<frozen::string, css_property_type>({
+ {"font", css_property_type::PROPERTY_FONT},
+ {"font-color", css_property_type::PROPERTY_FONT_COLOR},
+ {"font-size", css_property_type::PROPERTY_FONT_SIZE},
+ {"color", css_property_type::PROPERTY_COLOR},
+ {"bgcolor", css_property_type::PROPERTY_BGCOLOR},
+ {"background-color", css_property_type::PROPERTY_BGCOLOR},
+ {"background", css_property_type::PROPERTY_BACKGROUND},
+ {"height", css_property_type::PROPERTY_HEIGHT},
+ {"width", css_property_type::PROPERTY_WIDTH},
+ {"display", css_property_type::PROPERTY_DISPLAY},
+ {"visibility", css_property_type::PROPERTY_VISIBILITY},
+ {"opacity", css_property_type::PROPERTY_OPACITY},
+});
+
+/* Ensure that we have all cases listed */
+static_assert(prop_names_map.size() >= static_cast<int>(css_property_type::PROPERTY_NYI));
+
+auto token_string_to_property(const std::string_view &inp)
+ -> css_property_type
+{
+
+ css_property_type ret = css_property_type::PROPERTY_NYI;
+
+ auto known_type = find_map(prop_names_map, inp);
+
+ if (known_type) {
+ ret = known_type.value().get();
+ }
+
+ return ret;
+}
+
+auto css_property::from_token(const css_parser_token &tok)
+ -> tl::expected<css_property, css_parse_error>
+{
+ if (tok.type == css_parser_token::token_type::ident_token) {
+ auto sv = tok.get_string_or_default("");
+
+ return css_property{token_string_to_property(sv), css_property_flag::FLAG_NORMAL};
+ }
+
+ return tl::unexpected{css_parse_error(css_parse_error_type::PARSE_ERROR_NYI)};
+}
+
+}// namespace rspamd::css
diff --git a/src/libserver/css/css_property.hxx b/src/libserver/css/css_property.hxx
new file mode 100644
index 0000000..9661222
--- /dev/null
+++ b/src/libserver/css/css_property.hxx
@@ -0,0 +1,172 @@
+/*-
+ * Copyright 2021 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.
+ */
+#pragma once
+
+#ifndef RSPAMD_CSS_PROPERTY_HXX
+#define RSPAMD_CSS_PROPERTY_HXX
+
+#include <string>
+#include "css_tokeniser.hxx"
+#include "parse_error.hxx"
+#include "contrib/expected/expected.hpp"
+
+namespace rspamd::css {
+
+/*
+ * To be extended with properties that are interesting from the email
+ * point of view
+ */
+enum class css_property_type : std::uint16_t {
+ PROPERTY_FONT = 0,
+ PROPERTY_FONT_COLOR,
+ PROPERTY_FONT_SIZE,
+ PROPERTY_COLOR,
+ PROPERTY_BGCOLOR,
+ PROPERTY_BACKGROUND,
+ PROPERTY_HEIGHT,
+ PROPERTY_WIDTH,
+ PROPERTY_DISPLAY,
+ PROPERTY_VISIBILITY,
+ PROPERTY_OPACITY,
+ PROPERTY_NYI,
+};
+
+enum class css_property_flag : std::uint16_t {
+ FLAG_NORMAL,
+ FLAG_IMPORTANT,
+ FLAG_NOT_IMPORTANT
+};
+
+struct alignas(int) css_property {
+ css_property_type type;
+ css_property_flag flag;
+
+ css_property(css_property_type t, css_property_flag fl = css_property_flag::FLAG_NORMAL)
+ : type(t), flag(fl)
+ {
+ }
+ static tl::expected<css_property, css_parse_error> from_token(
+ const css_parser_token &tok);
+
+ constexpr auto to_string(void) const -> const char *
+ {
+ const char *ret = "nyi";
+
+ switch (type) {
+ case css_property_type::PROPERTY_FONT:
+ ret = "font";
+ break;
+ case css_property_type::PROPERTY_FONT_COLOR:
+ ret = "font-color";
+ break;
+ case css_property_type::PROPERTY_FONT_SIZE:
+ ret = "font-size";
+ break;
+ case css_property_type::PROPERTY_COLOR:
+ ret = "color";
+ break;
+ case css_property_type::PROPERTY_BGCOLOR:
+ ret = "bgcolor";
+ break;
+ case css_property_type::PROPERTY_BACKGROUND:
+ ret = "background";
+ break;
+ case css_property_type::PROPERTY_HEIGHT:
+ ret = "height";
+ break;
+ case css_property_type::PROPERTY_WIDTH:
+ ret = "width";
+ break;
+ case css_property_type::PROPERTY_DISPLAY:
+ ret = "display";
+ break;
+ case css_property_type::PROPERTY_VISIBILITY:
+ ret = "visibility";
+ break;
+ case css_property_type::PROPERTY_OPACITY:
+ ret = "opacity";
+ break;
+ default:
+ break;
+ }
+
+ return ret;
+ }
+
+ /* Helpers to define which values are valid for which properties */
+ auto is_color(void) const -> bool
+ {
+ return type == css_property_type::PROPERTY_COLOR ||
+ type == css_property_type::PROPERTY_BACKGROUND ||
+ type == css_property_type::PROPERTY_BGCOLOR ||
+ type == css_property_type::PROPERTY_FONT_COLOR ||
+ type == css_property_type::PROPERTY_FONT;
+ }
+ auto is_dimension(void) const -> bool
+ {
+ return type == css_property_type::PROPERTY_HEIGHT ||
+ type == css_property_type::PROPERTY_WIDTH ||
+ type == css_property_type::PROPERTY_FONT_SIZE ||
+ type == css_property_type::PROPERTY_FONT;
+ }
+
+ auto is_normal_number(void) const -> bool
+ {
+ return type == css_property_type::PROPERTY_OPACITY;
+ }
+
+ auto is_display(void) const -> bool
+ {
+ return type == css_property_type::PROPERTY_DISPLAY;
+ }
+
+ auto is_visibility(void) const -> bool
+ {
+ return type == css_property_type::PROPERTY_VISIBILITY;
+ }
+
+ auto operator==(const css_property &other) const
+ {
+ return type == other.type;
+ }
+};
+
+
+}// namespace rspamd::css
+
+/* Make properties hashable */
+namespace std {
+template<>
+class hash<rspamd::css::css_property> {
+public:
+ using is_avalanching = void;
+ /* Mix bits to provide slightly better distribution but being constexpr */
+ constexpr size_t operator()(const rspamd::css::css_property &prop) const
+ {
+ std::size_t key = 0xdeadbeef ^ static_cast<std::size_t>(prop.type);
+ key = (~key) + (key << 21);
+ key = key ^ (key >> 24);
+ key = (key + (key << 3)) + (key << 8);
+ key = key ^ (key >> 14);
+ key = (key + (key << 2)) + (key << 4);
+ key = key ^ (key >> 28);
+ key = key + (key << 31);
+ return key;
+ }
+};
+}// namespace std
+
+#endif//RSPAMD_CSS_PROPERTY_HXX \ No newline at end of file
diff --git a/src/libserver/css/css_rule.cxx b/src/libserver/css/css_rule.cxx
new file mode 100644
index 0000000..4e33ac7
--- /dev/null
+++ b/src/libserver/css/css_rule.cxx
@@ -0,0 +1,531 @@
+/*-
+ * Copyright 2021 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 "css_rule.hxx"
+#include "css.hxx"
+#include "libserver/html/html_block.hxx"
+#include <limits>
+
+#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
+#include "doctest/doctest.h"
+
+namespace rspamd::css {
+
+/* Class methods */
+void css_rule::override_values(const css_rule &other)
+{
+ int bits = 0;
+ /* Ensure that our bitset is large enough */
+ static_assert(1 << std::variant_size_v<decltype(css_value::value)> <
+ std::numeric_limits<int>::max());
+
+ for (const auto &v: values) {
+ bits |= static_cast<int>(1 << v.value.index());
+ }
+
+ for (const auto &ov: other.values) {
+ if (isset(&bits, static_cast<int>(1 << ov.value.index()))) {
+ /* We need to override the existing value */
+ /*
+ * The algorithm is not very efficient,
+ * so we need to sort the values first and have a O(N) algorithm
+ * On the other hand, values vectors are usually limited to the
+ * number of elements about less then 10, so this O(N^2) algorithm
+ * is probably ok here
+ */
+ for (auto &v: values) {
+ if (v.value.index() == ov.value.index()) {
+ v = ov;
+ }
+ }
+ }
+ }
+
+ /* Copy only not set values */
+ std::copy_if(other.values.begin(), other.values.end(), std::back_inserter(values),
+ [&bits](const auto &elt) -> bool {
+ return (bits & (1 << static_cast<int>(elt.value.index()))) == 0;
+ });
+}
+
+void css_rule::merge_values(const css_rule &other)
+{
+ unsigned int bits = 0;
+
+ for (const auto &v: values) {
+ bits |= 1 << v.value.index();
+ }
+
+ /* Copy only not set values */
+ std::copy_if(other.values.begin(), other.values.end(), std::back_inserter(values),
+ [&bits](const auto &elt) -> bool {
+ return (bits & (1 << elt.value.index())) == 0;
+ });
+}
+
+auto css_declarations_block::add_rule(rule_shared_ptr rule) -> bool
+{
+ auto it = rules.find(rule);
+ auto &&remote_prop = rule->get_prop();
+ auto ret = true;
+
+ if (rule->get_values().size() == 0) {
+ /* Ignore rules with no values */
+ return false;
+ }
+
+ if (it != rules.end()) {
+ auto &&local_rule = *it;
+ auto &&local_prop = local_rule->get_prop();
+
+ if (local_prop.flag == css_property_flag::FLAG_IMPORTANT) {
+ if (remote_prop.flag == css_property_flag::FLAG_IMPORTANT) {
+ local_rule->override_values(*rule);
+ }
+ else {
+ /* Override remote not important over local important */
+ local_rule->merge_values(*rule);
+ }
+ }
+ else if (local_prop.flag == css_property_flag::FLAG_NOT_IMPORTANT) {
+ if (remote_prop.flag == css_property_flag::FLAG_NOT_IMPORTANT) {
+ local_rule->override_values(*rule);
+ }
+ else {
+ /* Override local not important over important */
+ local_rule->merge_values(*rule);
+ }
+ }
+ else {
+ if (remote_prop.flag == css_property_flag::FLAG_IMPORTANT) {
+ /* Override with remote */
+ local_rule->override_values(*rule);
+ }
+ else if (remote_prop.flag == css_property_flag::FLAG_NOT_IMPORTANT) {
+ /* Ignore remote not important over local normal */
+ ret = false;
+ }
+ else {
+ /* Merge both */
+ local_rule->merge_values(*rule);
+ }
+ }
+ }
+ else {
+ rules.insert(std::move(rule));
+ }
+
+ return ret;
+}
+
+}// namespace rspamd::css
+
+namespace rspamd::css {
+
+/* Static functions */
+
+static auto
+allowed_property_value(const css_property &prop, const css_consumed_block &parser_block)
+ -> std::optional<css_value>
+{
+ if (prop.is_color()) {
+ if (parser_block.is_token()) {
+ /* A single token */
+ const auto &tok = parser_block.get_token_or_empty();
+
+ if (tok.type == css_parser_token::token_type::hash_token) {
+ return css_value::maybe_color_from_hex(tok.get_string_or_default(""));
+ }
+ else if (tok.type == css_parser_token::token_type::ident_token) {
+ auto &&ret = css_value::maybe_color_from_string(tok.get_string_or_default(""));
+
+ return ret;
+ }
+ }
+ else if (parser_block.is_function()) {
+ const auto &func = parser_block.get_function_or_invalid();
+
+ auto &&ret = css_value::maybe_color_from_function(func);
+ return ret;
+ }
+ }
+ if (prop.is_dimension()) {
+ if (parser_block.is_token()) {
+ /* A single token */
+ const auto &tok = parser_block.get_token_or_empty();
+
+ if (tok.type == css_parser_token::token_type::number_token) {
+ return css_value::maybe_dimension_from_number(tok);
+ }
+ }
+ }
+ if (prop.is_display()) {
+ if (parser_block.is_token()) {
+ /* A single token */
+ const auto &tok = parser_block.get_token_or_empty();
+
+ if (tok.type == css_parser_token::token_type::ident_token) {
+ return css_value::maybe_display_from_string(tok.get_string_or_default(""));
+ }
+ }
+ }
+ if (prop.is_visibility()) {
+ if (parser_block.is_token()) {
+ /* A single token */
+ const auto &tok = parser_block.get_token_or_empty();
+
+ if (tok.type == css_parser_token::token_type::ident_token) {
+ return css_value::maybe_display_from_string(tok.get_string_or_default(""));
+ }
+ }
+ }
+ if (prop.is_normal_number()) {
+ if (parser_block.is_token()) {
+ /* A single token */
+ const auto &tok = parser_block.get_token_or_empty();
+
+ if (tok.type == css_parser_token::token_type::number_token) {
+ return css_value{tok.get_normal_number_or_default(0)};
+ }
+ }
+ }
+
+ return std::nullopt;
+}
+
+auto process_declaration_tokens(rspamd_mempool_t *pool,
+ blocks_gen_functor &&next_block_functor)
+ -> css_declarations_block_ptr
+{
+ css_declarations_block_ptr ret;
+ bool can_continue = true;
+ css_property cur_property{css_property_type::PROPERTY_NYI,
+ css_property_flag::FLAG_NORMAL};
+ static const css_property bad_property{css_property_type::PROPERTY_NYI,
+ css_property_flag::FLAG_NORMAL};
+ std::shared_ptr<css_rule> cur_rule;
+
+ enum {
+ parse_property,
+ parse_value,
+ ignore_value, /* For unknown properties */
+ } state = parse_property;
+
+ auto seen_not = false;
+ ret = std::make_shared<css_declarations_block>();
+
+ while (can_continue) {
+ const auto &next_tok = next_block_functor();
+
+ switch (next_tok.tag) {
+ case css_consumed_block::parser_tag_type::css_component:
+ /* Component can be a property or a compound list of values */
+ if (state == parse_property) {
+ cur_property = css_property::from_token(next_tok.get_token_or_empty())
+ .value_or(bad_property);
+
+ if (cur_property.type == css_property_type::PROPERTY_NYI) {
+ state = ignore_value;
+ /* Ignore everything till ; */
+ continue;
+ }
+
+ msg_debug_css("got css property: %s", cur_property.to_string());
+
+ /* We now expect colon block */
+ const auto &expect_colon_block = next_block_functor();
+
+ if (expect_colon_block.tag != css_consumed_block::parser_tag_type::css_component) {
+ state = ignore_value; /* Ignore up to the next rule */
+ }
+ else {
+ const auto &expect_colon_tok = expect_colon_block.get_token_or_empty();
+
+ if (expect_colon_tok.type != css_parser_token::token_type::colon_token) {
+ msg_debug_css("invalid rule, no colon after property");
+ state = ignore_value; /* Ignore up to the next rule */
+ }
+ else {
+ state = parse_value;
+ cur_rule = std::make_shared<css_rule>(cur_property);
+ }
+ }
+ }
+ else if (state == parse_value) {
+ /* Check semicolon */
+ if (next_tok.is_token()) {
+ const auto &parser_tok = next_tok.get_token_or_empty();
+
+ if (parser_tok.type == css_parser_token::token_type::semicolon_token && cur_rule) {
+ ret->add_rule(std::move(cur_rule));
+ state = parse_property;
+ seen_not = false;
+ continue;
+ }
+ else if (parser_tok.type == css_parser_token::token_type::delim_token) {
+ if (parser_tok.get_string_or_default("") == "!") {
+ /* Probably something like !important */
+ seen_not = true;
+ }
+ }
+ else if (parser_tok.type == css_parser_token::token_type::ident_token) {
+ if (parser_tok.get_string_or_default("") == "important") {
+ if (seen_not) {
+ msg_debug_css("add !important flag to property %s",
+ cur_property.to_string());
+ cur_property.flag = css_property_flag::FLAG_NOT_IMPORTANT;
+ }
+ else {
+ msg_debug_css("add important flag to property %s",
+ cur_property.to_string());
+ cur_property.flag = css_property_flag::FLAG_IMPORTANT;
+ }
+
+ seen_not = false;
+
+ continue;
+ }
+ else {
+ seen_not = false;
+ }
+ }
+ }
+
+ auto maybe_value = allowed_property_value(cur_property, next_tok);
+
+ if (maybe_value) {
+ msg_debug_css("added value %s to the property %s",
+ maybe_value.value().debug_str().c_str(),
+ cur_property.to_string());
+ cur_rule->add_value(maybe_value.value());
+ }
+ }
+ else {
+ /* Ignore all till ; */
+ if (next_tok.is_token()) {
+ const auto &parser_tok = next_tok.get_token_or_empty();
+
+ if (parser_tok.type == css_parser_token::token_type::semicolon_token) {
+ state = parse_property;
+ }
+ }
+ }
+ break;
+ case css_consumed_block::parser_tag_type::css_function:
+ if (state == parse_value) {
+ auto maybe_value = allowed_property_value(cur_property, next_tok);
+
+ if (maybe_value && cur_rule) {
+ msg_debug_css("added value %s to the property %s",
+ maybe_value.value().debug_str().c_str(),
+ cur_property.to_string());
+ cur_rule->add_value(maybe_value.value());
+ }
+ }
+ break;
+ case css_consumed_block::parser_tag_type::css_eof_block:
+ if (state == parse_value) {
+ ret->add_rule(std::move(cur_rule));
+ }
+ can_continue = false;
+ break;
+ default:
+ can_continue = false;
+ break;
+ }
+ }
+
+ return ret; /* copy elision */
+}
+
+auto css_declarations_block::merge_block(const css_declarations_block &other, merge_type how) -> void
+{
+ const auto &other_rules = other.get_rules();
+
+
+ for (auto &rule: other_rules) {
+ auto &&found_it = rules.find(rule);
+
+ if (found_it != rules.end()) {
+ /* Duplicate, need to merge */
+ switch (how) {
+ case merge_type::merge_override:
+ /* Override */
+ (*found_it)->override_values(*rule);
+ break;
+ case merge_type::merge_duplicate:
+ /* Merge values */
+ add_rule(rule);
+ break;
+ case merge_type::merge_parent:
+ /* Do not merge parent rule if more specific local one is presented */
+ break;
+ }
+ }
+ else {
+ /* New property, just insert */
+ rules.insert(rule);
+ }
+ }
+}
+
+auto css_declarations_block::compile_to_block(rspamd_mempool_t *pool) const -> rspamd::html::html_block *
+{
+ auto *block = rspamd_mempool_alloc0_type(pool, rspamd::html::html_block);
+ auto opacity = -1;
+ const css_rule *font_rule = nullptr, *background_rule = nullptr;
+
+ for (const auto &rule: rules) {
+ auto prop = rule->get_prop().type;
+ const auto &vals = rule->get_values();
+
+ if (vals.empty()) {
+ continue;
+ }
+
+ switch (prop) {
+ case css_property_type::PROPERTY_VISIBILITY:
+ case css_property_type::PROPERTY_DISPLAY: {
+ auto disp = vals.back().to_display().value_or(css_display_value::DISPLAY_INLINE);
+ block->set_display(disp);
+ break;
+ }
+ case css_property_type::PROPERTY_FONT_SIZE: {
+ auto fs = vals.back().to_dimension();
+ if (fs) {
+ block->set_font_size(fs.value().dim, fs.value().is_percent);
+ }
+ }
+ case css_property_type::PROPERTY_OPACITY: {
+ opacity = vals.back().to_number().value_or(opacity);
+ break;
+ }
+ case css_property_type::PROPERTY_FONT_COLOR:
+ case css_property_type::PROPERTY_COLOR: {
+ auto color = vals.back().to_color();
+ if (color) {
+ block->set_fgcolor(color.value());
+ }
+ break;
+ }
+ case css_property_type::PROPERTY_BGCOLOR: {
+ auto color = vals.back().to_color();
+ if (color) {
+ block->set_bgcolor(color.value());
+ }
+ break;
+ }
+ case css_property_type::PROPERTY_HEIGHT: {
+ auto w = vals.back().to_dimension();
+ if (w) {
+ block->set_width(w.value().dim, w.value().is_percent);
+ }
+ break;
+ }
+ case css_property_type::PROPERTY_WIDTH: {
+ auto h = vals.back().to_dimension();
+ if (h) {
+ block->set_width(h.value().dim, h.value().is_percent);
+ }
+ break;
+ }
+ /* Optional attributes */
+ case css_property_type::PROPERTY_FONT:
+ font_rule = rule.get();
+ break;
+ case css_property_type::PROPERTY_BACKGROUND:
+ background_rule = rule.get();
+ break;
+ default:
+ /* Do nothing for now */
+ break;
+ }
+ }
+
+ /* Optional properties */
+ if (!(block->fg_color_mask) && font_rule) {
+ auto &vals = font_rule->get_values();
+
+ for (const auto &val: vals) {
+ auto maybe_color = val.to_color();
+
+ if (maybe_color) {
+ block->set_fgcolor(maybe_color.value());
+ }
+ }
+ }
+
+ if (!(block->font_mask) && font_rule) {
+ auto &vals = font_rule->get_values();
+
+ for (const auto &val: vals) {
+ auto maybe_dim = val.to_dimension();
+
+ if (maybe_dim) {
+ block->set_font_size(maybe_dim.value().dim, maybe_dim.value().is_percent);
+ }
+ }
+ }
+
+ if (!(block->bg_color_mask) && background_rule) {
+ auto &vals = background_rule->get_values();
+
+ for (const auto &val: vals) {
+ auto maybe_color = val.to_color();
+
+ if (maybe_color) {
+ block->set_bgcolor(maybe_color.value());
+ }
+ }
+ }
+
+ return block;
+}
+
+void css_rule::add_value(const css_value &value)
+{
+ values.push_back(value);
+}
+
+
+TEST_SUITE("css")
+{
+ TEST_CASE("simple css rules")
+ {
+ const std::vector<std::pair<const char *, std::vector<css_property>>> cases{
+ {"font-size:12.0pt;line-height:115%",
+ {css_property(css_property_type::PROPERTY_FONT_SIZE)}},
+ {"font-size:12.0pt;display:none",
+ {css_property(css_property_type::PROPERTY_FONT_SIZE),
+ css_property(css_property_type::PROPERTY_DISPLAY)}}};
+
+ auto *pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ "css", 0);
+
+ for (const auto &c: cases) {
+ auto res = process_declaration_tokens(pool,
+ get_rules_parser_functor(pool, c.first));
+
+ CHECK(res.get() != nullptr);
+
+ for (auto i = 0; i < c.second.size(); i++) {
+ CHECK(res->has_property(c.second[i]));
+ }
+ }
+ }
+}
+
+}// namespace rspamd::css \ No newline at end of file
diff --git a/src/libserver/css/css_rule.hxx b/src/libserver/css/css_rule.hxx
new file mode 100644
index 0000000..114b83e
--- /dev/null
+++ b/src/libserver/css/css_rule.hxx
@@ -0,0 +1,153 @@
+/*-
+ * Copyright 2021 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.
+ */
+#pragma once
+
+#ifndef RSPAMD_CSS_RULE_HXX
+#define RSPAMD_CSS_RULE_HXX
+
+#include "css_value.hxx"
+#include "css_property.hxx"
+#include "css_parser.hxx"
+#include "contrib/ankerl/unordered_dense.h"
+#include "libutil/cxx/util.hxx"
+#include "libutil/cxx/hash_util.hxx"
+#include <vector>
+#include <memory>
+
+namespace rspamd::html {
+/* Forward declaration */
+struct html_block;
+}// namespace rspamd::html
+
+namespace rspamd::css {
+
+class css_rule {
+ css_property prop;
+ using css_values_vec = std::vector<css_value>;
+ css_values_vec values;
+
+public:
+ /* We must create css rule explicitly from a property and values */
+ css_rule() = delete;
+
+ css_rule(const css_rule &other) = delete;
+
+ /* Constructors */
+ css_rule(css_rule &&other) noexcept = default;
+
+ explicit css_rule(css_property &&prop, css_values_vec &&values) noexcept
+ : prop(prop), values(std::forward<css_values_vec>(values))
+ {
+ }
+
+ explicit css_rule(const css_property &prop) noexcept
+ : prop(prop), values{}
+ {
+ }
+
+ /* Methods */
+ /* Comparison is special, as we care merely about property, not the values */
+ auto operator==(const css_rule &other) const
+ {
+ return prop == other.prop;
+ }
+
+ constexpr const css_values_vec &get_values(void) const
+ {
+ return values;
+ }
+ constexpr const css_property &get_prop(void) const
+ {
+ return prop;
+ }
+
+ /* Import values from another rules according to the importance */
+ void override_values(const css_rule &other);
+ void merge_values(const css_rule &other);
+ void add_value(const css_value &value);
+};
+
+}// namespace rspamd::css
+
+/* Make rules hashable by property */
+namespace std {
+template<>
+class hash<rspamd::css::css_rule> {
+public:
+ using is_avalanching = void;
+ constexpr auto operator()(const rspamd::css::css_rule &rule) const -> auto
+ {
+ return hash<rspamd::css::css_property>()(rule.get_prop());
+ }
+};
+
+}// namespace std
+
+namespace rspamd::css {
+
+/**
+ * Class that is designed to hold css declaration (a set of rules)
+ */
+class css_declarations_block {
+public:
+ using rule_shared_ptr = std::shared_ptr<css_rule>;
+ using rule_shared_hash = smart_ptr_hash<css_rule>;
+ using rule_shared_eq = smart_ptr_equal<css_rule>;
+ enum class merge_type {
+ merge_duplicate,
+ merge_parent,
+ merge_override
+ };
+
+ css_declarations_block() = default;
+ auto add_rule(rule_shared_ptr rule) -> bool;
+ auto merge_block(const css_declarations_block &other,
+ merge_type how = merge_type::merge_duplicate) -> void;
+ auto get_rules(void) const -> const auto &
+ {
+ return rules;
+ }
+
+ /**
+ * Returns if a declaration block has some property
+ * @param prop
+ * @return
+ */
+ auto has_property(const css_property &prop) const -> bool
+ {
+ return (rules.find(css_rule{prop}) != rules.end());
+ }
+
+ /**
+ * Compile CSS declaration to the html block
+ * @param pool used to carry memory required for html_block
+ * @return html block structure
+ */
+ auto compile_to_block(rspamd_mempool_t *pool) const -> rspamd::html::html_block *;
+
+private:
+ ankerl::unordered_dense::set<rule_shared_ptr, rule_shared_hash, rule_shared_eq> rules;
+};
+
+using css_declarations_block_ptr = std::shared_ptr<css_declarations_block>;
+
+auto process_declaration_tokens(rspamd_mempool_t *pool,
+ blocks_gen_functor &&next_token_functor)
+ -> css_declarations_block_ptr;
+
+}// namespace rspamd::css
+
+#endif//RSPAMD_CSS_RULE_HXX \ No newline at end of file
diff --git a/src/libserver/css/css_rule_parser.rl b/src/libserver/css/css_rule_parser.rl
new file mode 100644
index 0000000..e3b1876
--- /dev/null
+++ b/src/libserver/css/css_rule_parser.rl
@@ -0,0 +1,27 @@
+%%{
+ machine css_parser;
+ alphtype unsigned char;
+ include css_syntax "css_syntax.rl";
+
+ main := declaration;
+}%%
+
+%% write data;
+
+#include <cstddef>
+
+namespace rspamd::css {
+
+int
+foo (const unsigned char *data, std::size_t len)
+{
+ const unsigned char *p = data, *pe = data + len, *eof;
+ int cs;
+
+ %% write init;
+ %% write exec;
+
+ return cs;
+}
+
+} \ No newline at end of file
diff --git a/src/libserver/css/css_selector.cxx b/src/libserver/css/css_selector.cxx
new file mode 100644
index 0000000..a62ffff
--- /dev/null
+++ b/src/libserver/css/css_selector.cxx
@@ -0,0 +1,226 @@
+/*-
+ * Copyright 2021 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 "css_selector.hxx"
+#include "css.hxx"
+#include "libserver/html/html.hxx"
+#include "fmt/core.h"
+#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
+#include "doctest/doctest.h"
+
+namespace rspamd::css {
+
+auto process_selector_tokens(rspamd_mempool_t *pool,
+ blocks_gen_functor &&next_token_functor)
+ -> selectors_vec
+{
+ selectors_vec ret;
+ bool can_continue = true;
+ enum class selector_process_state {
+ selector_parse_start = 0,
+ selector_expect_ident,
+ selector_ident_consumed,
+ selector_ignore_attribute,
+ selector_ignore_function,
+ selector_ignore_combination
+ } state = selector_process_state::selector_parse_start;
+ std::unique_ptr<css_selector> cur_selector;
+
+
+ while (can_continue) {
+ const auto &next_tok = next_token_functor();
+
+ if (next_tok.tag == css_consumed_block::parser_tag_type::css_component) {
+ const auto &parser_tok = next_tok.get_token_or_empty();
+
+ if (state == selector_process_state::selector_parse_start) {
+ /*
+ * At the beginning of the parsing we can expect either
+ * delim or an ident, everything else is discarded for now
+ */
+ msg_debug_css("start consume selector");
+
+ switch (parser_tok.type) {
+ case css_parser_token::token_type::delim_token: {
+ auto delim_c = parser_tok.get_delim();
+
+ if (delim_c == '.') {
+ cur_selector = std::make_unique<css_selector>(
+ css_selector::selector_type::SELECTOR_CLASS);
+ state = selector_process_state::selector_expect_ident;
+ }
+ else if (delim_c == '#') {
+ cur_selector = std::make_unique<css_selector>(
+ css_selector::selector_type::SELECTOR_ID);
+ state = selector_process_state::selector_expect_ident;
+ }
+ else if (delim_c == '*') {
+ cur_selector = std::make_unique<css_selector>(
+ css_selector::selector_type::SELECTOR_ALL);
+ state = selector_process_state::selector_ident_consumed;
+ }
+ break;
+ }
+ case css_parser_token::token_type::ident_token: {
+ auto tag_id = html::html_tag_by_name(parser_tok.get_string_or_default(""));
+
+ if (tag_id) {
+ cur_selector = std::make_unique<css_selector>(tag_id.value());
+ }
+ state = selector_process_state::selector_ident_consumed;
+ break;
+ }
+ case css_parser_token::token_type::hash_token:
+ cur_selector = std::make_unique<css_selector>(
+ css_selector::selector_type::SELECTOR_ID);
+ cur_selector->value =
+ parser_tok.get_string_or_default("");
+ state = selector_process_state::selector_ident_consumed;
+ break;
+ default:
+ msg_debug_css("cannot consume more of a selector, invalid parser token: %s; expected start",
+ next_tok.token_type_str());
+ can_continue = false;
+ break;
+ }
+ }
+ else if (state == selector_process_state::selector_expect_ident) {
+ /*
+ * We got something like a selector start, so we expect
+ * a plain ident
+ */
+ if (parser_tok.type == css_parser_token::token_type::ident_token && cur_selector) {
+ cur_selector->value = parser_tok.get_string_or_default("");
+ state = selector_process_state::selector_ident_consumed;
+ }
+ else {
+ msg_debug_css("cannot consume more of a selector, invalid parser token: %s; expected ident",
+ next_tok.token_type_str());
+ can_continue = false;
+ }
+ }
+ else if (state == selector_process_state::selector_ident_consumed) {
+ if (parser_tok.type == css_parser_token::token_type::comma_token && cur_selector) {
+ /* Got full selector, attach it to the vector and go further */
+ msg_debug_css("attached selector: %s", cur_selector->debug_str().c_str());
+ ret.push_back(std::move(cur_selector));
+ state = selector_process_state::selector_parse_start;
+ }
+ else if (parser_tok.type == css_parser_token::token_type::semicolon_token) {
+ /* TODO: implement adjustments */
+ state = selector_process_state::selector_ignore_function;
+ }
+ else if (parser_tok.type == css_parser_token::token_type::osqbrace_token) {
+ /* TODO: implement attributes checks */
+ state = selector_process_state::selector_ignore_attribute;
+ }
+ else {
+ /* TODO: implement selectors combinations */
+ state = selector_process_state::selector_ignore_combination;
+ }
+ }
+ else {
+ /* Ignore state; ignore all till ',' token or eof token */
+ if (parser_tok.type == css_parser_token::token_type::comma_token && cur_selector) {
+ /* Got full selector, attach it to the vector and go further */
+ ret.push_back(std::move(cur_selector));
+ state = selector_process_state::selector_parse_start;
+ }
+ else {
+ auto debug_str = parser_tok.get_string_or_default("");
+ msg_debug_css("ignore token %*s", (int) debug_str.size(),
+ debug_str.data());
+ }
+ }
+ }
+ else {
+ /* End of parsing */
+ if (state == selector_process_state::selector_ident_consumed && cur_selector) {
+ msg_debug_css("attached selector: %s", cur_selector->debug_str().c_str());
+ ret.push_back(std::move(cur_selector));
+ }
+ else {
+ msg_debug_css("not attached selector, state: %d", static_cast<int>(state));
+ }
+ can_continue = false;
+ }
+ }
+
+ return ret; /* copy elision */
+}
+
+auto css_selector::debug_str() const -> std::string
+{
+ std::string ret;
+
+ if (type == selector_type::SELECTOR_ID) {
+ ret += "#";
+ }
+ else if (type == selector_type::SELECTOR_CLASS) {
+ ret += ".";
+ }
+ else if (type == selector_type::SELECTOR_ALL) {
+ ret = "*";
+
+ return ret;
+ }
+
+ std::visit([&](auto arg) -> void {
+ using T = std::decay_t<decltype(arg)>;
+
+ if constexpr (std::is_same_v<T, tag_id_t>) {
+ ret += fmt::format("tag: {}", static_cast<int>(arg));
+ }
+ else {
+ ret += arg;
+ }
+ },
+ value);
+
+ return ret;
+}
+
+TEST_SUITE("css")
+{
+ TEST_CASE("simple css selectors")
+ {
+ const std::vector<std::pair<const char *, std::vector<css_selector::selector_type>>> cases{
+ {"em", {css_selector::selector_type::SELECTOR_TAG}},
+ {"*", {css_selector::selector_type::SELECTOR_ALL}},
+ {".class", {css_selector::selector_type::SELECTOR_CLASS}},
+ {"#id", {css_selector::selector_type::SELECTOR_ID}},
+ {"em,.class,#id", {css_selector::selector_type::SELECTOR_TAG, css_selector::selector_type::SELECTOR_CLASS, css_selector::selector_type::SELECTOR_ID}},
+ };
+
+ auto *pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ "css", 0);
+
+ for (const auto &c: cases) {
+ auto res = process_selector_tokens(pool,
+ get_selectors_parser_functor(pool, c.first));
+
+ CHECK(c.second.size() == res.size());
+
+ for (auto i = 0; i < c.second.size(); i++) {
+ CHECK(res[i]->type == c.second[i]);
+ }
+ }
+
+ rspamd_mempool_delete(pool);
+ }
+}
+
+}// namespace rspamd::css
diff --git a/src/libserver/css/css_selector.hxx b/src/libserver/css/css_selector.hxx
new file mode 100644
index 0000000..65b185a
--- /dev/null
+++ b/src/libserver/css/css_selector.hxx
@@ -0,0 +1,134 @@
+/*-
+ * Copyright 2021 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.
+ */
+
+#pragma once
+
+#ifndef RSPAMD_CSS_SELECTOR_HXX
+#define RSPAMD_CSS_SELECTOR_HXX
+
+#include <variant>
+#include <string>
+#include <optional>
+#include <vector>
+#include <memory>
+
+#include "function2/function2.hpp"
+#include "parse_error.hxx"
+#include "css_parser.hxx"
+#include "libserver/html/html_tags.h"
+#include "libcryptobox/cryptobox.h"
+
+namespace rspamd::css {
+
+/*
+ * Holds a value for css selector, internal is handled by variant
+ */
+struct css_selector {
+ enum class selector_type {
+ SELECTOR_TAG, /* e.g. tr, for this value we use tag_id_t */
+ SELECTOR_CLASS, /* generic class, e.g. .class */
+ SELECTOR_ID, /* e.g. #id */
+ SELECTOR_ALL /* * selector */
+ };
+
+ selector_type type;
+ std::variant<tag_id_t, std::string_view> value;
+
+ /* Conditions for the css selector */
+ /* Dependency on attributes */
+ struct css_attribute_condition {
+ std::string_view attribute;
+ std::string_view op = "";
+ std::string_view value = "";
+ };
+
+ /* General dependency chain */
+ using css_selector_ptr = std::unique_ptr<css_selector>;
+ using css_selector_dep = std::variant<css_attribute_condition, css_selector_ptr>;
+ std::vector<css_selector_dep> dependencies;
+
+ auto to_tag(void) const -> std::optional<tag_id_t>
+ {
+ if (type == selector_type::SELECTOR_TAG) {
+ return std::get<tag_id_t>(value);
+ }
+ return std::nullopt;
+ }
+
+ auto to_string(void) const -> std::optional<const std::string_view>
+ {
+ if (type != selector_type::SELECTOR_TAG) {
+ return std::string_view(std::get<std::string_view>(value));
+ }
+ return std::nullopt;
+ };
+
+ explicit css_selector(selector_type t)
+ : type(t)
+ {
+ }
+ explicit css_selector(tag_id_t t)
+ : type(selector_type::SELECTOR_TAG)
+ {
+ value = t;
+ }
+ explicit css_selector(const std::string_view &st, selector_type t = selector_type::SELECTOR_ID)
+ : type(t)
+ {
+ value = st;
+ }
+
+ auto operator==(const css_selector &other) const -> bool
+ {
+ return type == other.type && value == other.value;
+ }
+
+ auto debug_str(void) const -> std::string;
+};
+
+
+using selectors_vec = std::vector<std::unique_ptr<css_selector>>;
+
+/*
+ * Consume selectors token and split them to the list of selectors
+ */
+auto process_selector_tokens(rspamd_mempool_t *pool,
+ blocks_gen_functor &&next_token_functor)
+ -> selectors_vec;
+
+}// namespace rspamd::css
+
+/* Selectors hashing */
+namespace std {
+template<>
+class hash<rspamd::css::css_selector> {
+public:
+ using is_avalanching = void;
+ auto operator()(const rspamd::css::css_selector &sel) const -> std::size_t
+ {
+ if (sel.type == rspamd::css::css_selector::selector_type::SELECTOR_TAG) {
+ return static_cast<std::size_t>(std::get<tag_id_t>(sel.value));
+ }
+ else {
+ const auto &sv = std::get<std::string_view>(sel.value);
+
+ return rspamd_cryptobox_fast_hash(sv.data(), sv.size(), 0xdeadbabe);
+ }
+ }
+};
+}// namespace std
+
+#endif//RSPAMD_CSS_SELECTOR_HXX
diff --git a/src/libserver/css/css_selector_parser.rl b/src/libserver/css/css_selector_parser.rl
new file mode 100644
index 0000000..f5ae936
--- /dev/null
+++ b/src/libserver/css/css_selector_parser.rl
@@ -0,0 +1,27 @@
+%%{
+ machine css_parser;
+ alphtype unsigned char;
+ include css_syntax "css_syntax.rl";
+
+ main := selectors_group;
+}%%
+
+%% write data;
+
+#include <cstddef>
+
+namespace rspamd::css {
+
+int
+parse_css_selector (const unsigned char *data, std::size_t len)
+{
+ const unsigned char *p = data, *pe = data + len, *eof;
+ int cs;
+
+ %% write init;
+ %% write exec;
+
+ return cs;
+}
+
+} \ No newline at end of file
diff --git a/src/libserver/css/css_style.hxx b/src/libserver/css/css_style.hxx
new file mode 100644
index 0000000..429e58f
--- /dev/null
+++ b/src/libserver/css/css_style.hxx
@@ -0,0 +1,66 @@
+/*-
+ * Copyright 2021 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.
+ */
+
+#pragma once
+
+#ifndef RSPAMD_CSS_STYLE_HXX
+#define RSPAMD_CSS_STYLE_HXX
+
+#include <memory>
+#include <vector>
+#include "css_rule.hxx"
+#include "css_selector.hxx"
+
+namespace rspamd::css {
+
+/*
+ * Full CSS style representation
+ */
+class css_style {
+public:
+ /* Make class trivial */
+ css_style(const css_style &other) = default;
+
+ css_style(const std::shared_ptr<css_style> &_parent)
+ : parent(_parent)
+ {
+ propagate_from_parent();
+ }
+ css_style(const std::shared_ptr<css_style> &_parent,
+ const std::vector<std::shared_ptr<css_selector>> &_selectors)
+ : parent(_parent)
+ {
+ selectors.reserve(_selectors.size());
+
+ for (const auto &sel_ptr: _selectors) {
+ selectors.emplace_back(sel_ptr);
+ }
+
+ propagate_from_parent();
+ }
+
+private:
+ std::vector<std::weak_ptr<css_selector>> selectors;
+ std::weak_ptr<css_style> parent;
+ std::vector<css_rule> rules;
+
+private:
+ void propagate_from_parent(void); /* Construct full style using parent */
+};
+
+}// namespace rspamd::css
+
+#endif//RSPAMD_CSS_STYLE_HXX
diff --git a/src/libserver/css/css_syntax.rl b/src/libserver/css/css_syntax.rl
new file mode 100644
index 0000000..93da44b
--- /dev/null
+++ b/src/libserver/css/css_syntax.rl
@@ -0,0 +1,110 @@
+%%{
+ # CSS3 EBNF derived
+ machine css_syntax;
+
+ # Primitive Atoms
+ COMMENT = (
+ '/*' ( any )* :>> '*/'
+ );
+ QUOTED_STRING = ('"' ( [^"\\] | /\\./ )* "'");
+ BARE_URL_CHARS = ((0x21
+ | 0x23..0x26
+ | 0x2A..0xFF)+);
+ BARE_URL = BARE_URL_CHARS;
+ URL = 'url(' ( QUOTED_STRING | space* BARE_URL space* ) ')';
+ nonascii = [^0x00-0x7F];
+ nmstart = ([_a-zA-Z] | nonascii);
+ nmchar = ([_a-zA-Z0-9] | 0x2D | nonascii);
+ name = nmchar+;
+ num = ([0-9]+ | ([0-9]* '.' [0-9]+));
+ CRLF = "\r\n" | ("\r" [^\n]) | ([^\r] "\n");
+ IDENT = ([\-]? nmstart nmchar*);
+ ATTR = 'attr(' IDENT ')';
+
+ DIMENSION = '-'? num space? ( 'ch' | 'cm' | 'em' | 'ex' | 'fr' | 'in' | 'mm' | 'pc' | 'pt' | 'px' | 'Q' | 'rem' | 'vh' | 'vmax' | 'vmin' | 'vw' | 'dpi' );
+ NUMBER = '-'? num;
+ HASH = '#' name;
+ HEX = '#' [0-9a-fA-F]{1,6};
+ PERCENTAGE = '-'? num '%';
+ INCLUDES = '~=';
+ DASHMATCH = '|=';
+ PREFIXMATCH = '^=';
+ SUFFIXMATCH = '$=';
+ SUBSTRINGMATCH = '*=';
+ PLUS = '+';
+ GREATER = '>';
+ COMMA = ',';
+ TILDE = '~';
+ S = space;
+
+ # Property name
+ property = ( QUOTED_STRING | IDENT );
+
+ # Values
+ important = space* '!' space* 'important';
+ expression = ( ( '+' | PERCENTAGE | URL | ATTR | HEX | '-' | DIMENSION | NUMBER | QUOTED_STRING | IDENT | ',') S* )+;
+ functional_pseudo = (IDENT - ('attr'|'url')) '(' space* expression? ')';
+ value = ( URL | ATTR | PLUS | HEX | PERCENTAGE | '-' | DIMENSION | NUMBER | QUOTED_STRING | IDENT | functional_pseudo);
+ values = value (space value | '/' value )* ( space* ',' space* value (space value | '/' value )* )* important?;
+
+ # Declaration definition
+ declaration = (property space? ':' (property ':')* space? values);
+
+ # Selectors
+ class = '.' IDENT;
+ element_name = IDENT;
+ namespace_prefix = ( IDENT | '*' )? '|';
+ type_selector = namespace_prefix? element_name;
+ universal = namespace_prefix? '*';
+ attrib = '[' space* namespace_prefix? IDENT space* ( ( PREFIXMATCH | SUFFIXMATCH | SUBSTRINGMATCH | '=' | INCLUDES | DASHMATCH ) space* ( IDENT | QUOTED_STRING ) space* )? ']';
+ pseudo = ':' ':'? ( IDENT | functional_pseudo );
+ atrule = '@' IDENT;
+ mediaquery_selector = '(' declaration ')';
+ negation_arg = type_selector
+ | universal
+ | HASH
+ | class
+ | attrib
+ | pseudo;
+ negation = 'NOT'|'not' space* negation_arg space* ')';
+ # Haha, so simple...
+ # there should be also mediaquery_selector but it makes grammar too large, so rip it off
+ simple_selector_sequence = ( type_selector | universal ) ( HASH | class | attrib | pseudo | negation | atrule )*
+ | ( HASH | class | attrib | pseudo | negation | atrule )+;
+ combinator = space* PLUS space*
+ | space* GREATER space*
+ | space* TILDE space*
+ | space+;
+ # Combine simple stuff and obtain just... an ordinary selector, bingo
+ selector = simple_selector_sequence ( combinator simple_selector_sequence )*;
+ # Multiple beasts
+ selectors_group = selector ( COMMENT? ',' space* selector )*;
+
+ # Rules
+ # This is mostly used stuff
+ rule = selectors_group space? "{" space*
+ (COMMENT? space* declaration ( space? ";" space? declaration?)* ";"? space?)* COMMENT* space* '}';
+ query_declaration = rule;
+
+ # Areas used in css
+ arearule = '@'('bottom-left'|'bottom-right'|'top-left'|'top-right');
+ areaquery = arearule space? '{' space* (COMMENT? space* declaration ( S? ';' S? declaration?)* ';'? space?)* COMMENT* space* '}';
+ # Printed media stuff, useless but we have to parse it :(
+ printcssrule = '@media print';
+ pagearea = ':'('left'|'right');
+ pagerule = '@page' space? pagearea?;
+ pagequery = pagerule space? '{' space* (areaquery| (COMMENT? space* declaration ( space? ';' space? declaration?)* ';'? S?)*) COMMENT* space* '}';
+ printcssquery = printcssrule S? '{' ( S? COMMENT* S? (pagequery| COMMENT|query_declaration) S*)* S? '}';
+ # Something that defines media
+ conditions = ('and'|'screen'|'or'|'only'|'not'|'amzn-mobi'|'amzn-kf8'|'amzn-mobi7'|',');
+ mediarule = '@media' space conditions ( space? conditions| space? mediaquery_selector )*;
+ mediaquery = mediarule space? '{' ( space? COMMENT* query_declaration)* S? '}';
+
+ simple_atrule = ("@charset"|"@namespace") space+ QUOTED_STRING space* ";";
+
+ import_rule = "@import" space+ ( QUOTED_STRING | URL ) space* ";";
+
+ # Final css definition
+ css_style = space* ( ( rule | simple_atrule | import_rule | mediaquery | printcssquery | COMMENT) space* )*;
+
+}%% \ No newline at end of file
diff --git a/src/libserver/css/css_tokeniser.cxx b/src/libserver/css/css_tokeniser.cxx
new file mode 100644
index 0000000..6d3f41e
--- /dev/null
+++ b/src/libserver/css/css_tokeniser.cxx
@@ -0,0 +1,836 @@
+/*-
+ * Copyright 2021 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 "css_tokeniser.hxx"
+#include "css_util.hxx"
+#include "css.hxx"
+#include "frozen/unordered_map.h"
+#include "frozen/string.h"
+#include <string>
+#include <cmath>
+
+namespace rspamd::css {
+
+/* Helpers to create tokens */
+
+/*
+ * This helper is intended to create tokens either with a tag and value
+ * or with just a tag.
+ */
+template<css_parser_token::token_type T, class Arg>
+auto make_token(const Arg &arg) -> css_parser_token;
+
+template<>
+auto make_token<css_parser_token::token_type::string_token, std::string_view>(const std::string_view &s)
+ -> css_parser_token
+{
+ return css_parser_token{css_parser_token::token_type::string_token, s};
+}
+
+template<>
+auto make_token<css_parser_token::token_type::ident_token, std::string_view>(const std::string_view &s)
+ -> css_parser_token
+{
+ return css_parser_token{css_parser_token::token_type::ident_token, s};
+}
+
+template<>
+auto make_token<css_parser_token::token_type::function_token, std::string_view>(const std::string_view &s)
+ -> css_parser_token
+{
+ return css_parser_token{css_parser_token::token_type::function_token, s};
+}
+
+template<>
+auto make_token<css_parser_token::token_type::url_token, std::string_view>(const std::string_view &s)
+ -> css_parser_token
+{
+ return css_parser_token{css_parser_token::token_type::url_token, s};
+}
+
+template<>
+auto make_token<css_parser_token::token_type::whitespace_token, std::string_view>(const std::string_view &s)
+ -> css_parser_token
+{
+ return css_parser_token{css_parser_token::token_type::whitespace_token, s};
+}
+
+template<>
+auto make_token<css_parser_token::token_type::delim_token, char>(const char &c)
+ -> css_parser_token
+{
+ return css_parser_token{css_parser_token::token_type::delim_token, c};
+}
+
+template<>
+auto make_token<css_parser_token::token_type::number_token, float>(const float &d)
+ -> css_parser_token
+{
+ return css_parser_token{css_parser_token::token_type::number_token, d};
+}
+
+/*
+ * Generic tokens with no value (non-terminals)
+ */
+template<css_parser_token::token_type T>
+auto make_token(void) -> css_parser_token
+{
+ return css_parser_token{T, css_parser_token_placeholder()};
+}
+
+static constexpr inline auto is_plain_ident_start(char c) -> bool
+{
+ if ((c & 0x80) || g_ascii_isalpha(c) || c == '_') {
+ return true;
+ }
+
+ return false;
+};
+
+static constexpr inline auto is_plain_ident(char c) -> bool
+{
+ if (is_plain_ident_start(c) || c == '-' || g_ascii_isdigit(c)) {
+ return true;
+ }
+
+ return false;
+};
+
+struct css_dimension_data {
+ css_parser_token::dim_type dtype;
+ double mult;
+};
+
+/*
+ * Maps from css dimensions to the multipliers that look reasonable in email
+ */
+constexpr const auto max_dims = static_cast<int>(css_parser_token::dim_type::dim_max);
+constexpr frozen::unordered_map<frozen::string, css_dimension_data, max_dims> dimensions_map{
+ {"px", {css_parser_token::dim_type::dim_px, 1.0}},
+ /* EM/REM are 16 px, so multiply and round */
+ {"em", {css_parser_token::dim_type::dim_em, 16.0}},
+ {"rem", {css_parser_token::dim_type::dim_rem, 16.0}},
+ /*
+ * Represents the x-height of the element's font.
+ * On fonts with the "x" letter, this is generally the height
+ * of lowercase letters in the font; 1ex = 0.5em in many fonts.
+ */
+ {"ex", {css_parser_token::dim_type::dim_ex, 8.0}},
+ {"wv", {css_parser_token::dim_type::dim_wv, 8.0}},
+ {"wh", {css_parser_token::dim_type::dim_wh, 6.0}},
+ {"vmax", {css_parser_token::dim_type::dim_vmax, 8.0}},
+ {"vmin", {css_parser_token::dim_type::dim_vmin, 6.0}},
+ /* One point. 1pt = 1/72nd of 1in */
+ {"pt", {css_parser_token::dim_type::dim_pt, 96.0 / 72.0}},
+ /* 96px/2.54 */
+ {"cm", {css_parser_token::dim_type::dim_cm, 96.0 / 2.54}},
+ {"mm", {css_parser_token::dim_type::dim_mm, 9.60 / 2.54}},
+ {"in", {css_parser_token::dim_type::dim_in, 96.0}},
+ /* 1pc = 12pt = 1/6th of 1in. */
+ {"pc", {css_parser_token::dim_type::dim_pc, 96.0 / 6.0}}};
+
+auto css_parser_token::adjust_dim(const css_parser_token &dim_token) -> bool
+{
+ if (!std::holds_alternative<float>(value) ||
+ !std::holds_alternative<std::string_view>(dim_token.value)) {
+ /* Invalid tokens */
+ return false;
+ }
+
+ auto num = std::get<float>(value);
+ auto sv = std::get<std::string_view>(dim_token.value);
+
+ auto dim_found = find_map(dimensions_map, sv);
+
+ if (dim_found) {
+ auto dim_elt = dim_found.value().get();
+ dimension_type = dim_elt.dtype;
+ flags |= css_parser_token::number_dimension;
+ num *= dim_elt.mult;
+ }
+ else {
+ flags |= css_parser_token::flag_bad_dimension;
+
+ return false;
+ }
+
+ value = num;
+
+ return true;
+}
+
+
+/*
+ * Consume functions: return a token and advance lexer offset
+ */
+auto css_tokeniser::consume_ident(bool allow_number) -> struct css_parser_token {
+ auto i = offset;
+ auto need_escape = false;
+ auto allow_middle_minus = false;
+
+ auto maybe_escape_sv = [&](auto cur_pos, auto tok_type) -> auto {
+ if (need_escape) {
+ auto escaped = rspamd::css::unescape_css(pool, {&input[offset],
+ cur_pos - offset});
+ offset = cur_pos;
+
+ return css_parser_token{tok_type, escaped};
+ }
+
+ auto result = std::string_view{&input[offset], cur_pos - offset};
+ offset = cur_pos;
+
+ return css_parser_token{tok_type, result};
+ };
+
+ /* Ident token can start from `-` or `--` */
+ if (input[i] == '-') {
+ i++;
+
+ if (i < input.size() && input[i] == '-') {
+ i++;
+ allow_middle_minus = true;
+ }
+ }
+
+ while (i < input.size()) {
+ auto c = input[i];
+
+ auto is_plain_c = (allow_number || allow_middle_minus) ? is_plain_ident(c) : is_plain_ident_start(c);
+ if (!is_plain_c) {
+ if (c == '\\' && i + 1 < input.size()) {
+ /* Escape token */
+ need_escape = true;
+ auto nhex = 0;
+
+ /* Need to find an escape end */
+ do {
+ c = input[++i];
+ if (g_ascii_isxdigit(c)) {
+ nhex++;
+
+ if (nhex > 6) {
+ /* End of the escape */
+ break;
+ }
+ }
+ else if (nhex > 0 && c == ' ') {
+ /* \[hex]{1,6} */
+ i++; /* Skip one space */
+ break;
+ }
+ else {
+ /* Single \ + char */
+ break;
+ }
+ } while (i < input.size());
+ }
+ else if (c == '(') {
+ /* Function or url token */
+ auto j = i + 1;
+
+ while (j < input.size() && g_ascii_isspace(input[j])) {
+ j++;
+ }
+
+ if (input.size() - offset > 3 && input.substr(offset, 3) == "url") {
+ if (j < input.size() && (input[j] == '"' || input[j] == '\'')) {
+ /* Function token */
+ auto ret = maybe_escape_sv(i,
+ css_parser_token::token_type::function_token);
+ return ret;
+ }
+ else {
+ /* Consume URL token */
+ while (j < input.size() && input[j] != ')') {
+ j++;
+ }
+
+ if (j < input.size() && input[j] == ')') {
+ /* Valid url token */
+ auto ret = maybe_escape_sv(j + 1,
+ css_parser_token::token_type::url_token);
+ return ret;
+ }
+ else {
+ /* Incomplete url token */
+ auto ret = maybe_escape_sv(j,
+ css_parser_token::token_type::url_token);
+
+ ret.flags |= css_parser_token::flag_bad_string;
+ return ret;
+ }
+ }
+ }
+ else {
+ auto ret = maybe_escape_sv(i,
+ css_parser_token::token_type::function_token);
+ return ret;
+ }
+ }
+ else if (c == '-' && allow_middle_minus) {
+ i++;
+ continue;
+ }
+ else {
+ break; /* Not an ident token */
+ }
+ } /* !plain ident */
+ else {
+ allow_middle_minus = true;
+ }
+
+ i++;
+ }
+
+ return maybe_escape_sv(i, css_parser_token::token_type::ident_token);
+}
+
+auto
+css_tokeniser::consume_number() -> struct css_parser_token {
+ auto i = offset;
+ auto seen_dot = false, seen_exp = false;
+
+ if (input[i] == '-' || input[i] == '+') {
+ i++;
+ }
+ if (input[i] == '.' && i < input.size()) {
+ seen_dot = true;
+ i++;
+ }
+
+ while (i < input.size()) {
+ auto c = input[i];
+
+ if (!g_ascii_isdigit(c)) {
+ if (c == '.') {
+ if (!seen_dot) {
+ seen_dot = true;
+ }
+ else {
+ break;
+ }
+ }
+ else if (c == 'e' || c == 'E') {
+ if (!seen_exp) {
+ seen_exp = true;
+ seen_dot = true; /* dots are not allowed after e */
+
+ if (i + 1 < input.size()) {
+ auto next_c = input[i + 1];
+ if (next_c == '+' || next_c == '-') {
+ i++;
+ }
+ else if (!g_ascii_isdigit(next_c)) {
+ /* Not an exponent */
+ break;
+ }
+ }
+ else {
+ /* Not an exponent */
+ break;
+ }
+ }
+ else {
+ break;
+ }
+ }
+ else {
+ break;
+ }
+ }
+
+ i++;
+ }
+
+ if (i > offset) {
+ /* I wish it was supported properly */
+ //auto conv_res = std::from_chars(&input[offset], &input[i], num);
+ char numbuf[128], *endptr = nullptr;
+ rspamd_strlcpy(numbuf, &input[offset], MIN(i - offset + 1, sizeof(numbuf)));
+ auto num = g_ascii_strtod(numbuf, &endptr);
+ offset = i;
+
+ if (fabs(num) >= G_MAXFLOAT || std::isnan(num)) {
+ msg_debug_css("invalid number: %s", numbuf);
+ return make_token<css_parser_token::token_type::delim_token>(input[i - 1]);
+ }
+ else {
+
+ auto ret = make_token<css_parser_token::token_type::number_token>(static_cast<float>(num));
+
+ if (i < input.size()) {
+ if (input[i] == '%') {
+ ret.flags |= css_parser_token::number_percent;
+ i++;
+
+ offset = i;
+ }
+ else if (is_plain_ident_start(input[i])) {
+ auto dim_token = consume_ident();
+
+ if (dim_token.type == css_parser_token::token_type::ident_token) {
+ if (!ret.adjust_dim(dim_token)) {
+ auto sv = std::get<std::string_view>(dim_token.value);
+ msg_debug_css("cannot apply dimension from the token %*s; number value = %.1f",
+ (int) sv.size(), sv.begin(), num);
+ /* Unconsume ident */
+ offset = i;
+ }
+ }
+ else {
+ /* We have no option but to uncosume ident token in this case */
+ msg_debug_css("got invalid ident like token after number, unconsume it");
+ }
+ }
+ else {
+ /* Plain number, nothing to do */
+ }
+ }
+
+ return ret;
+ }
+ }
+ else {
+ msg_err_css("internal error: invalid number, empty token");
+ i++;
+ }
+
+ offset = i;
+ /* Should not happen */
+ return make_token<css_parser_token::token_type::delim_token>(input[i - 1]);
+}
+
+/*
+ * Main routine to produce lexer tokens
+ */
+auto
+css_tokeniser::next_token(void) -> struct css_parser_token {
+ /* Check pushback queue */
+ if (!backlog.empty()) {
+ auto tok = backlog.front();
+ backlog.pop_front();
+
+ return tok;
+ }
+ /* Helpers */
+
+ /*
+ * This lambda eats comment handling nested comments;
+ * offset is set to the next character after a comment (or eof)
+ * Nothing is returned
+ */
+ auto consume_comment = [this]() {
+ auto i = offset;
+ auto nested = 0;
+
+ if (input.empty()) {
+ /* Nothing to consume */
+ return;
+ }
+
+ /* We handle nested comments just because they can exist... */
+ while (i < input.size() - 1) {
+ auto c = input[i];
+ if (c == '*' && input[i + 1] == '/') {
+ if (nested == 0) {
+ offset = i + 2;
+ return;
+ }
+ else {
+ nested--;
+ i += 2;
+ continue;
+ }
+ }
+ else if (c == '/' && input[i + 1] == '*') {
+ nested++;
+ i += 2;
+ continue;
+ }
+
+ i++;
+ }
+
+ offset = i;
+ };
+
+ /*
+ * Consume quoted string, returns a string_view over a string, offset
+ * is set one character after the string. Css unescaping is done automatically
+ * Accepts a quote char to find end of string
+ */
+ auto consume_string = [this](auto quote_char) -> auto {
+ auto i = offset;
+ bool need_unescape = false;
+
+ while (i < input.size()) {
+ auto c = input[i];
+
+ if (c == '\\') {
+ if (i + 1 < input.size()) {
+ need_unescape = true;
+ }
+ else {
+ /* \ at the end -> ignore */
+ }
+ }
+ else if (c == quote_char) {
+ /* End of string */
+ std::string_view res{&input[offset], i - offset};
+
+ if (need_unescape) {
+ res = rspamd::css::unescape_css(pool, res);
+ }
+
+ offset = i + 1;
+
+ return res;
+ }
+ else if (c == '\n') {
+ /* Should be a error, but we ignore it for now */
+ }
+
+ i++;
+ }
+
+ /* EOF with no quote character, consider it fine */
+ std::string_view res{&input[offset], i - offset};
+
+ if (need_unescape) {
+ res = rspamd::css::unescape_css(pool, res);
+ }
+
+ offset = i;
+
+ return res;
+ };
+
+ /* Main tokenisation loop */
+ for (auto i = offset; i < input.size(); ++i) {
+ auto c = input[i];
+
+ switch (c) {
+ case '/':
+ if (i + 1 < input.size() && input[i + 1] == '*') {
+ offset = i + 2;
+ consume_comment(); /* Consume comment and go forward */
+ return next_token(); /* Tail call */
+ }
+ else {
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::delim_token>(c);
+ }
+ break;
+ case ' ':
+ case '\t':
+ case '\n':
+ case '\r':
+ case '\f': {
+ /* Consume as much space as we can */
+ while (i < input.size() && g_ascii_isspace(input[i])) {
+ i++;
+ }
+
+ auto ret = make_token<css_parser_token::token_type::whitespace_token>(
+ std::string_view(&input[offset], i - offset));
+ offset = i;
+ return ret;
+ }
+ case '"':
+ case '\'':
+ offset = i + 1;
+ if (offset < input.size()) {
+ return make_token<css_parser_token::token_type::string_token>(consume_string(c));
+ }
+ else {
+ /* Unpaired quote at the end of the rule */
+ return make_token<css_parser_token::token_type::delim_token>(c);
+ }
+ case '(':
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::obrace_token>();
+ case ')':
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::ebrace_token>();
+ case '[':
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::osqbrace_token>();
+ case ']':
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::esqbrace_token>();
+ case '{':
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::ocurlbrace_token>();
+ case '}':
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::ecurlbrace_token>();
+ case ',':
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::comma_token>();
+ case ';':
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::semicolon_token>();
+ case ':':
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::colon_token>();
+ case '<':
+ /* Maybe an xml like comment */
+ if (i + 3 < input.size() && input[i + 1] == '!' && input[i + 2] == '-' && input[i + 3] == '-') {
+ offset += 3;
+
+ return make_token<css_parser_token::token_type::cdo_token>();
+ }
+ else {
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::delim_token>(c);
+ }
+ break;
+ case '-':
+ if (i + 1 < input.size()) {
+ auto next_c = input[i + 1];
+
+ if (g_ascii_isdigit(next_c)) {
+ /* negative number */
+ return consume_number();
+ }
+ else if (next_c == '-') {
+ if (i + 2 < input.size() && input[i + 2] == '>') {
+ /* XML like comment */
+ offset += 3;
+
+ return make_token<css_parser_token::token_type::cdc_token>();
+ }
+ }
+ }
+ /* No other options, a delimiter - */
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::delim_token>(c);
+
+ break;
+ case '+':
+ case '.':
+ /* Maybe number */
+ if (i + 1 < input.size()) {
+ auto next_c = input[i + 1];
+
+ if (g_ascii_isdigit(next_c)) {
+ /* Numeric token */
+ return consume_number();
+ }
+ else {
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::delim_token>(c);
+ }
+ }
+ /* No other options, a delimiter - */
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::delim_token>(c);
+
+ break;
+ case '\\':
+ if (i + 1 < input.size()) {
+ if (input[i + 1] == '\n' || input[i + 1] == '\r') {
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::delim_token>(c);
+ }
+ else {
+ /* Valid escape, assume ident */
+ return consume_ident();
+ }
+ }
+ else {
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::delim_token>(c);
+ }
+ break;
+ case '@':
+ if (i + 3 < input.size()) {
+ if (is_plain_ident_start(input[i + 1]) &&
+ is_plain_ident(input[i + 2]) && is_plain_ident(input[i + 3])) {
+ offset = i + 1;
+ auto ident_token = consume_ident();
+
+ if (ident_token.type == css_parser_token::token_type::ident_token) {
+ /* Update type */
+ ident_token.type = css_parser_token::token_type::at_keyword_token;
+ }
+
+ return ident_token;
+ }
+ else {
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::delim_token>(c);
+ }
+ }
+ else {
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::delim_token>(c);
+ }
+ break;
+ case '#':
+ /* TODO: make it more conformant */
+ if (i + 2 < input.size()) {
+ auto next_c = input[i + 1], next_next_c = input[i + 2];
+ if ((is_plain_ident(next_c) || next_c == '-') &&
+ (is_plain_ident(next_next_c) || next_next_c == '-')) {
+ offset = i + 1;
+ /* We consume indent, but we allow numbers there */
+ auto ident_token = consume_ident(true);
+
+ if (ident_token.type == css_parser_token::token_type::ident_token) {
+ /* Update type */
+ ident_token.type = css_parser_token::token_type::hash_token;
+ }
+
+ return ident_token;
+ }
+ else {
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::delim_token>(c);
+ }
+ }
+ else {
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::delim_token>(c);
+ }
+ break;
+ default:
+ /* Generic parsing code */
+
+ if (g_ascii_isdigit(c)) {
+ return consume_number();
+ }
+ else if (is_plain_ident_start(c)) {
+ return consume_ident();
+ }
+ else {
+ offset = i + 1;
+ return make_token<css_parser_token::token_type::delim_token>(c);
+ }
+ break;
+ }
+ }
+
+ return make_token<css_parser_token::token_type::eof_token>();
+}
+
+constexpr auto
+css_parser_token::get_token_type() -> const char *
+{
+ const char *ret = "unknown";
+
+ switch (type) {
+ case token_type::whitespace_token:
+ ret = "whitespace";
+ break;
+ case token_type::ident_token:
+ ret = "ident";
+ break;
+ case token_type::function_token:
+ ret = "function";
+ break;
+ case token_type::at_keyword_token:
+ ret = "atkeyword";
+ break;
+ case token_type::hash_token:
+ ret = "hash";
+ break;
+ case token_type::string_token:
+ ret = "string";
+ break;
+ case token_type::number_token:
+ ret = "number";
+ break;
+ case token_type::url_token:
+ ret = "url";
+ break;
+ case token_type::cdo_token: /* xml open comment */
+ ret = "cdo";
+ break;
+ case token_type::cdc_token: /* xml close comment */
+ ret = "cdc";
+ break;
+ case token_type::delim_token:
+ ret = "delim";
+ break;
+ case token_type::obrace_token: /* ( */
+ ret = "obrace";
+ break;
+ case token_type::ebrace_token: /* ) */
+ ret = "ebrace";
+ break;
+ case token_type::osqbrace_token: /* [ */
+ ret = "osqbrace";
+ break;
+ case token_type::esqbrace_token: /* ] */
+ ret = "esqbrace";
+ break;
+ case token_type::ocurlbrace_token: /* { */
+ ret = "ocurlbrace";
+ break;
+ case token_type::ecurlbrace_token: /* } */
+ ret = "ecurlbrace";
+ break;
+ case token_type::comma_token:
+ ret = "comma";
+ break;
+ case token_type::colon_token:
+ ret = "colon";
+ break;
+ case token_type::semicolon_token:
+ ret = "semicolon";
+ break;
+ case token_type::eof_token:
+ ret = "eof";
+ break;
+ }
+
+ return ret;
+}
+
+
+auto css_parser_token::debug_token_str() -> std::string
+{
+ const auto *token_type_str = get_token_type();
+ std::string ret = token_type_str;
+
+ std::visit([&](auto arg) -> auto {
+ using T = std::decay_t<decltype(arg)>;
+
+ if constexpr (std::is_same_v<T, std::string_view> || std::is_same_v<T, char>) {
+ ret += "; value=";
+ ret += arg;
+ }
+ else if constexpr (std::is_same_v<T, double>) {
+ ret += "; value=";
+ ret += std::to_string(arg);
+ }
+ },
+ value);
+
+ if ((flags & (~number_dimension)) != default_flags) {
+ ret += "; flags=" + std::to_string(flags);
+ }
+
+ if (flags & number_dimension) {
+ ret += "; dim=" + std::to_string(static_cast<int>(dimension_type));
+ }
+
+ return ret; /* Copy elision */
+}
+
+}// namespace rspamd::css \ No newline at end of file
diff --git a/src/libserver/css/css_tokeniser.hxx b/src/libserver/css/css_tokeniser.hxx
new file mode 100644
index 0000000..aa6a1a7
--- /dev/null
+++ b/src/libserver/css/css_tokeniser.hxx
@@ -0,0 +1,215 @@
+/*-
+ * Copyright 2021 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.
+ */
+
+#pragma once
+
+#ifndef RSPAMD_CSS_TOKENISER_HXX
+#define RSPAMD_CSS_TOKENISER_HXX
+
+#include <string_view>
+#include <utility>
+#include <variant>
+#include <list>
+#include <functional>
+#include <cstdint>
+#include "mem_pool.h"
+
+namespace rspamd::css {
+
+struct css_parser_token_placeholder {}; /* For empty tokens */
+
+struct css_parser_token {
+
+ enum class token_type : std::uint8_t {
+ whitespace_token,
+ ident_token,
+ function_token,
+ at_keyword_token,
+ hash_token,
+ string_token,
+ number_token,
+ url_token,
+ cdo_token, /* xml open comment */
+ cdc_token, /* xml close comment */
+ delim_token,
+ obrace_token, /* ( */
+ ebrace_token, /* ) */
+ osqbrace_token, /* [ */
+ esqbrace_token, /* ] */
+ ocurlbrace_token, /* { */
+ ecurlbrace_token, /* } */
+ comma_token,
+ colon_token,
+ semicolon_token,
+ eof_token,
+ };
+
+ enum class dim_type : std::uint8_t {
+ dim_px = 0,
+ dim_em,
+ dim_rem,
+ dim_ex,
+ dim_wv,
+ dim_wh,
+ dim_vmax,
+ dim_vmin,
+ dim_pt,
+ dim_cm,
+ dim_mm,
+ dim_in,
+ dim_pc,
+ dim_max,
+ };
+
+ static const std::uint8_t default_flags = 0;
+ static const std::uint8_t flag_bad_string = (1u << 0u);
+ static const std::uint8_t number_dimension = (1u << 1u);
+ static const std::uint8_t number_percent = (1u << 2u);
+ static const std::uint8_t flag_bad_dimension = (1u << 3u);
+
+ using value_type = std::variant<std::string_view, /* For strings and string like tokens */
+ char, /* For delimiters (might need to move to unicode point) */
+ float, /* For numeric stuff */
+ css_parser_token_placeholder /* For general no token stuff */
+ >;
+
+ /* Typed storage */
+ value_type value;
+
+ int lineno;
+
+ token_type type;
+ std::uint8_t flags = default_flags;
+ dim_type dimension_type;
+
+ css_parser_token() = delete;
+ explicit css_parser_token(token_type type, const value_type &value)
+ : value(value), type(type)
+ {
+ }
+ css_parser_token(css_parser_token &&other) = default;
+ css_parser_token(const css_parser_token &token) = default;
+ auto operator=(css_parser_token &&other) -> css_parser_token & = default;
+ auto adjust_dim(const css_parser_token &dim_token) -> bool;
+
+ auto get_string_or_default(const std::string_view &def) const -> std::string_view
+ {
+ if (std::holds_alternative<std::string_view>(value)) {
+ return std::get<std::string_view>(value);
+ }
+ else if (std::holds_alternative<char>(value)) {
+ return std::string_view(&std::get<char>(value), 1);
+ }
+
+ return def;
+ }
+
+ auto get_delim() const -> char
+ {
+ if (std::holds_alternative<char>(value)) {
+ return std::get<char>(value);
+ }
+
+ return (char) -1;
+ }
+
+ auto get_number_or_default(float def) const -> float
+ {
+ if (std::holds_alternative<float>(value)) {
+ auto dbl = std::get<float>(value);
+
+ if (flags & css_parser_token::number_percent) {
+ dbl /= 100.0;
+ }
+
+ return dbl;
+ }
+
+ return def;
+ }
+
+ auto get_normal_number_or_default(float def) const -> float
+ {
+ if (std::holds_alternative<float>(value)) {
+ auto dbl = std::get<float>(value);
+
+ if (flags & css_parser_token::number_percent) {
+ dbl /= 100.0;
+ }
+
+ if (dbl < 0) {
+ return 0.0;
+ }
+ else if (dbl > 1.0) {
+ return 1.0;
+ }
+
+ return dbl;
+ }
+
+ return def;
+ }
+
+ /* Debugging routines */
+ constexpr auto get_token_type() -> const char *;
+ /* This function might be slow */
+ auto debug_token_str() -> std::string;
+};
+
+static auto css_parser_eof_token(void) -> const css_parser_token &
+{
+ static css_parser_token eof_tok{
+ css_parser_token::token_type::eof_token,
+ css_parser_token_placeholder()};
+
+ return eof_tok;
+}
+
+/* Ensure that parser tokens are simple enough */
+/*
+ * compiler must implement P0602 "variant and optional should propagate copy/move triviality"
+ * This is broken on gcc < 8!
+ */
+static_assert(std::is_trivially_copyable_v<css_parser_token>);
+
+class css_tokeniser {
+public:
+ css_tokeniser() = delete;
+ css_tokeniser(rspamd_mempool_t *pool, const std::string_view &sv)
+ : input(sv), offset(0), pool(pool)
+ {
+ }
+
+ auto next_token(void) -> struct css_parser_token;
+ auto pushback_token(const struct css_parser_token &t) const -> void
+ {
+ backlog.push_back(t);
+ }
+
+private:
+ std::string_view input;
+ std::size_t offset;
+ rspamd_mempool_t *pool;
+ mutable std::list<css_parser_token> backlog;
+
+ auto consume_number() -> struct css_parser_token;
+ auto consume_ident(bool allow_number = false) -> struct css_parser_token;
+};
+
+}// namespace rspamd::css
+
+
+#endif//RSPAMD_CSS_TOKENISER_HXX
diff --git a/src/libserver/css/css_util.cxx b/src/libserver/css/css_util.cxx
new file mode 100644
index 0000000..07f8722
--- /dev/null
+++ b/src/libserver/css/css_util.cxx
@@ -0,0 +1,157 @@
+/*-
+ * Copyright 2021 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 "css_util.hxx"
+#include "css.hxx"
+#include <unicode/utf8.h>
+
+namespace rspamd::css {
+
+std::string_view unescape_css(rspamd_mempool_t *pool,
+ const std::string_view &sv)
+{
+ auto *nspace = reinterpret_cast<char *>(rspamd_mempool_alloc(pool, sv.length()));
+ auto *d = nspace;
+ auto nleft = sv.length();
+
+ enum {
+ normal = 0,
+ quoted,
+ escape,
+ skip_spaces,
+ } state = normal;
+
+ char quote_char, prev_c = 0;
+ auto escape_offset = 0, i = 0;
+
+#define MAYBE_CONSUME_CHAR(c) \
+ do { \
+ if ((c) == '"' || (c) == '\'') { \
+ state = quoted; \
+ quote_char = (c); \
+ nleft--; \
+ *d++ = (c); \
+ } \
+ else if ((c) == '\\') { \
+ escape_offset = i; \
+ state = escape; \
+ } \
+ else { \
+ state = normal; \
+ nleft--; \
+ *d++ = g_ascii_tolower(c); \
+ } \
+ } while (0)
+
+ for (const auto c: sv) {
+ if (nleft == 0) {
+ msg_err_css("cannot unescape css: truncated buffer of size %d",
+ (int) sv.length());
+ break;
+ }
+ switch (state) {
+ case normal:
+ MAYBE_CONSUME_CHAR(c);
+ break;
+ case quoted:
+ if (c == quote_char) {
+ if (prev_c != '\\') {
+ state = normal;
+ }
+ }
+ prev_c = c;
+ nleft--;
+ *d++ = c;
+ break;
+ case escape:
+ if (!g_ascii_isxdigit(c)) {
+ if (i > escape_offset + 1) {
+ /* Try to decode an escape */
+ const auto *escape_start = &sv[escape_offset + 1];
+ unsigned long val;
+
+ if (!rspamd_xstrtoul(escape_start, i - escape_offset - 1, &val)) {
+ msg_debug_css("invalid broken escape found at pos %d",
+ escape_offset);
+ }
+ else {
+ if (val < 0x80) {
+ /* Trivial case: ascii character */
+ *d++ = (unsigned char) g_ascii_tolower(val);
+ nleft--;
+ }
+ else {
+ UChar32 uc = val;
+ auto off = 0;
+ UTF8_APPEND_CHAR_SAFE((uint8_t *) d, off,
+ sv.length(), u_tolower(uc));
+ d += off;
+ nleft -= off;
+ }
+ }
+ }
+ else {
+ /* Empty escape, ignore it */
+ msg_debug_css("invalid empty escape found at pos %d",
+ escape_offset);
+ }
+
+ if (nleft <= 0) {
+ msg_err_css("cannot unescape css: truncated buffer of size %d",
+ (int) sv.length());
+ }
+ else {
+ /* Escape is done, advance forward */
+ if (g_ascii_isspace(c)) {
+ state = skip_spaces;
+ }
+ else {
+ MAYBE_CONSUME_CHAR(c);
+ }
+ }
+ }
+ break;
+ case skip_spaces:
+ if (!g_ascii_isspace(c)) {
+ MAYBE_CONSUME_CHAR(c);
+ }
+ /* Ignore spaces */
+ break;
+ }
+
+ i++;
+ }
+
+ return std::string_view{nspace, sv.size() - nleft};
+}
+
+}// namespace rspamd::css
+
+/* C API */
+const gchar *rspamd_css_unescape(rspamd_mempool_t *pool,
+ const guchar *begin,
+ gsize len,
+ gsize *outlen)
+{
+ auto sv = rspamd::css::unescape_css(pool, {(const char *) begin, len});
+ const auto *v = sv.begin();
+
+ if (outlen) {
+ *outlen = sv.size();
+ }
+
+ return v;
+} \ No newline at end of file
diff --git a/src/libserver/css/css_util.hxx b/src/libserver/css/css_util.hxx
new file mode 100644
index 0000000..4837a46
--- /dev/null
+++ b/src/libserver/css/css_util.hxx
@@ -0,0 +1,37 @@
+/*-
+ * Copyright 2021 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.
+ */
+
+#pragma once
+
+#ifndef RSPAMD_CSS_UTIL_HXX
+#define RSPAMD_CSS_UTIL_HXX
+
+#include <string_view>
+#include "mem_pool.h"
+
+namespace rspamd::css {
+
+/*
+ * Unescape css escapes
+ * \20AC : must be followed by a space if the next character is one of a-f, A-F, 0-9
+ * \0020AC : must be 6 digits long, no space needed (but can be included)
+ */
+std::string_view unescape_css(rspamd_mempool_t *pool,
+ const std::string_view &sv);
+
+}// namespace rspamd::css
+
+#endif//RSPAMD_CSS_UTIL_HXX
diff --git a/src/libserver/css/css_value.cxx b/src/libserver/css/css_value.cxx
new file mode 100644
index 0000000..2546e01
--- /dev/null
+++ b/src/libserver/css/css_value.cxx
@@ -0,0 +1,449 @@
+/*-
+ * Copyright 2021 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 "css_value.hxx"
+#include "css_colors_list.hxx"
+#include "frozen/unordered_map.h"
+#include "frozen/string.h"
+#include "libutil/util.h"
+#include "contrib/ankerl/unordered_dense.h"
+#include "fmt/core.h"
+
+#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
+#include "doctest/doctest.h"
+
+/* Helper for unit test stringification */
+namespace doctest {
+template<>
+struct StringMaker<rspamd::css::css_color> {
+ static String convert(const rspamd::css::css_color &value)
+ {
+ return fmt::format("r={};g={};b={};alpha={}",
+ value.r, value.g, value.b, value.alpha)
+ .c_str();
+ }
+};
+
+}// namespace doctest
+
+namespace rspamd::css {
+
+auto css_value::maybe_color_from_string(const std::string_view &input)
+ -> std::optional<css_value>
+{
+
+ if (input.size() > 1 && input.front() == '#') {
+ return css_value::maybe_color_from_hex(input.substr(1));
+ }
+ else {
+ auto found_it = css_colors_map.find(input);
+
+ if (found_it != css_colors_map.end()) {
+ return css_value{found_it->second};
+ }
+ }
+
+ return std::nullopt;
+}
+
+constexpr static inline auto hexpair_decode(char c1, char c2) -> std::uint8_t
+{
+ std::uint8_t ret = 0;
+
+ if (c1 >= '0' && c1 <= '9') ret = c1 - '0';
+ else if (c1 >= 'A' && c1 <= 'F')
+ ret = c1 - 'A' + 10;
+ else if (c1 >= 'a' && c1 <= 'f')
+ ret = c1 - 'a' + 10;
+
+ ret *= 16;
+
+ if (c2 >= '0' && c2 <= '9') ret += c2 - '0';
+ else if (c2 >= 'A' && c2 <= 'F')
+ ret += c2 - 'A' + 10;
+ else if (c2 >= 'a' && c2 <= 'f')
+ ret += c2 - 'a' + 10;
+
+ return ret;
+}
+
+auto css_value::maybe_color_from_hex(const std::string_view &input)
+ -> std::optional<css_value>
+{
+ if (input.length() == 6) {
+ /* Plain RGB */
+ css_color col(hexpair_decode(input[0], input[1]),
+ hexpair_decode(input[2], input[3]),
+ hexpair_decode(input[4], input[5]));
+ return css_value(col);
+ }
+ else if (input.length() == 3) {
+ /* Rgb as 3 hex digests */
+ css_color col(hexpair_decode(input[0], input[0]),
+ hexpair_decode(input[1], input[1]),
+ hexpair_decode(input[2], input[2]));
+ return css_value(col);
+ }
+ else if (input.length() == 8) {
+ /* RGBA */
+ css_color col(hexpair_decode(input[0], input[1]),
+ hexpair_decode(input[2], input[3]),
+ hexpair_decode(input[4], input[5]),
+ hexpair_decode(input[6], input[7]));
+ return css_value(col);
+ }
+
+ return std::nullopt;
+}
+
+constexpr static inline auto rgb_color_component_convert(const css_parser_token &tok)
+ -> std::uint8_t
+{
+ std::uint8_t ret = 0;
+
+ if (tok.type == css_parser_token::token_type::number_token) {
+ auto dbl = std::get<float>(tok.value);
+
+ if (tok.flags & css_parser_token::number_percent) {
+ if (dbl > 100) {
+ dbl = 100;
+ }
+ else if (dbl < 0) {
+ dbl = 0;
+ }
+ ret = (std::uint8_t)(dbl / 100.0 * 255.0);
+ }
+ else {
+ if (dbl > 255) {
+ dbl = 255;
+ }
+ else if (dbl < 0) {
+ dbl = 0;
+ }
+
+ ret = (std::uint8_t)(dbl);
+ }
+ }
+
+ return ret;
+}
+
+constexpr static inline auto alpha_component_convert(const css_parser_token &tok)
+ -> std::uint8_t
+{
+ double ret = 1.0;
+
+ if (tok.type == css_parser_token::token_type::number_token) {
+ auto dbl = std::get<float>(tok.value);
+
+ if (tok.flags & css_parser_token::number_percent) {
+ if (dbl > 100) {
+ dbl = 100;
+ }
+ else if (dbl < 0) {
+ dbl = 0;
+ }
+ ret = (dbl / 100.0);
+ }
+ else {
+ if (dbl > 1.0) {
+ dbl = 1.0;
+ }
+ else if (dbl < 0) {
+ dbl = 0;
+ }
+
+ ret = dbl;
+ }
+ }
+
+ return (std::uint8_t)(ret * 255.0);
+}
+
+constexpr static inline auto h_component_convert(const css_parser_token &tok)
+ -> double
+{
+ double ret = 0.0;
+
+ if (tok.type == css_parser_token::token_type::number_token) {
+ auto dbl = std::get<float>(tok.value);
+
+ if (tok.flags & css_parser_token::number_percent) {
+ if (dbl > 100) {
+ dbl = 100;
+ }
+ else if (dbl < 0) {
+ dbl = 0;
+ }
+ ret = (dbl / 100.0);
+ }
+ else {
+ dbl = ((((int) dbl % 360) + 360) % 360); /* Deal with rotations */
+ ret = dbl / 360.0; /* Normalize to 0..1 */
+ }
+ }
+
+ return ret;
+}
+
+constexpr static inline auto sl_component_convert(const css_parser_token &tok)
+ -> double
+{
+ double ret = 0.0;
+
+ if (tok.type == css_parser_token::token_type::number_token) {
+ ret = tok.get_normal_number_or_default(ret);
+ }
+
+ return ret;
+}
+
+static inline auto hsl_to_rgb(double h, double s, double l)
+ -> css_color
+{
+ css_color ret;
+
+ constexpr auto hue2rgb = [](auto p, auto q, auto t) -> auto {
+ if (t < 0.0) {
+ t += 1.0;
+ }
+ if (t > 1.0) {
+ t -= 1.0;
+ }
+ if (t * 6. < 1.0) {
+ return p + (q - p) * 6.0 * t;
+ }
+ if (t * 2. < 1) {
+ return q;
+ }
+ if (t * 3. < 2.) {
+ return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
+ }
+ return p;
+ };
+
+ if (s == 0) {
+ /* Achromatic */
+ ret.r = l;
+ ret.g = l;
+ ret.b = l;
+ }
+ else {
+ auto q = l <= 0.5 ? l * (1.0 + s) : l + s - l * s;
+ auto p = 2.0 * l - q;
+ ret.r = (std::uint8_t)(hue2rgb(p, q, h + 1.0 / 3.0) * 255);
+ ret.g = (std::uint8_t)(hue2rgb(p, q, h) * 255);
+ ret.b = (std::uint8_t)(hue2rgb(p, q, h - 1.0 / 3.0) * 255);
+ }
+
+ ret.alpha = 255;
+
+ return ret;
+}
+
+auto css_value::maybe_color_from_function(const css_consumed_block::css_function_block &func)
+ -> std::optional<css_value>
+{
+
+ if (func.as_string() == "rgb" && func.args.size() == 3) {
+ css_color col{rgb_color_component_convert(func.args[0]->get_token_or_empty()),
+ rgb_color_component_convert(func.args[1]->get_token_or_empty()),
+ rgb_color_component_convert(func.args[2]->get_token_or_empty())};
+
+ return css_value(col);
+ }
+ else if (func.as_string() == "rgba" && func.args.size() == 4) {
+ css_color col{rgb_color_component_convert(func.args[0]->get_token_or_empty()),
+ rgb_color_component_convert(func.args[1]->get_token_or_empty()),
+ rgb_color_component_convert(func.args[2]->get_token_or_empty()),
+ alpha_component_convert(func.args[3]->get_token_or_empty())};
+
+ return css_value(col);
+ }
+ else if (func.as_string() == "hsl" && func.args.size() == 3) {
+ auto h = h_component_convert(func.args[0]->get_token_or_empty());
+ auto s = sl_component_convert(func.args[1]->get_token_or_empty());
+ auto l = sl_component_convert(func.args[2]->get_token_or_empty());
+
+ auto col = hsl_to_rgb(h, s, l);
+
+ return css_value(col);
+ }
+ else if (func.as_string() == "hsla" && func.args.size() == 4) {
+ auto h = h_component_convert(func.args[0]->get_token_or_empty());
+ auto s = sl_component_convert(func.args[1]->get_token_or_empty());
+ auto l = sl_component_convert(func.args[2]->get_token_or_empty());
+
+ auto col = hsl_to_rgb(h, s, l);
+ col.alpha = alpha_component_convert(func.args[3]->get_token_or_empty());
+
+ return css_value(col);
+ }
+
+ return std::nullopt;
+}
+
+auto css_value::maybe_dimension_from_number(const css_parser_token &tok)
+ -> std::optional<css_value>
+{
+ if (std::holds_alternative<float>(tok.value)) {
+ auto dbl = std::get<float>(tok.value);
+ css_dimension dim;
+
+ dim.dim = dbl;
+
+ if (tok.flags & css_parser_token::number_percent) {
+ dim.is_percent = true;
+ }
+ else {
+ dim.is_percent = false;
+ }
+
+ return css_value{dim};
+ }
+
+ return std::nullopt;
+}
+
+constexpr const auto display_names_map = frozen::make_unordered_map<frozen::string, css_display_value>({
+ {"hidden", css_display_value::DISPLAY_HIDDEN},
+ {"none", css_display_value::DISPLAY_HIDDEN},
+ {"inline", css_display_value::DISPLAY_INLINE},
+ {"block", css_display_value::DISPLAY_BLOCK},
+ {"content", css_display_value::DISPLAY_INLINE},
+ {"flex", css_display_value::DISPLAY_BLOCK},
+ {"grid", css_display_value::DISPLAY_BLOCK},
+ {"inline-block", css_display_value::DISPLAY_INLINE},
+ {"inline-flex", css_display_value::DISPLAY_INLINE},
+ {"inline-grid", css_display_value::DISPLAY_INLINE},
+ {"inline-table", css_display_value::DISPLAY_INLINE},
+ {"list-item", css_display_value::DISPLAY_BLOCK},
+ {"run-in", css_display_value::DISPLAY_INLINE},
+ {"table", css_display_value::DISPLAY_BLOCK},
+ {"table-caption", css_display_value::DISPLAY_TABLE_ROW},
+ {"table-column-group", css_display_value::DISPLAY_TABLE_ROW},
+ {"table-header-group", css_display_value::DISPLAY_TABLE_ROW},
+ {"table-footer-group", css_display_value::DISPLAY_TABLE_ROW},
+ {"table-row-group", css_display_value::DISPLAY_TABLE_ROW},
+ {"table-cell", css_display_value::DISPLAY_TABLE_ROW},
+ {"table-column", css_display_value::DISPLAY_TABLE_ROW},
+ {"table-row", css_display_value::DISPLAY_TABLE_ROW},
+ {"initial", css_display_value::DISPLAY_INLINE},
+});
+
+auto css_value::maybe_display_from_string(const std::string_view &input)
+ -> std::optional<css_value>
+{
+ auto f = display_names_map.find(input);
+
+ if (f != display_names_map.end()) {
+ return css_value{f->second};
+ }
+
+ return std::nullopt;
+}
+
+
+auto css_value::debug_str() const -> std::string
+{
+ std::string ret;
+
+ std::visit([&](const auto &arg) {
+ using T = std::decay_t<decltype(arg)>;
+
+ if constexpr (std::is_same_v<T, css_color>) {
+ ret += fmt::format("color: r={};g={};b={};alpha={}",
+ arg.r, arg.g, arg.b, arg.alpha);
+ }
+ else if constexpr (std::is_same_v<T, double>) {
+ ret += "size: " + std::to_string(arg);
+ }
+ else if constexpr (std::is_same_v<T, css_dimension>) {
+ ret += "dimension: " + std::to_string(arg.dim);
+ if (arg.is_percent) {
+ ret += "%";
+ }
+ }
+ else if constexpr (std::is_same_v<T, css_display_value>) {
+ ret += "display: ";
+ switch (arg) {
+ case css_display_value::DISPLAY_HIDDEN:
+ ret += "hidden";
+ break;
+ case css_display_value::DISPLAY_BLOCK:
+ ret += "block";
+ break;
+ case css_display_value::DISPLAY_INLINE:
+ ret += "inline";
+ break;
+ case css_display_value::DISPLAY_TABLE_ROW:
+ ret += "table_row";
+ break;
+ }
+ }
+ else if constexpr (std::is_integral_v<T>) {
+ ret += "integral: " + std::to_string(static_cast<int>(arg));
+ }
+ else {
+ ret += "nyi";
+ }
+ },
+ value);
+
+ return ret;
+}
+
+TEST_SUITE("css"){
+ TEST_CASE("css hex colors"){
+ const std::pair<const char *, css_color> hex_tests[] = {
+ {"000", css_color(0, 0, 0)},
+ {"000000", css_color(0, 0, 0)},
+ {"f00", css_color(255, 0, 0)},
+ {"FEDCBA", css_color(254, 220, 186)},
+ {"234", css_color(34, 51, 68)},
+ };
+
+for (const auto &p: hex_tests) {
+ SUBCASE((std::string("parse hex color: ") + p.first).c_str())
+ {
+ auto col_parsed = css_value::maybe_color_from_hex(p.first);
+ //CHECK_UNARY(col_parsed);
+ //CHECK_UNARY(col_parsed.value().to_color());
+ auto final_col = col_parsed.value().to_color().value();
+ CHECK(final_col == p.second);
+ }
+}
+}// namespace rspamd::css
+TEST_CASE("css colors strings")
+{
+ auto passed = 0;
+ for (const auto &p: css_colors_map) {
+ /* Match some of the colors selected randomly */
+ if (rspamd_random_double_fast() > 0.9) {
+ auto col_parsed = css_value::maybe_color_from_string(p.first);
+ auto final_col = col_parsed.value().to_color().value();
+ CHECK_MESSAGE(final_col == p.second, p.first.data());
+ passed++;
+
+ if (passed > 20) {
+ break;
+ }
+ }
+ }
+}
+}
+;
+}
diff --git a/src/libserver/css/css_value.hxx b/src/libserver/css/css_value.hxx
new file mode 100644
index 0000000..1d57421
--- /dev/null
+++ b/src/libserver/css/css_value.hxx
@@ -0,0 +1,174 @@
+/*-
+ * Copyright 2021 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.
+ */
+
+#pragma once
+
+#ifndef RSPAMD_CSS_VALUE_HXX
+#define RSPAMD_CSS_VALUE_HXX
+
+#include <string>
+#include <variant>
+#include <optional>
+#include <vector>
+#include <iosfwd>
+#include "parse_error.hxx"
+#include "css_parser.hxx"
+#include "contrib/expected/expected.hpp"
+
+namespace rspamd::css {
+
+struct alignas(int) css_color {
+ std::uint8_t r;
+ std::uint8_t g;
+ std::uint8_t b;
+
+ std::uint8_t alpha;
+
+ css_color(std::uint8_t _r, std::uint8_t _g, std::uint8_t _b, std::uint8_t _alpha = 255)
+ : r(_r), g(_g), b(_b), alpha(_alpha)
+ {
+ }
+ css_color() = default;
+ constexpr auto to_number() const -> std::uint32_t
+ {
+ return (std::uint32_t) alpha << 24 |
+ (std::uint32_t) r << 16 |
+ (std::uint32_t) g << 8 |
+ (std::uint32_t) b << 0;
+ }
+
+ constexpr auto to_rgb() const -> std::uint32_t
+ {
+ return (std::uint32_t) r << 16 |
+ (std::uint32_t) g << 8 |
+ (std::uint32_t) b << 0;
+ }
+ friend bool operator==(const css_color &l, const css_color &r)
+ {
+ return (memcmp(&l, &r, sizeof(css_color)) == 0);
+ }
+
+ static auto white() -> css_color
+ {
+ return css_color{255, 255, 255};
+ }
+ static auto black() -> css_color
+ {
+ return css_color{0, 0, 0};
+ }
+};
+
+struct css_dimension {
+ float dim;
+ bool is_percent;
+};
+
+/*
+ * Simple enum class for display stuff
+ */
+enum class css_display_value : std::uint8_t {
+ DISPLAY_INLINE,
+ DISPLAY_BLOCK,
+ DISPLAY_TABLE_ROW,
+ DISPLAY_HIDDEN
+};
+
+/*
+ * Value handler, uses std::variant instead of polymorphic classes for now
+ * for simplicity
+ */
+struct css_value {
+ std::variant<css_color,
+ float,
+ css_display_value,
+ css_dimension,
+ std::monostate>
+ value;
+
+ css_value()
+ {
+ }
+ css_value(const css_color &color)
+ : value(color)
+ {
+ }
+ css_value(float num)
+ : value(num)
+ {
+ }
+ css_value(css_dimension dim)
+ : value(dim)
+ {
+ }
+ css_value(css_display_value d)
+ : value(d)
+ {
+ }
+
+ auto to_color(void) const -> std::optional<css_color>
+ {
+ return extract_value_maybe<css_color>();
+ }
+
+ auto to_number(void) const -> std::optional<float>
+ {
+ return extract_value_maybe<float>();
+ }
+
+ auto to_dimension(void) const -> std::optional<css_dimension>
+ {
+ return extract_value_maybe<css_dimension>();
+ }
+
+ auto to_display(void) const -> std::optional<css_display_value>
+ {
+ return extract_value_maybe<css_display_value>();
+ }
+
+ auto is_valid(void) const -> bool
+ {
+ return !(std::holds_alternative<std::monostate>(value));
+ }
+
+ auto debug_str() const -> std::string;
+
+ static auto maybe_color_from_string(const std::string_view &input)
+ -> std::optional<css_value>;
+ static auto maybe_color_from_hex(const std::string_view &input)
+ -> std::optional<css_value>;
+ static auto maybe_color_from_function(const css_consumed_block::css_function_block &func)
+ -> std::optional<css_value>;
+ static auto maybe_dimension_from_number(const css_parser_token &tok)
+ -> std::optional<css_value>;
+ static auto maybe_display_from_string(const std::string_view &input)
+ -> std::optional<css_value>;
+
+private:
+ template<typename T>
+ auto extract_value_maybe(void) const -> std::optional<T>
+ {
+ if (std::holds_alternative<T>(value)) {
+ return std::get<T>(value);
+ }
+
+ return std::nullopt;
+ }
+};
+
+}// namespace rspamd::css
+
+
+#endif//RSPAMD_CSS_VALUE_HXX
diff --git a/src/libserver/css/parse_error.hxx b/src/libserver/css/parse_error.hxx
new file mode 100644
index 0000000..22b76f0
--- /dev/null
+++ b/src/libserver/css/parse_error.hxx
@@ -0,0 +1,61 @@
+/*-
+ * Copyright 2021 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.
+ */
+
+#pragma once
+
+#ifndef RSPAMD_PARSE_ERROR_HXX
+#define RSPAMD_PARSE_ERROR_HXX
+
+#include <string>
+#include <optional>
+
+namespace rspamd::css {
+
+/*
+ * Generic parser errors
+ */
+enum class css_parse_error_type {
+ PARSE_ERROR_UNKNOWN_OPTION,
+ PARSE_ERROR_INVALID_SYNTAX,
+ PARSE_ERROR_BAD_NESTING,
+ PARSE_ERROR_NYI,
+ PARSE_ERROR_UNKNOWN_ERROR,
+ /* All above is treated as fatal error in parsing */
+ PARSE_ERROR_NO_ERROR,
+ PARSE_ERROR_EMPTY,
+};
+
+struct css_parse_error {
+ css_parse_error_type type = css_parse_error_type::PARSE_ERROR_UNKNOWN_ERROR;
+ std::optional<std::string> description;
+
+ explicit css_parse_error(css_parse_error_type type, const std::string &description)
+ : type(type), description(description)
+ {
+ }
+ explicit css_parse_error(css_parse_error_type type = css_parse_error_type::PARSE_ERROR_NO_ERROR)
+ : type(type)
+ {
+ }
+
+ constexpr auto is_fatal(void) const -> bool
+ {
+ return type < css_parse_error_type::PARSE_ERROR_NO_ERROR;
+ }
+};
+
+}// namespace rspamd::css
+#endif//RSPAMD_PARSE_ERROR_HXX
diff --git a/src/libserver/dkim.c b/src/libserver/dkim.c
new file mode 100644
index 0000000..4318e87
--- /dev/null
+++ b/src/libserver/dkim.c
@@ -0,0 +1,3588 @@
+/*-
+ * 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 "rspamd.h"
+#include "message.h"
+#include "dkim.h"
+#include "dns.h"
+#include "utlist.h"
+#include "unix-std.h"
+#include "mempool_vars_internal.h"
+
+#include <openssl/evp.h>
+#include <openssl/rsa.h>
+#include <openssl/engine.h>
+
+/* special DNS tokens */
+#define DKIM_DNSKEYNAME "_domainkey"
+
+/* ed25519 key lengths */
+#define ED25519_B64_BYTES 45
+#define ED25519_BYTES 32
+
+/* Canonization methods */
+#define DKIM_CANON_UNKNOWN (-1) /* unknown method */
+#define DKIM_CANON_SIMPLE 0 /* as specified in DKIM spec */
+#define DKIM_CANON_RELAXED 1 /* as specified in DKIM spec */
+
+#define DKIM_CANON_DEFAULT DKIM_CANON_SIMPLE
+
+#define RSPAMD_SHORT_BH_LEN 8
+
+/* Params */
+enum rspamd_dkim_param_type {
+ DKIM_PARAM_UNKNOWN = -1,
+ DKIM_PARAM_SIGNATURE = 0,
+ DKIM_PARAM_SIGNALG,
+ DKIM_PARAM_DOMAIN,
+ DKIM_PARAM_CANONALG,
+ DKIM_PARAM_QUERYMETHOD,
+ DKIM_PARAM_SELECTOR,
+ DKIM_PARAM_HDRLIST,
+ DKIM_PARAM_VERSION,
+ DKIM_PARAM_IDENTITY,
+ DKIM_PARAM_TIMESTAMP,
+ DKIM_PARAM_EXPIRATION,
+ DKIM_PARAM_COPIEDHDRS,
+ DKIM_PARAM_BODYHASH,
+ DKIM_PARAM_BODYLENGTH,
+ DKIM_PARAM_IDX,
+ DKIM_PARAM_CV,
+ DKIM_PARAM_IGNORE
+};
+
+#define RSPAMD_DKIM_MAX_ARC_IDX 10
+
+#define msg_err_dkim(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "dkim", ctx->pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_dkim(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ "dkim", ctx->pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_dkim(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ "dkim", ctx->pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_dkim(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_dkim_log_id, "dkim", ctx->pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_dkim_taskless(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_dkim_log_id, "dkim", "", \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+INIT_LOG_MODULE(dkim)
+
+#define RSPAMD_DKIM_FLAG_OVERSIGN (1u << 0u)
+#define RSPAMD_DKIM_FLAG_OVERSIGN_EXISTING (1u << 1u)
+
+union rspamd_dkim_header_stat {
+ struct _st {
+ guint16 count;
+ guint16 flags;
+ } s;
+ guint32 n;
+};
+
+struct rspamd_dkim_common_ctx {
+ rspamd_mempool_t *pool;
+ guint64 sig_hash;
+ gsize len;
+ GPtrArray *hlist;
+ GHashTable *htable; /* header -> count mapping */
+ EVP_MD_CTX *headers_hash;
+ EVP_MD_CTX *body_hash;
+ enum rspamd_dkim_type type;
+ guint idx;
+ gint header_canon_type;
+ gint body_canon_type;
+ guint body_canonicalised;
+ guint headers_canonicalised;
+ gboolean is_sign;
+};
+
+enum rspamd_arc_seal_cv {
+ RSPAMD_ARC_UNKNOWN = 0,
+ RSPAMD_ARC_NONE,
+ RSPAMD_ARC_INVALID,
+ RSPAMD_ARC_FAIL,
+ RSPAMD_ARC_PASS
+};
+
+
+struct rspamd_dkim_context_s {
+ struct rspamd_dkim_common_ctx common;
+ rspamd_mempool_t *pool;
+ struct rspamd_dns_resolver *resolver;
+ gsize blen;
+ gsize bhlen;
+ gint sig_alg;
+ guint ver;
+ time_t timestamp;
+ time_t expiration;
+ gchar *domain;
+ gchar *selector;
+ gint8 *b;
+ gchar *short_b;
+ gint8 *bh;
+ gchar *dns_key;
+ enum rspamd_arc_seal_cv cv;
+ const gchar *dkim_header;
+};
+
+#define RSPAMD_DKIM_KEY_ID_LEN 16
+
+struct rspamd_dkim_key_s {
+ guint8 *keydata;
+ guint8 *raw_key;
+ gsize keylen;
+ gsize decoded_len;
+ gchar key_id[RSPAMD_DKIM_KEY_ID_LEN];
+ union {
+ RSA *key_rsa;
+ EC_KEY *key_ecdsa;
+ guchar *key_eddsa;
+ } key;
+ BIO *key_bio;
+ EVP_PKEY *key_evp;
+ time_t mtime;
+ guint ttl;
+ enum rspamd_dkim_key_type type;
+ ref_entry_t ref;
+};
+
+struct rspamd_dkim_sign_context_s {
+ struct rspamd_dkim_common_ctx common;
+ rspamd_dkim_sign_key_t *key;
+};
+
+struct rspamd_dkim_header {
+ const gchar *name;
+ gint count;
+};
+
+/* Parser of dkim params */
+typedef gboolean (*dkim_parse_param_f)(rspamd_dkim_context_t *ctx,
+ const gchar *param, gsize len, GError **err);
+
+static gboolean rspamd_dkim_parse_signature(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+static gboolean rspamd_dkim_parse_signalg(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+static gboolean rspamd_dkim_parse_domain(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+static gboolean rspamd_dkim_parse_canonalg(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+static gboolean rspamd_dkim_parse_ignore(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+static gboolean rspamd_dkim_parse_selector(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+static gboolean rspamd_dkim_parse_hdrlist(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+static gboolean rspamd_dkim_parse_version(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+static gboolean rspamd_dkim_parse_timestamp(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+static gboolean rspamd_dkim_parse_expiration(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+static gboolean rspamd_dkim_parse_bodyhash(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+static gboolean rspamd_dkim_parse_bodylength(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+static gboolean rspamd_dkim_parse_idx(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+static gboolean rspamd_dkim_parse_cv(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err);
+
+
+static const dkim_parse_param_f parser_funcs[] = {
+ [DKIM_PARAM_SIGNATURE] = rspamd_dkim_parse_signature,
+ [DKIM_PARAM_SIGNALG] = rspamd_dkim_parse_signalg,
+ [DKIM_PARAM_DOMAIN] = rspamd_dkim_parse_domain,
+ [DKIM_PARAM_CANONALG] = rspamd_dkim_parse_canonalg,
+ [DKIM_PARAM_QUERYMETHOD] = rspamd_dkim_parse_ignore,
+ [DKIM_PARAM_SELECTOR] = rspamd_dkim_parse_selector,
+ [DKIM_PARAM_HDRLIST] = rspamd_dkim_parse_hdrlist,
+ [DKIM_PARAM_VERSION] = rspamd_dkim_parse_version,
+ [DKIM_PARAM_IDENTITY] = rspamd_dkim_parse_ignore,
+ [DKIM_PARAM_TIMESTAMP] = rspamd_dkim_parse_timestamp,
+ [DKIM_PARAM_EXPIRATION] = rspamd_dkim_parse_expiration,
+ [DKIM_PARAM_COPIEDHDRS] = rspamd_dkim_parse_ignore,
+ [DKIM_PARAM_BODYHASH] = rspamd_dkim_parse_bodyhash,
+ [DKIM_PARAM_BODYLENGTH] = rspamd_dkim_parse_bodylength,
+ [DKIM_PARAM_IDX] = rspamd_dkim_parse_idx,
+ [DKIM_PARAM_CV] = rspamd_dkim_parse_cv,
+ [DKIM_PARAM_IGNORE] = rspamd_dkim_parse_ignore,
+};
+
+#define DKIM_ERROR dkim_error_quark()
+GQuark
+dkim_error_quark(void)
+{
+ return g_quark_from_static_string("dkim-error-quark");
+}
+
+/* Parsers implementation */
+static gboolean
+rspamd_dkim_parse_signature(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+ ctx->b = rspamd_mempool_alloc0(ctx->pool, len);
+ ctx->short_b = rspamd_mempool_alloc0(ctx->pool, RSPAMD_SHORT_BH_LEN + 1);
+ rspamd_strlcpy(ctx->short_b, param, MIN(len, RSPAMD_SHORT_BH_LEN + 1));
+ (void) rspamd_cryptobox_base64_decode(param, len, ctx->b, &ctx->blen);
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_dkim_parse_signalg(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+ /* XXX: ugly size comparison, improve this code style some day */
+ if (len == 8) {
+ if (memcmp(param, "rsa-sha1", len) == 0) {
+ ctx->sig_alg = DKIM_SIGN_RSASHA1;
+ return TRUE;
+ }
+ }
+ else if (len == 10) {
+ if (memcmp(param, "rsa-sha256", len) == 0) {
+ ctx->sig_alg = DKIM_SIGN_RSASHA256;
+ return TRUE;
+ }
+ else if (memcmp(param, "rsa-sha512", len) == 0) {
+ ctx->sig_alg = DKIM_SIGN_RSASHA512;
+ return TRUE;
+ }
+ }
+ else if (len == 15) {
+ if (memcmp(param, "ecdsa256-sha256", len) == 0) {
+ ctx->sig_alg = DKIM_SIGN_ECDSASHA256;
+ return TRUE;
+ }
+ else if (memcmp(param, "ecdsa256-sha512", len) == 0) {
+ ctx->sig_alg = DKIM_SIGN_ECDSASHA512;
+ return TRUE;
+ }
+ }
+ else if (len == 14) {
+ if (memcmp(param, "ed25519-sha256", len) == 0) {
+ ctx->sig_alg = DKIM_SIGN_EDDSASHA256;
+ return TRUE;
+ }
+ }
+
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_INVALID_A,
+ "invalid dkim sign algorithm");
+ return FALSE;
+}
+
+static gboolean
+rspamd_dkim_parse_domain(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+ if (!rspamd_str_has_8bit(param, len)) {
+ ctx->domain = rspamd_mempool_alloc(ctx->pool, len + 1);
+ rspamd_strlcpy(ctx->domain, param, len + 1);
+ }
+ else {
+ ctx->domain = rspamd_dns_resolver_idna_convert_utf8(ctx->resolver,
+ ctx->pool, param, len, NULL);
+
+ if (!ctx->domain) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_INVALID_H,
+ "invalid dkim domain tag %.*s: idna failed",
+ (int) len, param);
+
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_dkim_parse_canonalg(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+ const gchar *p, *slash = NULL, *end = param + len;
+ gsize sl = 0;
+
+ p = param;
+ while (p != end) {
+ if (*p == '/') {
+ slash = p;
+ break;
+ }
+ p++;
+ sl++;
+ }
+
+ if (slash == NULL) {
+ /* Only check header */
+ if (len == 6 && memcmp(param, "simple", len) == 0) {
+ ctx->common.header_canon_type = DKIM_CANON_SIMPLE;
+ return TRUE;
+ }
+ else if (len == 7 && memcmp(param, "relaxed", len) == 0) {
+ ctx->common.header_canon_type = DKIM_CANON_RELAXED;
+ return TRUE;
+ }
+ }
+ else {
+ /* First check header */
+ if (sl == 6 && memcmp(param, "simple", sl) == 0) {
+ ctx->common.header_canon_type = DKIM_CANON_SIMPLE;
+ }
+ else if (sl == 7 && memcmp(param, "relaxed", sl) == 0) {
+ ctx->common.header_canon_type = DKIM_CANON_RELAXED;
+ }
+ else {
+ goto err;
+ }
+ /* Check body */
+ len -= sl + 1;
+ slash++;
+ if (len == 6 && memcmp(slash, "simple", len) == 0) {
+ ctx->common.body_canon_type = DKIM_CANON_SIMPLE;
+ return TRUE;
+ }
+ else if (len == 7 && memcmp(slash, "relaxed", len) == 0) {
+ ctx->common.body_canon_type = DKIM_CANON_RELAXED;
+ return TRUE;
+ }
+ }
+
+err:
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_INVALID_A,
+ "invalid dkim canonization algorithm");
+ return FALSE;
+}
+
+static gboolean
+rspamd_dkim_parse_ignore(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+ /* Just ignore unused params */
+ return TRUE;
+}
+
+static gboolean
+rspamd_dkim_parse_selector(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+
+ if (!rspamd_str_has_8bit(param, len)) {
+ ctx->selector = rspamd_mempool_alloc(ctx->pool, len + 1);
+ rspamd_strlcpy(ctx->selector, param, len + 1);
+ }
+ else {
+ ctx->selector = rspamd_dns_resolver_idna_convert_utf8(ctx->resolver,
+ ctx->pool, param, len, NULL);
+
+ if (!ctx->selector) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_INVALID_H,
+ "invalid dkim selector tag %.*s: idna failed",
+ (int) len, param);
+
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+static void
+rspamd_dkim_hlist_free(void *ud)
+{
+ GPtrArray *a = ud;
+
+ g_ptr_array_free(a, TRUE);
+}
+
+static gboolean
+rspamd_dkim_parse_hdrlist_common(struct rspamd_dkim_common_ctx *ctx,
+ const gchar *param,
+ gsize len,
+ gboolean sign,
+ GError **err)
+{
+ const gchar *c, *p, *end = param + len;
+ gchar *h;
+ gboolean from_found = FALSE, oversign, existing;
+ guint count = 0;
+ struct rspamd_dkim_header *new;
+ gpointer found;
+ union rspamd_dkim_header_stat u;
+
+ p = param;
+ while (p <= end) {
+ if ((p == end || *p == ':')) {
+ count++;
+ }
+ p++;
+ }
+
+ if (count > 0) {
+ ctx->hlist = g_ptr_array_sized_new(count);
+ }
+ else {
+ return FALSE;
+ }
+
+ c = param;
+ p = param;
+ ctx->htable = g_hash_table_new(rspamd_strcase_hash, rspamd_strcase_equal);
+
+ while (p <= end) {
+ if ((p == end || *p == ':') && p - c > 0) {
+ oversign = FALSE;
+ existing = FALSE;
+ h = rspamd_mempool_alloc(ctx->pool, p - c + 1);
+ rspamd_strlcpy(h, c, p - c + 1);
+
+ g_strstrip(h);
+
+ if (sign) {
+ if (rspamd_lc_cmp(h, "(o)", 3) == 0) {
+ oversign = TRUE;
+ h += 3;
+ msg_debug_dkim("oversign header: %s", h);
+ }
+ else if (rspamd_lc_cmp(h, "(x)", 3) == 0) {
+ oversign = TRUE;
+ existing = TRUE;
+ h += 3;
+ msg_debug_dkim("oversign existing header: %s", h);
+ }
+ }
+
+ /* Check mandatory from */
+ if (!from_found && g_ascii_strcasecmp(h, "from") == 0) {
+ from_found = TRUE;
+ }
+
+ new = rspamd_mempool_alloc(ctx->pool,
+ sizeof(struct rspamd_dkim_header));
+ new->name = h;
+ new->count = 0;
+ u.n = 0;
+
+ g_ptr_array_add(ctx->hlist, new);
+ found = g_hash_table_lookup(ctx->htable, h);
+
+ if (oversign) {
+ if (found) {
+ msg_err_dkim("specified oversigned header more than once: %s",
+ h);
+ }
+
+ u.s.flags |= RSPAMD_DKIM_FLAG_OVERSIGN;
+
+ if (existing) {
+ u.s.flags |= RSPAMD_DKIM_FLAG_OVERSIGN_EXISTING;
+ }
+
+ u.s.count = 0;
+ }
+ else {
+ if (found != NULL) {
+ u.n = GPOINTER_TO_UINT(found);
+ new->count = u.s.count;
+ u.s.count++;
+ }
+ else {
+ /* Insert new header order to the list */
+ u.s.count = new->count + 1;
+ }
+ }
+
+ g_hash_table_insert(ctx->htable, h, GUINT_TO_POINTER(u.n));
+
+ c = p + 1;
+ p++;
+ }
+ else {
+ p++;
+ }
+ }
+
+ if (!ctx->hlist) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_INVALID_H,
+ "invalid dkim header list");
+ return FALSE;
+ }
+ else {
+ if (!from_found) {
+ g_ptr_array_free(ctx->hlist, TRUE);
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_INVALID_H,
+ "invalid dkim header list, from header is missing");
+ return FALSE;
+ }
+
+ rspamd_mempool_add_destructor(ctx->pool,
+ (rspamd_mempool_destruct_t) rspamd_dkim_hlist_free,
+ ctx->hlist);
+ rspamd_mempool_add_destructor(ctx->pool,
+ (rspamd_mempool_destruct_t) g_hash_table_unref,
+ ctx->htable);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_dkim_parse_hdrlist(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+ return rspamd_dkim_parse_hdrlist_common(&ctx->common, param, len, FALSE, err);
+}
+
+static gboolean
+rspamd_dkim_parse_version(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+ if (len != 1 || *param != '1') {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_VERSION,
+ "invalid dkim version");
+ return FALSE;
+ }
+
+ ctx->ver = 1;
+ return TRUE;
+}
+
+static gboolean
+rspamd_dkim_parse_timestamp(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+ gulong val;
+
+ if (!rspamd_strtoul(param, len, &val)) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_UNKNOWN,
+ "invalid dkim timestamp");
+ return FALSE;
+ }
+ ctx->timestamp = val;
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_dkim_parse_expiration(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+ gulong val;
+
+ if (!rspamd_strtoul(param, len, &val)) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_UNKNOWN,
+ "invalid dkim expiration");
+ return FALSE;
+ }
+ ctx->expiration = val;
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_dkim_parse_bodyhash(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+ ctx->bh = rspamd_mempool_alloc0(ctx->pool, len);
+ (void) rspamd_cryptobox_base64_decode(param, len, ctx->bh, &ctx->bhlen);
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_dkim_parse_bodylength(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+ gulong val;
+
+ if (!rspamd_strtoul(param, len, &val)) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_INVALID_L,
+ "invalid dkim body length");
+ return FALSE;
+ }
+ ctx->common.len = val;
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_dkim_parse_idx(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+ gulong val;
+
+ if (!rspamd_strtoul(param, len, &val)) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_INVALID_L,
+ "invalid ARC idx");
+ return FALSE;
+ }
+ ctx->common.idx = val;
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_dkim_parse_cv(rspamd_dkim_context_t *ctx,
+ const gchar *param,
+ gsize len,
+ GError **err)
+{
+
+ /* Only check header */
+ if (len == 4 && memcmp(param, "fail", len) == 0) {
+ ctx->cv = RSPAMD_ARC_FAIL;
+ return TRUE;
+ }
+ else if (len == 4 && memcmp(param, "pass", len) == 0) {
+ ctx->cv = RSPAMD_ARC_PASS;
+ return TRUE;
+ }
+ else if (len == 4 && memcmp(param, "none", len) == 0) {
+ ctx->cv = RSPAMD_ARC_NONE;
+ return TRUE;
+ }
+ else if (len == 7 && memcmp(param, "invalid", len) == 0) {
+ ctx->cv = RSPAMD_ARC_INVALID;
+ return TRUE;
+ }
+
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_UNKNOWN,
+ "invalid arc seal verification result");
+
+ return FALSE;
+}
+
+
+static void
+rspamd_dkim_add_arc_seal_headers(rspamd_mempool_t *pool,
+ struct rspamd_dkim_common_ctx *ctx)
+{
+ struct rspamd_dkim_header *hdr;
+ gint count = ctx->idx, i;
+
+ ctx->hlist = g_ptr_array_sized_new(count * 3 - 1);
+
+ for (i = 0; i < count; i++) {
+ /* Authentication results */
+ hdr = rspamd_mempool_alloc(pool, sizeof(*hdr));
+ hdr->name = RSPAMD_DKIM_ARC_AUTHHEADER;
+ hdr->count = -(i + 1);
+ g_ptr_array_add(ctx->hlist, hdr);
+
+ /* Arc signature */
+ hdr = rspamd_mempool_alloc(pool, sizeof(*hdr));
+ hdr->name = RSPAMD_DKIM_ARC_SIGNHEADER;
+ hdr->count = -(i + 1);
+ g_ptr_array_add(ctx->hlist, hdr);
+
+ /* Arc seal (except last one) */
+ if (i != count - 1) {
+ hdr = rspamd_mempool_alloc(pool, sizeof(*hdr));
+ hdr->name = RSPAMD_DKIM_ARC_SEALHEADER;
+ hdr->count = -(i + 1);
+ g_ptr_array_add(ctx->hlist, hdr);
+ }
+ }
+
+ rspamd_mempool_add_destructor(ctx->pool,
+ (rspamd_mempool_destruct_t) rspamd_dkim_hlist_free,
+ ctx->hlist);
+}
+
+/**
+ * Create new dkim context from signature
+ * @param sig message's signature
+ * @param pool pool to allocate memory from
+ * @param err pointer to error object
+ * @return new context or NULL
+ */
+rspamd_dkim_context_t *
+rspamd_create_dkim_context(const gchar *sig,
+ rspamd_mempool_t *pool,
+ struct rspamd_dns_resolver *resolver,
+ guint time_jitter,
+ enum rspamd_dkim_type type,
+ GError **err)
+{
+ const gchar *p, *c, *tag = NULL, *end;
+ gint taglen;
+ gint param = DKIM_PARAM_UNKNOWN;
+ const EVP_MD *md_alg;
+ time_t now;
+ rspamd_dkim_context_t *ctx;
+ enum {
+ DKIM_STATE_TAG = 0,
+ DKIM_STATE_AFTER_TAG,
+ DKIM_STATE_VALUE,
+ DKIM_STATE_SKIP_SPACES = 99,
+ DKIM_STATE_ERROR = 100
+ } state,
+ next_state;
+
+
+ if (sig == NULL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_EMPTY_B,
+ "empty signature");
+ return NULL;
+ }
+
+ ctx = rspamd_mempool_alloc0(pool, sizeof(rspamd_dkim_context_t));
+ ctx->pool = pool;
+ ctx->resolver = resolver;
+
+ if (type == RSPAMD_DKIM_ARC_SEAL) {
+ ctx->common.header_canon_type = DKIM_CANON_RELAXED;
+ ctx->common.body_canon_type = DKIM_CANON_RELAXED;
+ }
+ else {
+ ctx->common.header_canon_type = DKIM_CANON_DEFAULT;
+ ctx->common.body_canon_type = DKIM_CANON_DEFAULT;
+ }
+
+ ctx->sig_alg = DKIM_SIGN_UNKNOWN;
+ ctx->common.pool = pool;
+ ctx->common.type = type;
+ /* A simple state machine of parsing tags */
+ state = DKIM_STATE_SKIP_SPACES;
+ next_state = DKIM_STATE_TAG;
+ taglen = 0;
+ p = sig;
+ c = sig;
+ end = p + strlen(p);
+ ctx->common.sig_hash = rspamd_cryptobox_fast_hash(sig, end - sig,
+ rspamd_hash_seed());
+
+ msg_debug_dkim("create dkim context sig = %L", ctx->common.sig_hash);
+
+ while (p <= end) {
+ switch (state) {
+ case DKIM_STATE_TAG:
+ if (g_ascii_isspace(*p)) {
+ taglen = (int) (p - c);
+ while (*p && g_ascii_isspace(*p)) {
+ /* Skip spaces before '=' sign */
+ p++;
+ }
+ if (*p != '=') {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_UNKNOWN,
+ "invalid dkim param");
+ state = DKIM_STATE_ERROR;
+ }
+ else {
+ state = DKIM_STATE_SKIP_SPACES;
+ next_state = DKIM_STATE_AFTER_TAG;
+ param = DKIM_PARAM_UNKNOWN;
+ p++;
+ tag = c;
+ }
+ }
+ else if (*p == '=') {
+ state = DKIM_STATE_SKIP_SPACES;
+ next_state = DKIM_STATE_AFTER_TAG;
+ param = DKIM_PARAM_UNKNOWN;
+ p++;
+ tag = c;
+ }
+ else {
+ taglen++;
+
+ if (taglen > G_MAXINT8) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_UNKNOWN,
+ "too long dkim tag");
+ state = DKIM_STATE_ERROR;
+ }
+ else {
+ p++;
+ }
+ }
+ break;
+ case DKIM_STATE_AFTER_TAG:
+ /* We got tag at tag and len at taglen */
+ switch (taglen) {
+ case 0:
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_UNKNOWN,
+ "zero length dkim param");
+ state = DKIM_STATE_ERROR;
+ break;
+ case 1:
+ /* 1 character tags */
+ switch (*tag) {
+ case 'v':
+ if (type == RSPAMD_DKIM_NORMAL) {
+ param = DKIM_PARAM_VERSION;
+ }
+ else {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_UNKNOWN,
+ "invalid ARC v param");
+ state = DKIM_STATE_ERROR;
+ break;
+ }
+ break;
+ case 'a':
+ param = DKIM_PARAM_SIGNALG;
+ break;
+ case 'b':
+ param = DKIM_PARAM_SIGNATURE;
+ break;
+ case 'c':
+ param = DKIM_PARAM_CANONALG;
+ break;
+ case 'd':
+ param = DKIM_PARAM_DOMAIN;
+ break;
+ case 'h':
+ if (type == RSPAMD_DKIM_ARC_SEAL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_UNKNOWN,
+ "ARC seal must NOT have h= tag");
+ state = DKIM_STATE_ERROR;
+ break;
+ }
+ else {
+ param = DKIM_PARAM_HDRLIST;
+ }
+ break;
+ case 'i':
+ if (type == RSPAMD_DKIM_NORMAL) {
+ param = DKIM_PARAM_IDENTITY;
+ }
+ else {
+ param = DKIM_PARAM_IDX;
+ }
+ break;
+ case 'l':
+ param = DKIM_PARAM_BODYLENGTH;
+ break;
+ case 'q':
+ param = DKIM_PARAM_QUERYMETHOD;
+ break;
+ case 's':
+ param = DKIM_PARAM_SELECTOR;
+ break;
+ case 't':
+ param = DKIM_PARAM_TIMESTAMP;
+ break;
+ case 'x':
+ param = DKIM_PARAM_EXPIRATION;
+ break;
+ case 'z':
+ param = DKIM_PARAM_COPIEDHDRS;
+ break;
+ case 'r':
+ param = DKIM_PARAM_IGNORE;
+ break;
+ default:
+ param = DKIM_PARAM_UNKNOWN;
+ msg_debug_dkim("unknown DKIM param %c, ignoring it", *tag);
+ break;
+ }
+ break;
+ case 2:
+ /* Two characters tags, e.g. `bh` */
+ if (tag[0] == 'b' && tag[1] == 'h') {
+ if (type == RSPAMD_DKIM_ARC_SEAL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_UNKNOWN,
+ "ARC seal must NOT have bh= tag");
+ state = DKIM_STATE_ERROR;
+ }
+ else {
+ param = DKIM_PARAM_BODYHASH;
+ }
+ }
+ else if (tag[0] == 'c' && tag[1] == 'v') {
+ if (type != RSPAMD_DKIM_ARC_SEAL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_UNKNOWN,
+ "cv tag is valid for ARC-Seal only");
+ state = DKIM_STATE_ERROR;
+ }
+ else {
+ param = DKIM_PARAM_CV;
+ }
+ }
+ else {
+ param = DKIM_PARAM_UNKNOWN;
+ msg_debug_dkim("unknown DKIM param %*s, ignoring it", taglen, tag);
+ }
+ break;
+ default:
+ /* Long and unknown (yet) DKIM tag */
+ param = DKIM_PARAM_UNKNOWN;
+ msg_debug_dkim("unknown DKIM param %*s, ignoring it", taglen, tag);
+ break;
+ }
+
+ if (state != DKIM_STATE_ERROR) {
+ /* Skip spaces */
+ state = DKIM_STATE_SKIP_SPACES;
+ next_state = DKIM_STATE_VALUE;
+ }
+ break;
+ case DKIM_STATE_VALUE:
+ if (*p == ';') {
+ if (p - c == 0 || c > p) {
+ state = DKIM_STATE_ERROR;
+ }
+ else {
+ /* Cut trailing spaces for value */
+ gint tlen = p - c;
+ const gchar *tmp = p - 1;
+
+ while (tlen > 0) {
+ if (!g_ascii_isspace(*tmp)) {
+ break;
+ }
+ tlen--;
+ tmp--;
+ }
+
+ if (param != DKIM_PARAM_UNKNOWN) {
+ if (!parser_funcs[param](ctx, c, tlen, err)) {
+ state = DKIM_STATE_ERROR;
+ }
+ else {
+ state = DKIM_STATE_SKIP_SPACES;
+ next_state = DKIM_STATE_TAG;
+ p++;
+ taglen = 0;
+ }
+ }
+ else {
+ /* Unknown param has been ignored */
+ msg_debug_dkim("ignored unknown tag parameter value: %*s = %*s",
+ taglen, tag, tlen, c);
+ state = DKIM_STATE_SKIP_SPACES;
+ next_state = DKIM_STATE_TAG;
+ p++;
+ taglen = 0;
+ }
+ }
+ }
+ else if (p == end) {
+ /* Last parameter with no `;` character */
+ gint tlen = p - c;
+ const gchar *tmp = p - 1;
+
+ while (tlen > 0) {
+ if (!g_ascii_isspace(*tmp)) {
+ break;
+ }
+ tlen--;
+ tmp--;
+ }
+
+ if (param != DKIM_PARAM_UNKNOWN) {
+ if (!parser_funcs[param](ctx, c, tlen, err)) {
+ state = DKIM_STATE_ERROR;
+ }
+ }
+ else {
+ msg_debug_dkim("ignored unknown tag parameter value: %*s: %*s",
+ taglen, tag, tlen, c);
+ }
+
+ if (state == DKIM_STATE_ERROR) {
+ /*
+ * We need to return from here as state machine won't
+ * do any more steps after p == end
+ */
+ if (err) {
+ msg_info_dkim("dkim parse failed: %e", *err);
+ }
+
+ return NULL;
+ }
+ /* Finish processing */
+ p++;
+ }
+ else {
+ p++;
+ }
+ break;
+ case DKIM_STATE_SKIP_SPACES:
+ if (g_ascii_isspace(*p)) {
+ p++;
+ }
+ else {
+ c = p;
+ state = next_state;
+ }
+ break;
+ case DKIM_STATE_ERROR:
+ if (err && *err) {
+ msg_info_dkim("dkim parse failed: %s", (*err)->message);
+ return NULL;
+ }
+ else {
+ msg_info_dkim("dkim parse failed: unknown error when parsing %c tag",
+ tag ? *tag : '?');
+ return NULL;
+ }
+ break;
+ }
+ }
+
+ if (type == RSPAMD_DKIM_ARC_SEAL) {
+ rspamd_dkim_add_arc_seal_headers(pool, &ctx->common);
+ }
+
+ /* Now check validity of signature */
+ if (ctx->b == NULL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_EMPTY_B,
+ "b parameter missing");
+ return NULL;
+ }
+ if (ctx->common.type != RSPAMD_DKIM_ARC_SEAL && ctx->bh == NULL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_EMPTY_BH,
+ "bh parameter missing");
+ return NULL;
+ }
+ if (ctx->domain == NULL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_EMPTY_D,
+ "domain parameter missing");
+ return NULL;
+ }
+ if (ctx->selector == NULL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_EMPTY_S,
+ "selector parameter missing");
+ return NULL;
+ }
+ if (ctx->common.type == RSPAMD_DKIM_NORMAL && ctx->ver == 0) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_EMPTY_V,
+ "v parameter missing");
+ return NULL;
+ }
+ if (ctx->common.hlist == NULL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_EMPTY_H,
+ "h parameter missing");
+ return NULL;
+ }
+ if (ctx->sig_alg == DKIM_SIGN_UNKNOWN) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_EMPTY_S,
+ "s parameter missing");
+ return NULL;
+ }
+
+ if (type != RSPAMD_DKIM_ARC_SEAL) {
+ if (ctx->sig_alg == DKIM_SIGN_RSASHA1) {
+ /* Check bh length */
+ if (ctx->bhlen != (guint) EVP_MD_size(EVP_sha1())) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_BADSIG,
+ "signature has incorrect length: %zu",
+ ctx->bhlen);
+ return NULL;
+ }
+ }
+ else if (ctx->sig_alg == DKIM_SIGN_RSASHA256 ||
+ ctx->sig_alg == DKIM_SIGN_ECDSASHA256) {
+ if (ctx->bhlen !=
+ (guint) EVP_MD_size(EVP_sha256())) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_BADSIG,
+ "signature has incorrect length: %zu",
+ ctx->bhlen);
+ return NULL;
+ }
+ }
+ else if (ctx->sig_alg == DKIM_SIGN_RSASHA512 ||
+ ctx->sig_alg == DKIM_SIGN_ECDSASHA512) {
+ if (ctx->bhlen !=
+ (guint) EVP_MD_size(EVP_sha512())) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_BADSIG,
+ "signature has incorrect length: %zu",
+ ctx->bhlen);
+ return NULL;
+ }
+ }
+ }
+
+ /* Check expiration */
+ now = time(NULL);
+ if (ctx->timestamp && now < ctx->timestamp && ctx->timestamp - now > (gint) time_jitter) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_FUTURE,
+ "signature was made in future, ignoring");
+ return NULL;
+ }
+ if (ctx->expiration && ctx->expiration < now) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_EXPIRED,
+ "signature has expired");
+ return NULL;
+ }
+
+ if (ctx->common.type != RSPAMD_DKIM_NORMAL && (ctx->common.idx == 0 ||
+ ctx->common.idx > RSPAMD_DKIM_MAX_ARC_IDX)) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_UNKNOWN,
+ "i parameter missing or invalid for ARC");
+ return NULL;
+ }
+
+ if (ctx->common.type == RSPAMD_DKIM_ARC_SEAL) {
+ if (ctx->cv == RSPAMD_ARC_UNKNOWN) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_UNKNOWN,
+ "cv parameter missing or invalid for ARC");
+ return NULL;
+ }
+ }
+
+ /* Now create dns key to request further */
+ gsize dnslen = strlen(ctx->domain) + strlen(ctx->selector) +
+ sizeof(DKIM_DNSKEYNAME) + 2;
+ ctx->dns_key = rspamd_mempool_alloc(ctx->pool, dnslen);
+ rspamd_snprintf(ctx->dns_key,
+ dnslen,
+ "%s.%s.%s",
+ ctx->selector,
+ DKIM_DNSKEYNAME,
+ ctx->domain);
+
+ /* Create checksums for further operations */
+ if (ctx->sig_alg == DKIM_SIGN_RSASHA1) {
+ md_alg = EVP_sha1();
+ }
+ else if (ctx->sig_alg == DKIM_SIGN_RSASHA256 ||
+ ctx->sig_alg == DKIM_SIGN_ECDSASHA256 ||
+ ctx->sig_alg == DKIM_SIGN_EDDSASHA256) {
+ md_alg = EVP_sha256();
+ }
+ else if (ctx->sig_alg == DKIM_SIGN_RSASHA512 ||
+ ctx->sig_alg == DKIM_SIGN_ECDSASHA512) {
+ md_alg = EVP_sha512();
+ }
+ else {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_BADSIG,
+ "signature has unsupported signature algorithm");
+
+ return NULL;
+ }
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
+ ctx->common.body_hash = EVP_MD_CTX_create();
+ EVP_DigestInit_ex(ctx->common.body_hash, md_alg, NULL);
+ ctx->common.headers_hash = EVP_MD_CTX_create();
+ EVP_DigestInit_ex(ctx->common.headers_hash, md_alg, NULL);
+ rspamd_mempool_add_destructor(pool,
+ (rspamd_mempool_destruct_t) EVP_MD_CTX_destroy, ctx->common.body_hash);
+ rspamd_mempool_add_destructor(pool,
+ (rspamd_mempool_destruct_t) EVP_MD_CTX_destroy, ctx->common.headers_hash);
+#else
+ ctx->common.body_hash = EVP_MD_CTX_new();
+ EVP_DigestInit_ex(ctx->common.body_hash, md_alg, NULL);
+ ctx->common.headers_hash = EVP_MD_CTX_new();
+ EVP_DigestInit_ex(ctx->common.headers_hash, md_alg, NULL);
+ rspamd_mempool_add_destructor(pool,
+ (rspamd_mempool_destruct_t) EVP_MD_CTX_free, ctx->common.body_hash);
+ rspamd_mempool_add_destructor(pool,
+ (rspamd_mempool_destruct_t) EVP_MD_CTX_free, ctx->common.headers_hash);
+#endif
+ ctx->dkim_header = sig;
+
+ return ctx;
+}
+
+struct rspamd_dkim_key_cbdata {
+ rspamd_dkim_context_t *ctx;
+ dkim_key_handler_f handler;
+ gpointer ud;
+};
+
+rspamd_dkim_key_t *
+rspamd_dkim_make_key(const gchar *keydata,
+ guint keylen, enum rspamd_dkim_key_type type, GError **err)
+{
+ rspamd_dkim_key_t *key = NULL;
+
+ if (keylen < 3) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_KEYFAIL,
+ "DKIM key is too short to be valid");
+ return NULL;
+ }
+
+ key = g_malloc0(sizeof(rspamd_dkim_key_t));
+ REF_INIT_RETAIN(key, rspamd_dkim_key_free);
+ key->keydata = g_malloc0(keylen + 1);
+ key->raw_key = g_malloc(keylen);
+ key->decoded_len = keylen;
+ key->type = type;
+
+ /* Copy key skipping all spaces and newlines */
+ const char *h = keydata;
+ guint8 *t = key->raw_key;
+
+ while (h - keydata < keylen) {
+ if (!g_ascii_isspace(*h)) {
+ *t++ = *h++;
+ }
+ else {
+ h++;
+ }
+ }
+
+ key->keylen = t - key->raw_key;
+
+ if (!rspamd_cryptobox_base64_decode(key->raw_key, key->keylen, key->keydata,
+ &key->decoded_len)) {
+ REF_RELEASE(key);
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_KEYFAIL,
+ "DKIM key is not a valid base64 string");
+
+ return NULL;
+ }
+
+ /* Calculate ID -> md5 */
+ EVP_MD_CTX *mdctx = EVP_MD_CTX_create();
+
+#ifdef EVP_MD_CTX_FLAG_NON_FIPS_ALLOW
+ EVP_MD_CTX_set_flags(mdctx, EVP_MD_CTX_FLAG_NON_FIPS_ALLOW);
+#endif
+
+ if (EVP_DigestInit_ex(mdctx, EVP_md5(), NULL) == 1) {
+ guint dlen = sizeof(key->key_id);
+
+ EVP_DigestUpdate(mdctx, key->keydata, key->decoded_len);
+ EVP_DigestFinal_ex(mdctx, key->key_id, &dlen);
+ }
+
+ EVP_MD_CTX_destroy(mdctx);
+
+ if (key->type == RSPAMD_DKIM_KEY_EDDSA) {
+ key->key.key_eddsa = key->keydata;
+
+ if (key->decoded_len != rspamd_cryptobox_pk_sig_bytes(
+ RSPAMD_CRYPTOBOX_MODE_25519)) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_KEYFAIL,
+ "DKIM key is has invalid length %d for eddsa; expected %d",
+ (gint) key->decoded_len,
+ rspamd_cryptobox_pk_sig_bytes(RSPAMD_CRYPTOBOX_MODE_25519));
+ REF_RELEASE(key);
+
+ return NULL;
+ }
+ }
+ else {
+ key->key_bio = BIO_new_mem_buf(key->keydata, key->decoded_len);
+
+ if (key->key_bio == NULL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_KEYFAIL,
+ "cannot make ssl bio from key");
+ REF_RELEASE(key);
+
+ return NULL;
+ }
+
+ key->key_evp = d2i_PUBKEY_bio(key->key_bio, NULL);
+
+ if (key->key_evp == NULL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_KEYFAIL,
+ "cannot extract pubkey from bio");
+ REF_RELEASE(key);
+
+ return NULL;
+ }
+
+ if (type == RSPAMD_DKIM_KEY_RSA) {
+ key->key.key_rsa = EVP_PKEY_get1_RSA(key->key_evp);
+
+ if (key->key.key_rsa == NULL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_KEYFAIL,
+ "cannot extract rsa key from evp key");
+ REF_RELEASE(key);
+
+ return NULL;
+ }
+ }
+ else {
+ key->key.key_ecdsa = EVP_PKEY_get1_EC_KEY(key->key_evp);
+
+ if (key->key.key_ecdsa == NULL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_KEYFAIL,
+ "cannot extract ecdsa key from evp key");
+ REF_RELEASE(key);
+
+ return NULL;
+ }
+ }
+ }
+
+ return key;
+}
+
+const guchar *
+rspamd_dkim_key_id(rspamd_dkim_key_t *key)
+{
+ if (key) {
+ return key->key_id;
+ }
+
+ return NULL;
+}
+
+/**
+ * Free DKIM key
+ * @param key
+ */
+void rspamd_dkim_key_free(rspamd_dkim_key_t *key)
+{
+ if (key->key_evp) {
+ EVP_PKEY_free(key->key_evp);
+ }
+
+ if (key->type == RSPAMD_DKIM_KEY_RSA) {
+ if (key->key.key_rsa) {
+ RSA_free(key->key.key_rsa);
+ }
+ }
+ else if (key->type == RSPAMD_DKIM_KEY_ECDSA) {
+ if (key->key.key_ecdsa) {
+ EC_KEY_free(key->key.key_ecdsa);
+ }
+ }
+ /* Nothing in case of eddsa key */
+ if (key->key_bio) {
+ BIO_free(key->key_bio);
+ }
+
+ g_free(key->raw_key);
+ g_free(key->keydata);
+ g_free(key);
+}
+
+void rspamd_dkim_sign_key_free(rspamd_dkim_sign_key_t *key)
+{
+ if (key->key_evp) {
+ EVP_PKEY_free(key->key_evp);
+ }
+ if (key->type == RSPAMD_DKIM_KEY_RSA) {
+ if (key->key.key_rsa) {
+ RSA_free(key->key.key_rsa);
+ }
+ }
+ if (key->key_bio) {
+ BIO_free(key->key_bio);
+ }
+
+ if (key->type == RSPAMD_DKIM_KEY_EDDSA) {
+ rspamd_explicit_memzero(key->key.key_eddsa, key->keylen);
+ g_free(key->keydata);
+ }
+
+ g_free(key);
+}
+
+rspamd_dkim_key_t *
+rspamd_dkim_parse_key(const gchar *txt, gsize *keylen, GError **err)
+{
+ const gchar *c, *p, *end, *key = NULL, *alg = "rsa";
+ enum {
+ read_tag = 0,
+ read_tag_before_eqsign,
+ read_eqsign,
+ read_p_tag,
+ read_k_tag,
+ ignore_value,
+ skip_spaces,
+ } state = read_tag,
+ next_state;
+ gchar tag = '\0';
+ gsize klen = 0, alglen = 0;
+
+ c = txt;
+ p = txt;
+ end = txt + strlen(txt);
+
+ while (p < end) {
+ switch (state) {
+ case read_tag:
+ if (*p == '=') {
+ state = read_eqsign;
+ }
+ else if (g_ascii_isspace(*p)) {
+ state = skip_spaces;
+
+ if (tag != '\0') {
+ /* We had tag letter */
+ next_state = read_tag_before_eqsign;
+ }
+ else {
+ /* We had no tag letter, so we ignore empty tag */
+ next_state = read_tag;
+ }
+ }
+ else {
+ tag = *p;
+ }
+ p++;
+ break;
+ case read_tag_before_eqsign:
+ /* Input: spaces before eqsign
+ * Output: either read a next tag (previous had no value), or read value
+ * p is moved forward
+ */
+ if (*p == '=') {
+ state = read_eqsign;
+ }
+ else {
+ tag = *p;
+ state = read_tag;
+ }
+ p++;
+ break;
+ case read_eqsign:
+ /* Always switch to skip spaces state and do not advance p */
+ state = skip_spaces;
+
+ if (tag == 'p') {
+ next_state = read_p_tag;
+ }
+ else if (tag == 'k') {
+ next_state = read_k_tag;
+ }
+ else {
+ /* Unknown tag, ignore */
+ next_state = ignore_value;
+ tag = '\0';
+ }
+ break;
+ case read_p_tag:
+ if (*p == ';') {
+ klen = p - c;
+ key = c;
+ state = read_tag;
+ tag = '\0';
+ p++;
+ }
+ else {
+ p++;
+ }
+ break;
+ case read_k_tag:
+ if (*p == ';') {
+ alglen = p - c;
+ alg = c;
+ state = read_tag;
+ tag = '\0';
+ p++;
+ }
+ else if (g_ascii_isspace(*p)) {
+ alglen = p - c;
+ alg = c;
+ state = skip_spaces;
+ next_state = read_tag;
+ tag = '\0';
+ }
+ else {
+ p++;
+ }
+ break;
+ case ignore_value:
+ if (*p == ';') {
+ state = read_tag;
+ tag = '\0';
+ p++;
+ }
+ else if (g_ascii_isspace(*p)) {
+ state = skip_spaces;
+ next_state = read_tag;
+ tag = '\0';
+ }
+ else {
+ p++;
+ }
+ break;
+ case skip_spaces:
+ /* Skip spaces and switch to the next state if needed */
+ if (g_ascii_isspace(*p)) {
+ p++;
+ }
+ else {
+ c = p;
+ state = next_state;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* Leftover */
+ switch (state) {
+ case read_p_tag:
+ klen = p - c;
+ key = c;
+ break;
+ case read_k_tag:
+ alglen = p - c;
+ alg = c;
+ break;
+ default:
+ break;
+ }
+
+ if (klen == 0 || key == NULL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_KEYFAIL,
+ "key is missing");
+
+ return NULL;
+ }
+
+ if (alglen == 0 || alg == NULL) {
+ alg = "rsa"; /* Implicit */
+ alglen = 3;
+ }
+
+ if (keylen) {
+ *keylen = klen;
+ }
+
+ if (alglen == 8 && rspamd_lc_cmp(alg, "ecdsa256", alglen) == 0) {
+ return rspamd_dkim_make_key(key, klen,
+ RSPAMD_DKIM_KEY_ECDSA, err);
+ }
+ else if (alglen == 7 && rspamd_lc_cmp(alg, "ed25519", alglen) == 0) {
+ return rspamd_dkim_make_key(key, klen,
+ RSPAMD_DKIM_KEY_EDDSA, err);
+ }
+ else {
+ /* We assume RSA default in all cases */
+ return rspamd_dkim_make_key(key, klen,
+ RSPAMD_DKIM_KEY_RSA, err);
+ }
+
+ g_assert_not_reached();
+
+ return NULL;
+}
+
+/* Get TXT request data and parse it */
+static void
+rspamd_dkim_dns_cb(struct rdns_reply *reply, gpointer arg)
+{
+ struct rspamd_dkim_key_cbdata *cbdata = arg;
+ rspamd_dkim_key_t *key = NULL;
+ GError *err = NULL;
+ struct rdns_reply_entry *elt;
+ gsize keylen = 0;
+
+ if (reply->code != RDNS_RC_NOERROR) {
+ gint err_code = DKIM_SIGERROR_NOKEY;
+ if (reply->code == RDNS_RC_NOREC) {
+ err_code = DKIM_SIGERROR_NOREC;
+ }
+ else if (reply->code == RDNS_RC_NXDOMAIN) {
+ err_code = DKIM_SIGERROR_NOREC;
+ }
+ g_set_error(&err,
+ DKIM_ERROR,
+ err_code,
+ "dns request to %s failed: %s",
+ cbdata->ctx->dns_key,
+ rdns_strerror(reply->code));
+ cbdata->handler(NULL, 0, cbdata->ctx, cbdata->ud, err);
+ }
+ else {
+ LL_FOREACH(reply->entries, elt)
+ {
+ if (elt->type == RDNS_REQUEST_TXT) {
+ if (err != NULL) {
+ /* Free error as it is insignificant */
+ g_error_free(err);
+ err = NULL;
+ }
+ key = rspamd_dkim_parse_key(elt->content.txt.data,
+ &keylen,
+ &err);
+ if (key) {
+ key->ttl = elt->ttl;
+ break;
+ }
+ }
+ }
+ cbdata->handler(key, keylen, cbdata->ctx, cbdata->ud, err);
+ }
+}
+
+/**
+ * Make DNS request for specified context and obtain and parse key
+ * @param ctx dkim context from signature
+ * @param resolver dns resolver object
+ * @param s async session to make request
+ * @return
+ */
+gboolean
+rspamd_get_dkim_key(rspamd_dkim_context_t *ctx,
+ struct rspamd_task *task,
+ dkim_key_handler_f handler,
+ gpointer ud)
+{
+ struct rspamd_dkim_key_cbdata *cbdata;
+
+ g_return_val_if_fail(ctx != NULL, FALSE);
+ g_return_val_if_fail(ctx->dns_key != NULL, FALSE);
+
+ cbdata =
+ rspamd_mempool_alloc(ctx->pool,
+ sizeof(struct rspamd_dkim_key_cbdata));
+ cbdata->ctx = ctx;
+ cbdata->handler = handler;
+ cbdata->ud = ud;
+
+ return rspamd_dns_resolver_request_task_forced(task,
+ rspamd_dkim_dns_cb,
+ cbdata,
+ RDNS_REQUEST_TXT,
+ ctx->dns_key);
+}
+
+static gboolean
+rspamd_dkim_relaxed_body_step(struct rspamd_dkim_common_ctx *ctx, EVP_MD_CTX *ck,
+ const gchar **start, guint size,
+ gssize *remain)
+{
+ const gchar *h;
+ gchar *t;
+ guint len, inlen;
+ gssize octets_remain;
+ gboolean got_sp, ret = TRUE;
+ gchar buf[1024];
+
+ len = size;
+ inlen = sizeof(buf) - 1;
+ h = *start;
+ t = buf;
+ got_sp = FALSE;
+ octets_remain = *remain;
+
+ while (len > 0 && inlen > 0 && (octets_remain > 0)) {
+
+ if (*h == '\r' || *h == '\n') {
+ if (got_sp) {
+ /* Ignore spaces at the end of line */
+ t--;
+ }
+ *t++ = '\r';
+ *t++ = '\n';
+
+ if (len > 1 && (*h == '\r' && h[1] == '\n')) {
+ h += 2;
+ len -= 2;
+ octets_remain -= 2;
+ }
+ else {
+ h++;
+ len--;
+ if (octets_remain >= 2) {
+ octets_remain -= 2; /* Input has just \n or \r so we actually add more octets */
+ }
+ else {
+ octets_remain--;
+ break;
+ }
+ }
+ break;
+ }
+ else if (g_ascii_isspace(*h)) {
+ if (got_sp) {
+ /* Ignore multiply spaces */
+ h++;
+ len--;
+ continue;
+ }
+ else {
+ *t++ = ' ';
+ h++;
+ inlen--;
+ len--;
+ octets_remain--;
+ got_sp = TRUE;
+ continue;
+ }
+ }
+ else {
+ got_sp = FALSE;
+ }
+
+ *t++ = *h++;
+ inlen--;
+ len--;
+ octets_remain--;
+ }
+
+ if (octets_remain < 0) {
+ /* Absurdic l tag value, but we still need to rewind the t pointer back */
+ while (t > buf && octets_remain < 0) {
+ t--;
+ octets_remain++;
+ }
+
+ ret = FALSE;
+ }
+
+ *start = h;
+
+ if (t - buf > 0) {
+ gsize cklen = t - buf;
+
+ EVP_DigestUpdate(ck, buf, cklen);
+ ctx->body_canonicalised += cklen;
+ msg_debug_dkim("relaxed update signature with body buffer "
+ "(%z size, %z -> %z remain)",
+ cklen, *remain, octets_remain);
+ *remain = octets_remain;
+ }
+
+ return ret && ((len > 0) && (octets_remain > 0));
+}
+
+static gboolean
+rspamd_dkim_simple_body_step(struct rspamd_dkim_common_ctx *ctx,
+ EVP_MD_CTX *ck, const gchar **start, guint size,
+ gssize *remain)
+{
+ const gchar *h;
+ gchar *t;
+ guint len, inlen;
+ gssize octets_remain;
+ gchar buf[1024];
+
+ len = size;
+ inlen = sizeof(buf) - 1;
+ h = *start;
+ t = &buf[0];
+ octets_remain = *remain;
+
+ while (len > 0 && inlen > 0 && (octets_remain != 0)) {
+ if (*h == '\r' || *h == '\n') {
+ *t++ = '\r';
+ *t++ = '\n';
+
+ if (len > 1 && (*h == '\r' && h[1] == '\n')) {
+ h += 2;
+ len -= 2;
+
+ if (octets_remain >= 2) {
+ octets_remain -= 2; /* Input has just \n or \r so we actually add more octets */
+ }
+ else {
+ octets_remain--;
+ }
+ }
+ else {
+ h++;
+ len--;
+
+ if (octets_remain >= 2) {
+ octets_remain -= 2; /* Input has just \n or \r so we actually add more octets */
+ }
+ else {
+ octets_remain--;
+ }
+ }
+ break;
+ }
+
+ *t++ = *h++;
+ octets_remain--;
+ inlen--;
+ len--;
+ }
+
+ *start = h;
+
+ if (t - buf > 0) {
+ gsize cklen = t - buf;
+
+ EVP_DigestUpdate(ck, buf, cklen);
+ ctx->body_canonicalised += cklen;
+ msg_debug_dkim("simple update signature with body buffer "
+ "(%z size, %z -> %z remain)",
+ cklen, *remain, octets_remain);
+ *remain = octets_remain;
+ }
+
+ return ((len != 0) && (octets_remain != 0));
+}
+
+static const gchar *
+rspamd_dkim_skip_empty_lines(const gchar *start, const gchar *end,
+ guint type, gboolean sign, gboolean *need_crlf)
+{
+ const gchar *p = end - 1, *t;
+ enum {
+ init = 0,
+ init_2,
+ got_cr,
+ got_lf,
+ got_crlf,
+ test_spaces,
+ } state = init;
+ guint skip = 0;
+
+ while (p >= start) {
+ switch (state) {
+ case init:
+ if (*p == '\r') {
+ state = got_cr;
+ }
+ else if (*p == '\n') {
+ state = got_lf;
+ }
+ else if (type == DKIM_CANON_RELAXED && *p == ' ') {
+ skip = 0;
+ state = test_spaces;
+ }
+ else {
+ if (sign || type != DKIM_CANON_RELAXED) {
+ *need_crlf = TRUE;
+ }
+
+ goto end;
+ }
+ break;
+ case init_2:
+ if (*p == '\r') {
+ state = got_cr;
+ }
+ else if (*p == '\n') {
+ state = got_lf;
+ }
+ else if (type == DKIM_CANON_RELAXED && (*p == ' ' || *p == '\t')) {
+ skip = 0;
+ state = test_spaces;
+ }
+ else {
+ goto end;
+ }
+ break;
+ case got_cr:
+ if (p >= start + 1) {
+ if (*(p - 1) == '\r') {
+ p--;
+ state = got_cr;
+ }
+ else if (*(p - 1) == '\n') {
+ if ((*p - 2) == '\r') {
+ /* \r\n\r -> we know about one line */
+ p -= 1;
+ state = got_crlf;
+ }
+ else {
+ /* \n\r -> we know about one line */
+ p -= 1;
+ state = got_lf;
+ }
+ }
+ else if (type == DKIM_CANON_RELAXED && (*(p - 1) == ' ' ||
+ *(p - 1) == '\t')) {
+ skip = 1;
+ state = test_spaces;
+ }
+ else {
+ goto end;
+ }
+ }
+ else {
+ if (g_ascii_isspace(*(p - 1))) {
+ if (type == DKIM_CANON_RELAXED) {
+ p -= 1;
+ }
+ }
+ goto end;
+ }
+ break;
+ case got_lf:
+ if (p >= start + 1) {
+ if (*(p - 1) == '\r') {
+ state = got_crlf;
+ }
+ else if (*(p - 1) == '\n') {
+ /* We know about one line */
+ p--;
+ state = got_lf;
+ }
+ else if (type == DKIM_CANON_RELAXED && (*(p - 1) == ' ' ||
+ *(p - 1) == '\t')) {
+ skip = 1;
+ state = test_spaces;
+ }
+ else {
+ goto end;
+ }
+ }
+ else {
+ if (g_ascii_isspace(*(p - 1))) {
+ if (type == DKIM_CANON_RELAXED) {
+ p -= 1;
+ }
+ }
+ goto end;
+ }
+ break;
+ case got_crlf:
+ if (p >= start + 2) {
+ if (*(p - 2) == '\r') {
+ p -= 2;
+ state = got_cr;
+ }
+ else if (*(p - 2) == '\n') {
+ p -= 2;
+ state = got_lf;
+ }
+ else if (type == DKIM_CANON_RELAXED && (*(p - 2) == ' ' ||
+ *(p - 2) == '\t')) {
+ skip = 2;
+ state = test_spaces;
+ }
+ else {
+ goto end;
+ }
+ }
+ else {
+ if (g_ascii_isspace(*(p - 2))) {
+ if (type == DKIM_CANON_RELAXED) {
+ p -= 2;
+ }
+ }
+ goto end;
+ }
+ break;
+ case test_spaces:
+ t = p - skip;
+
+ while (t >= start + 2 && (*t == ' ' || *t == '\t')) {
+ t--;
+ }
+
+ if (*t == '\r') {
+ p = t;
+ state = got_cr;
+ }
+ else if (*t == '\n') {
+ p = t;
+ state = got_lf;
+ }
+ else {
+ goto end;
+ }
+ break;
+ }
+ }
+
+end:
+ return p;
+}
+
+static gboolean
+rspamd_dkim_canonize_body(struct rspamd_dkim_common_ctx *ctx,
+ const gchar *start,
+ const gchar *end,
+ gboolean sign)
+{
+ const gchar *p;
+ gssize remain = ctx->len ? ctx->len : G_MAXSSIZE;
+ guint total_len = end - start;
+ gboolean need_crlf = FALSE;
+
+ if (start == NULL) {
+ /* Empty body */
+ if (ctx->body_canon_type == DKIM_CANON_SIMPLE) {
+ EVP_DigestUpdate(ctx->body_hash, CRLF, sizeof(CRLF) - 1);
+ ctx->body_canonicalised += sizeof(CRLF) - 1;
+ }
+ else {
+ EVP_DigestUpdate(ctx->body_hash, "", 0);
+ }
+ }
+ else {
+ /* Strip extra ending CRLF */
+ p = rspamd_dkim_skip_empty_lines(start, end, ctx->body_canon_type,
+ sign, &need_crlf);
+ end = p + 1;
+
+ if (end == start) {
+ /* Empty body */
+ if (ctx->body_canon_type == DKIM_CANON_SIMPLE) {
+ EVP_DigestUpdate(ctx->body_hash, CRLF, sizeof(CRLF) - 1);
+ ctx->body_canonicalised += sizeof(CRLF) - 1;
+ }
+ else {
+ EVP_DigestUpdate(ctx->body_hash, "", 0);
+ }
+ }
+ else {
+ if (ctx->body_canon_type == DKIM_CANON_SIMPLE) {
+ /* Simple canonization */
+ while (rspamd_dkim_simple_body_step(ctx, ctx->body_hash,
+ &start, end - start, &remain))
+ ;
+
+ /*
+ * If we have l= tag then we cannot add crlf...
+ */
+ if (need_crlf) {
+ /* l is evil... */
+ if (ctx->len == 0) {
+ remain = 2;
+ }
+ else {
+ if (ctx->len <= total_len) {
+ /* We don't have enough l to add \r\n */
+ remain = 0;
+ }
+ else {
+ if (ctx->len - total_len >= 2) {
+ remain = 2;
+ }
+ else {
+ remain = ctx->len - total_len;
+ }
+ }
+ }
+
+ start = "\r\n";
+ end = start + 2;
+
+ rspamd_dkim_simple_body_step(ctx, ctx->body_hash,
+ &start, end - start, &remain);
+ }
+ }
+ else {
+ while (rspamd_dkim_relaxed_body_step(ctx, ctx->body_hash,
+ &start, end - start, &remain))
+ ;
+ if (need_crlf) {
+ start = "\r\n";
+ end = start + 2;
+ remain = 2;
+ rspamd_dkim_relaxed_body_step(ctx, ctx->body_hash,
+ &start, end - start, &remain);
+ }
+ }
+ }
+ return TRUE;
+ }
+
+ /* TODO: Implement relaxed algorithm */
+ return FALSE;
+}
+
+/* Update hash converting all CR and LF to CRLF */
+static void
+rspamd_dkim_hash_update(EVP_MD_CTX *ck, const gchar *begin, gsize len)
+{
+ const gchar *p, *c, *end;
+
+ end = begin + len;
+ p = begin;
+ c = p;
+
+ while (p < end) {
+ if (*p == '\r') {
+ EVP_DigestUpdate(ck, c, p - c);
+ EVP_DigestUpdate(ck, CRLF, sizeof(CRLF) - 1);
+ p++;
+
+ if (p < end && *p == '\n') {
+ p++;
+ }
+ c = p;
+ }
+ else if (*p == '\n') {
+ EVP_DigestUpdate(ck, c, p - c);
+ EVP_DigestUpdate(ck, CRLF, sizeof(CRLF) - 1);
+ p++;
+ c = p;
+ }
+ else {
+ p++;
+ }
+ }
+
+ if (p > c) {
+ EVP_DigestUpdate(ck, c, p - c);
+ }
+}
+
+/* Update hash by signature value (ignoring b= tag) */
+static void
+rspamd_dkim_signature_update(struct rspamd_dkim_common_ctx *ctx,
+ const gchar *begin,
+ guint len)
+{
+ const gchar *p, *c, *end;
+ gboolean tag, skip;
+
+ end = begin + len;
+ p = begin;
+ c = begin;
+ tag = TRUE;
+ skip = FALSE;
+
+ while (p < end) {
+ if (tag && p[0] == 'b' && p[1] == '=') {
+ /* Add to signature */
+ msg_debug_dkim("initial update hash with signature part: %*s",
+ (gint) (p - c + 2),
+ c);
+ ctx->headers_canonicalised += p - c + 2;
+ rspamd_dkim_hash_update(ctx->headers_hash, c, p - c + 2);
+ skip = TRUE;
+ }
+ else if (skip && (*p == ';' || p == end - 1)) {
+ skip = FALSE;
+ c = p;
+ }
+ else if (!tag && *p == ';') {
+ tag = TRUE;
+ }
+ else if (tag && *p == '=') {
+ tag = FALSE;
+ }
+ p++;
+ }
+
+ p--;
+ /* Skip \r\n at the end */
+ while ((*p == '\r' || *p == '\n') && p >= c) {
+ p--;
+ }
+
+ if (p - c + 1 > 0) {
+ msg_debug_dkim("final update hash with signature part: %*s",
+ (gint) (p - c + 1), c);
+ ctx->headers_canonicalised += p - c + 1;
+ rspamd_dkim_hash_update(ctx->headers_hash, c, p - c + 1);
+ }
+}
+
+goffset
+rspamd_dkim_canonize_header_relaxed_str(const gchar *hname,
+ const gchar *hvalue,
+ gchar *out,
+ gsize outlen)
+{
+ gchar *t;
+ const guchar *h;
+ gboolean got_sp;
+
+ /* Name part */
+ t = out;
+ h = hname;
+
+ while (*h && t - out < outlen) {
+ *t++ = lc_map[*h++];
+ }
+
+ if (t - out >= outlen) {
+ return -1;
+ }
+
+ *t++ = ':';
+
+ /* Value part */
+ h = hvalue;
+ /* Skip spaces at the beginning */
+ while (g_ascii_isspace(*h)) {
+ h++;
+ }
+
+ got_sp = FALSE;
+
+ while (*h && (t - out < outlen)) {
+ if (g_ascii_isspace(*h)) {
+ if (got_sp) {
+ h++;
+ continue;
+ }
+ else {
+ got_sp = TRUE;
+ *t++ = ' ';
+ h++;
+ continue;
+ }
+ }
+ else {
+ got_sp = FALSE;
+ }
+
+ *t++ = *h++;
+ }
+
+ if (g_ascii_isspace(*(t - 1))) {
+ t--;
+ }
+
+ if (t - out >= outlen - 2) {
+ return -1;
+ }
+
+ *t++ = '\r';
+ *t++ = '\n';
+ *t = '\0';
+
+ return t - out;
+}
+
+static gboolean
+rspamd_dkim_canonize_header_relaxed(struct rspamd_dkim_common_ctx *ctx,
+ const gchar *header,
+ const gchar *header_name,
+ gboolean is_sign,
+ guint count,
+ bool is_seal)
+{
+ static gchar st_buf[8192];
+ gchar *buf;
+ guint inlen;
+ goffset r;
+ gboolean allocated = FALSE;
+
+ inlen = strlen(header) + strlen(header_name) + sizeof(":" CRLF);
+
+ if (inlen > sizeof(st_buf)) {
+ buf = g_malloc(inlen);
+ allocated = TRUE;
+ }
+ else {
+ /* Faster */
+ buf = st_buf;
+ }
+
+ r = rspamd_dkim_canonize_header_relaxed_str(header_name, header, buf, inlen);
+
+ g_assert(r != -1);
+
+ if (!is_sign) {
+ msg_debug_dkim("update %s with header (idx=%d): %s",
+ is_seal ? "seal" : "signature", count, buf);
+ EVP_DigestUpdate(ctx->headers_hash, buf, r);
+ }
+ else {
+ rspamd_dkim_signature_update(ctx, buf, r);
+ }
+
+ if (allocated) {
+ g_free(buf);
+ }
+
+ return TRUE;
+}
+
+
+static gboolean
+rspamd_dkim_canonize_header(struct rspamd_dkim_common_ctx *ctx,
+ struct rspamd_task *task,
+ const gchar *header_name,
+ gint count,
+ const gchar *dkim_header,
+ const gchar *dkim_domain)
+{
+ struct rspamd_mime_header *rh, *cur, *sel = NULL;
+ gint hdr_cnt = 0;
+ bool use_idx = false, is_sign = ctx->is_sign;
+
+ /*
+ * TODO:
+ * Temporary hack to prevent linked list being misused until refactored
+ */
+ const guint max_list_iters = 1000;
+
+ if (count < 0) {
+ use_idx = true;
+ count = -(count); /* use i= in header content as it is arc stuff */
+ }
+
+ if (dkim_header == NULL) {
+ rh = rspamd_message_get_header_array(task, header_name,
+ is_sign);
+
+ if (rh) {
+ /* Check uniqueness of the header but we count from the bottom to top */
+ if (!use_idx) {
+ for (cur = rh->prev;; cur = cur->prev) {
+ if (hdr_cnt == count) {
+ sel = cur;
+ }
+
+ hdr_cnt++;
+
+ if (cur == rh || hdr_cnt >= max_list_iters) {
+ /* Cycle */
+ break;
+ }
+ }
+
+ if ((rh->flags & RSPAMD_HEADER_UNIQUE) && hdr_cnt > 1) {
+ guint64 random_cookie = ottery_rand_uint64();
+
+ msg_warn_dkim("header %s is intended to be unique by"
+ " email standards, but we have %d headers of this"
+ " type, artificially break DKIM check",
+ header_name,
+ hdr_cnt);
+ rspamd_dkim_hash_update(ctx->headers_hash,
+ (const gchar *) &random_cookie,
+ sizeof(random_cookie));
+ ctx->headers_canonicalised += sizeof(random_cookie);
+
+ return FALSE;
+ }
+
+ if (hdr_cnt <= count) {
+ /*
+ * If DKIM has less headers requested than there are in a
+ * message, then it's fine, it allows adding extra headers
+ */
+ return TRUE;
+ }
+ }
+ else {
+ /*
+ * This branch is used for ARC headers, and it orders them based on
+ * i=<number> string and not their real order in the list of headers
+ */
+ gchar idx_buf[16];
+ gint id_len, i;
+
+ id_len = rspamd_snprintf(idx_buf, sizeof(idx_buf), "i=%d;",
+ count);
+
+ for (cur = rh->prev, i = 0; i < max_list_iters; cur = cur->prev, i++) {
+ if (cur->decoded &&
+ rspamd_substring_search(cur->decoded, strlen(cur->decoded),
+ idx_buf, id_len) != -1) {
+ sel = cur;
+ break;
+ }
+
+ if (cur == rh) {
+ /* Cycle */
+ break;
+ }
+ }
+
+ if (sel == NULL) {
+ return FALSE;
+ }
+ }
+
+ /* Selected header must be non-null if previous condition is false */
+ g_assert(sel != NULL);
+
+ if (ctx->header_canon_type == DKIM_CANON_SIMPLE) {
+ rspamd_dkim_hash_update(ctx->headers_hash, sel->raw_value,
+ sel->raw_len);
+ ctx->headers_canonicalised += sel->raw_len;
+ msg_debug_dkim("update %s with header (idx=%d): %*s",
+ (use_idx ? "seal" : "signature"),
+ count, (gint) sel->raw_len, sel->raw_value);
+ }
+ else {
+ if (is_sign && (sel->flags & RSPAMD_HEADER_FROM)) {
+ /* Special handling of the From handling when rewrite is done */
+ gboolean has_rewrite = FALSE;
+ guint i;
+ struct rspamd_email_address *addr;
+
+ PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, from_mime), i, addr)
+ {
+ if ((addr->flags & RSPAMD_EMAIL_ADDR_ORIGINAL) && !(addr->flags & RSPAMD_EMAIL_ADDR_ALIASED)) {
+ has_rewrite = TRUE;
+ }
+ }
+
+ if (has_rewrite) {
+ PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, from_mime), i, addr)
+ {
+ if (!(addr->flags & RSPAMD_EMAIL_ADDR_ORIGINAL)) {
+ if (!rspamd_dkim_canonize_header_relaxed(ctx, addr->raw,
+ header_name, FALSE, i, use_idx)) {
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+ }
+ }
+ }
+
+ if (!rspamd_dkim_canonize_header_relaxed(ctx, sel->value,
+ header_name, FALSE, count, use_idx)) {
+ return FALSE;
+ }
+ }
+ }
+ }
+ else {
+ /* For signature check just use the saved dkim header */
+ if (ctx->header_canon_type == DKIM_CANON_SIMPLE) {
+ /* We need to find our own signature and use it */
+ rh = rspamd_message_get_header_array(task, header_name, is_sign);
+
+ if (rh) {
+ /* We need to find our own signature */
+ if (!dkim_domain) {
+ msg_err_dkim("cannot verify dkim as we have no dkim domain!");
+ return FALSE;
+ }
+
+ gboolean found = FALSE;
+
+ DL_FOREACH(rh, cur)
+ {
+ guint64 th = rspamd_cryptobox_fast_hash(cur->decoded,
+ strlen(cur->decoded), rspamd_hash_seed());
+
+ if (th == ctx->sig_hash) {
+ rspamd_dkim_signature_update(ctx, cur->raw_value,
+ cur->raw_len);
+ found = TRUE;
+ break;
+ }
+ }
+ if (!found) {
+ msg_err_dkim("BUGON: cannot verify dkim as we have lost our signature"
+ " during simple canonicalisation, expected hash=%L",
+ ctx->sig_hash);
+ return FALSE;
+ }
+ }
+ else {
+ return FALSE;
+ }
+ }
+ else {
+ if (!rspamd_dkim_canonize_header_relaxed(ctx,
+ dkim_header,
+ header_name,
+ TRUE, 0, use_idx)) {
+ return FALSE;
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+struct rspamd_dkim_cached_hash {
+ guchar *digest_normal;
+ guchar *digest_cr;
+ guchar *digest_crlf;
+ gchar *type;
+};
+
+static struct rspamd_dkim_cached_hash *
+rspamd_dkim_check_bh_cached(struct rspamd_dkim_common_ctx *ctx,
+ struct rspamd_task *task, gsize bhlen, gboolean is_sign)
+{
+ gchar typebuf[64];
+ struct rspamd_dkim_cached_hash *res;
+
+ rspamd_snprintf(typebuf, sizeof(typebuf),
+ RSPAMD_MEMPOOL_DKIM_BH_CACHE "%z_%s_%d_%z",
+ bhlen,
+ ctx->body_canon_type == DKIM_CANON_RELAXED ? "1" : "0",
+ !!is_sign,
+ ctx->len);
+
+ res = rspamd_mempool_get_variable(task->task_pool,
+ typebuf);
+
+ if (!res) {
+ res = rspamd_mempool_alloc0(task->task_pool, sizeof(*res));
+ res->type = rspamd_mempool_strdup(task->task_pool, typebuf);
+ rspamd_mempool_set_variable(task->task_pool,
+ res->type, res, NULL);
+ }
+
+ return res;
+}
+
+static const char *
+rspamd_dkim_type_to_string(enum rspamd_dkim_type t)
+{
+ switch (t) {
+ case RSPAMD_DKIM_NORMAL:
+ return "dkim";
+ case RSPAMD_DKIM_ARC_SIG:
+ return "arc_sig";
+ case RSPAMD_DKIM_ARC_SEAL:
+ default:
+ return "arc_seal";
+ }
+}
+
+/**
+ * Check task for dkim context using dkim key
+ * @param ctx dkim verify context
+ * @param key dkim key (from cache or from dns request)
+ * @param task task to check
+ * @return
+ */
+struct rspamd_dkim_check_result *
+rspamd_dkim_check(rspamd_dkim_context_t *ctx,
+ rspamd_dkim_key_t *key,
+ struct rspamd_task *task)
+{
+ const gchar *body_end, *body_start;
+ guchar raw_digest[EVP_MAX_MD_SIZE];
+ struct rspamd_dkim_cached_hash *cached_bh = NULL;
+ EVP_MD_CTX *cpy_ctx = NULL;
+ gsize dlen = 0;
+ struct rspamd_dkim_check_result *res;
+ guint i;
+ struct rspamd_dkim_header *dh;
+ gint nid;
+
+ g_return_val_if_fail(ctx != NULL, NULL);
+ g_return_val_if_fail(key != NULL, NULL);
+ g_return_val_if_fail(task->msg.len > 0, NULL);
+
+ /* First of all find place of body */
+ body_end = task->msg.begin + task->msg.len;
+
+ body_start = MESSAGE_FIELD(task, raw_headers_content).body_start;
+
+ res = rspamd_mempool_alloc0(task->task_pool, sizeof(*res));
+ res->ctx = ctx;
+ res->selector = ctx->selector;
+ res->domain = ctx->domain;
+ res->fail_reason = NULL;
+ res->short_b = ctx->short_b;
+ res->rcode = DKIM_CONTINUE;
+
+ if (!body_start) {
+ res->rcode = DKIM_ERROR;
+ return res;
+ }
+
+ if (ctx->common.type != RSPAMD_DKIM_ARC_SEAL) {
+ dlen = EVP_MD_CTX_size(ctx->common.body_hash);
+ cached_bh = rspamd_dkim_check_bh_cached(&ctx->common, task,
+ dlen, FALSE);
+
+ if (!cached_bh->digest_normal) {
+ /* Start canonization of body part */
+ if (!rspamd_dkim_canonize_body(&ctx->common, body_start, body_end,
+ FALSE)) {
+ res->rcode = DKIM_RECORD_ERROR;
+ return res;
+ }
+ }
+ }
+
+ /* Now canonize headers */
+ for (i = 0; i < ctx->common.hlist->len; i++) {
+ dh = g_ptr_array_index(ctx->common.hlist, i);
+ rspamd_dkim_canonize_header(&ctx->common, task, dh->name, dh->count,
+ NULL, NULL);
+ }
+
+ /* Canonize dkim signature */
+ switch (ctx->common.type) {
+ case RSPAMD_DKIM_NORMAL:
+ rspamd_dkim_canonize_header(&ctx->common, task, RSPAMD_DKIM_SIGNHEADER, 0,
+ ctx->dkim_header, ctx->domain);
+ break;
+ case RSPAMD_DKIM_ARC_SIG:
+ rspamd_dkim_canonize_header(&ctx->common, task, RSPAMD_DKIM_ARC_SIGNHEADER, 0,
+ ctx->dkim_header, ctx->domain);
+ break;
+ case RSPAMD_DKIM_ARC_SEAL:
+ rspamd_dkim_canonize_header(&ctx->common, task, RSPAMD_DKIM_ARC_SEALHEADER, 0,
+ ctx->dkim_header, ctx->domain);
+ break;
+ }
+
+
+ /* Use cached BH for all but arc seal, if it is not NULL we are not in arc seal mode */
+ if (cached_bh != NULL) {
+ if (!cached_bh->digest_normal) {
+ /* Copy md_ctx to deal with broken CRLF at the end */
+ cpy_ctx = EVP_MD_CTX_create();
+ EVP_MD_CTX_copy(cpy_ctx, ctx->common.body_hash);
+ EVP_DigestFinal_ex(cpy_ctx, raw_digest, NULL);
+
+ cached_bh->digest_normal = rspamd_mempool_alloc(task->task_pool,
+ sizeof(raw_digest));
+ memcpy(cached_bh->digest_normal, raw_digest, sizeof(raw_digest));
+ }
+
+ /* Check bh field */
+ if (memcmp(ctx->bh, cached_bh->digest_normal, ctx->bhlen) != 0) {
+ msg_debug_dkim(
+ "bh value mismatch: %*xs versus %*xs, try add LF; try adding CRLF",
+ (gint) dlen, ctx->bh,
+ (gint) dlen, raw_digest);
+
+ if (cpy_ctx) {
+ /* Try add CRLF */
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
+ EVP_MD_CTX_cleanup(cpy_ctx);
+#else
+ EVP_MD_CTX_reset(cpy_ctx);
+#endif
+ EVP_MD_CTX_copy(cpy_ctx, ctx->common.body_hash);
+ EVP_DigestUpdate(cpy_ctx, "\r\n", 2);
+ EVP_DigestFinal_ex(cpy_ctx, raw_digest, NULL);
+ cached_bh->digest_crlf = rspamd_mempool_alloc(task->task_pool,
+ sizeof(raw_digest));
+ memcpy(cached_bh->digest_crlf, raw_digest, sizeof(raw_digest));
+
+ if (memcmp(ctx->bh, raw_digest, ctx->bhlen) != 0) {
+ msg_debug_dkim(
+ "bh value mismatch after added CRLF: %*xs versus %*xs, try add LF",
+ (gint) dlen, ctx->bh,
+ (gint) dlen, raw_digest);
+
+ /* Try add LF */
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
+ EVP_MD_CTX_cleanup(cpy_ctx);
+#else
+ EVP_MD_CTX_reset(cpy_ctx);
+#endif
+ EVP_MD_CTX_copy(cpy_ctx, ctx->common.body_hash);
+ EVP_DigestUpdate(cpy_ctx, "\n", 1);
+ EVP_DigestFinal_ex(cpy_ctx, raw_digest, NULL);
+ cached_bh->digest_cr = rspamd_mempool_alloc(task->task_pool,
+ sizeof(raw_digest));
+ memcpy(cached_bh->digest_cr, raw_digest, sizeof(raw_digest));
+
+ if (memcmp(ctx->bh, raw_digest, ctx->bhlen) != 0) {
+ msg_debug_dkim("bh value mismatch after added LF: %*xs versus %*xs",
+ (gint) dlen, ctx->bh,
+ (gint) dlen, raw_digest);
+ res->fail_reason = "body hash did not verify";
+ res->rcode = DKIM_REJECT;
+ }
+ }
+ }
+ else if (cached_bh->digest_crlf) {
+ if (memcmp(ctx->bh, cached_bh->digest_crlf, ctx->bhlen) != 0) {
+ msg_debug_dkim("bh value mismatch after added CRLF: %*xs versus %*xs",
+ (gint) dlen, ctx->bh,
+ (gint) dlen, cached_bh->digest_crlf);
+
+ if (cached_bh->digest_cr) {
+ if (memcmp(ctx->bh, cached_bh->digest_cr, ctx->bhlen) != 0) {
+ msg_debug_dkim(
+ "bh value mismatch after added LF: %*xs versus %*xs",
+ (gint) dlen, ctx->bh,
+ (gint) dlen, cached_bh->digest_cr);
+
+ res->fail_reason = "body hash did not verify";
+ res->rcode = DKIM_REJECT;
+ }
+ }
+ else {
+
+ res->fail_reason = "body hash did not verify";
+ res->rcode = DKIM_REJECT;
+ }
+ }
+ }
+ else {
+ msg_debug_dkim(
+ "bh value mismatch: %*xs versus %*xs",
+ (gint) dlen, ctx->bh,
+ (gint) dlen, cached_bh->digest_normal);
+ res->fail_reason = "body hash did not verify";
+ res->rcode = DKIM_REJECT;
+ }
+ }
+
+ if (cpy_ctx) {
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
+ EVP_MD_CTX_cleanup(cpy_ctx);
+#else
+ EVP_MD_CTX_reset(cpy_ctx);
+#endif
+ EVP_MD_CTX_destroy(cpy_ctx);
+ }
+
+ if (res->rcode == DKIM_REJECT) {
+ msg_info_dkim(
+ "%s: bh value mismatch: got %*Bs, expected %*Bs; "
+ "body length %d->%d; d=%s; s=%s",
+ rspamd_dkim_type_to_string(ctx->common.type),
+ (gint) dlen, cached_bh->digest_normal,
+ (gint) dlen, ctx->bh,
+ (gint) (body_end - body_start), ctx->common.body_canonicalised,
+ ctx->domain, ctx->selector);
+
+ return res;
+ }
+ }
+
+ dlen = EVP_MD_CTX_size(ctx->common.headers_hash);
+ EVP_DigestFinal_ex(ctx->common.headers_hash, raw_digest, NULL);
+ /* Check headers signature */
+
+ if (ctx->sig_alg == DKIM_SIGN_RSASHA1) {
+ nid = NID_sha1;
+ }
+ else if (ctx->sig_alg == DKIM_SIGN_RSASHA256 ||
+ ctx->sig_alg == DKIM_SIGN_ECDSASHA256 ||
+ ctx->sig_alg == DKIM_SIGN_EDDSASHA256) {
+ nid = NID_sha256;
+ }
+ else if (ctx->sig_alg == DKIM_SIGN_RSASHA512 ||
+ ctx->sig_alg == DKIM_SIGN_ECDSASHA512) {
+ nid = NID_sha512;
+ }
+ else {
+ /* Not reached */
+ nid = NID_sha1;
+ }
+
+ switch (key->type) {
+ case RSPAMD_DKIM_KEY_RSA:
+ if (RSA_verify(nid, raw_digest, dlen, ctx->b, ctx->blen,
+ key->key.key_rsa) != 1) {
+ msg_debug_dkim("headers rsa verify failed");
+ ERR_clear_error();
+ res->rcode = DKIM_REJECT;
+ res->fail_reason = "headers rsa verify failed";
+
+ msg_info_dkim(
+ "%s: headers RSA verification failure; "
+ "body length %d->%d; headers length %d; d=%s; s=%s; key_md5=%*xs; orig header: %s",
+ rspamd_dkim_type_to_string(ctx->common.type),
+ (gint) (body_end - body_start), ctx->common.body_canonicalised,
+ ctx->common.headers_canonicalised,
+ ctx->domain, ctx->selector,
+ RSPAMD_DKIM_KEY_ID_LEN, rspamd_dkim_key_id(key),
+ ctx->dkim_header);
+ }
+ break;
+ case RSPAMD_DKIM_KEY_ECDSA:
+ if (ECDSA_verify(nid, raw_digest, dlen, ctx->b, ctx->blen,
+ key->key.key_ecdsa) != 1) {
+ msg_info_dkim(
+ "%s: headers ECDSA verification failure; "
+ "body length %d->%d; headers length %d; d=%s; s=%s; key_md5=%*xs; orig header: %s",
+ rspamd_dkim_type_to_string(ctx->common.type),
+ (gint) (body_end - body_start), ctx->common.body_canonicalised,
+ ctx->common.headers_canonicalised,
+ ctx->domain, ctx->selector,
+ RSPAMD_DKIM_KEY_ID_LEN, rspamd_dkim_key_id(key),
+ ctx->dkim_header);
+ msg_debug_dkim("headers ecdsa verify failed");
+ ERR_clear_error();
+ res->rcode = DKIM_REJECT;
+ res->fail_reason = "headers ecdsa verify failed";
+ }
+ break;
+ case RSPAMD_DKIM_KEY_EDDSA:
+ if (!rspamd_cryptobox_verify(ctx->b, ctx->blen, raw_digest, dlen,
+ key->key.key_eddsa, RSPAMD_CRYPTOBOX_MODE_25519)) {
+ msg_info_dkim(
+ "%s: headers EDDSA verification failure; "
+ "body length %d->%d; headers length %d; d=%s; s=%s; key_md5=%*xs; orig header: %s",
+ rspamd_dkim_type_to_string(ctx->common.type),
+ (gint) (body_end - body_start), ctx->common.body_canonicalised,
+ ctx->common.headers_canonicalised,
+ ctx->domain, ctx->selector,
+ RSPAMD_DKIM_KEY_ID_LEN, rspamd_dkim_key_id(key),
+ ctx->dkim_header);
+ msg_debug_dkim("headers eddsa verify failed");
+ res->rcode = DKIM_REJECT;
+ res->fail_reason = "headers eddsa verify failed";
+ }
+ break;
+ }
+
+
+ if (ctx->common.type == RSPAMD_DKIM_ARC_SEAL && res->rcode == DKIM_CONTINUE) {
+ switch (ctx->cv) {
+ case RSPAMD_ARC_INVALID:
+ msg_info_dkim("arc seal is invalid i=%d", ctx->common.idx);
+ res->rcode = DKIM_PERM_ERROR;
+ res->fail_reason = "arc seal is invalid";
+ break;
+ case RSPAMD_ARC_FAIL:
+ msg_info_dkim("arc seal failed i=%d", ctx->common.idx);
+ res->rcode = DKIM_REJECT;
+ res->fail_reason = "arc seal failed";
+ break;
+ default:
+ break;
+ }
+ }
+
+ return res;
+}
+
+struct rspamd_dkim_check_result *
+rspamd_dkim_create_result(rspamd_dkim_context_t *ctx,
+ enum rspamd_dkim_check_rcode rcode,
+ struct rspamd_task *task)
+{
+ struct rspamd_dkim_check_result *res;
+
+ res = rspamd_mempool_alloc0(task->task_pool, sizeof(*res));
+ res->ctx = ctx;
+ res->selector = ctx->selector;
+ res->domain = ctx->domain;
+ res->fail_reason = NULL;
+ res->short_b = ctx->short_b;
+ res->rcode = rcode;
+
+ return res;
+}
+
+rspamd_dkim_key_t *
+rspamd_dkim_key_ref(rspamd_dkim_key_t *k)
+{
+ REF_RETAIN(k);
+
+ return k;
+}
+
+void rspamd_dkim_key_unref(rspamd_dkim_key_t *k)
+{
+ REF_RELEASE(k);
+}
+
+rspamd_dkim_sign_key_t *
+rspamd_dkim_sign_key_ref(rspamd_dkim_sign_key_t *k)
+{
+ REF_RETAIN(k);
+
+ return k;
+}
+
+void rspamd_dkim_sign_key_unref(rspamd_dkim_sign_key_t *k)
+{
+ REF_RELEASE(k);
+}
+
+const gchar *
+rspamd_dkim_get_domain(rspamd_dkim_context_t *ctx)
+{
+ if (ctx) {
+ return ctx->domain;
+ }
+
+ return NULL;
+}
+
+const gchar *
+rspamd_dkim_get_selector(rspamd_dkim_context_t *ctx)
+{
+ if (ctx) {
+ return ctx->selector;
+ }
+
+ return NULL;
+}
+
+guint rspamd_dkim_key_get_ttl(rspamd_dkim_key_t *k)
+{
+ if (k) {
+ return k->ttl;
+ }
+
+ return 0;
+}
+
+const gchar *
+rspamd_dkim_get_dns_key(rspamd_dkim_context_t *ctx)
+{
+ if (ctx) {
+ return ctx->dns_key;
+ }
+
+ return NULL;
+}
+
+#define PEM_SIG "-----BEGIN"
+
+rspamd_dkim_sign_key_t *
+rspamd_dkim_sign_key_load(const gchar *key, gsize len,
+ enum rspamd_dkim_key_format type,
+ GError **err)
+{
+ guchar *map = NULL, *tmp = NULL;
+ gsize maplen;
+ rspamd_dkim_sign_key_t *nkey;
+ time_t mtime = time(NULL);
+
+ if (type < 0 || type > RSPAMD_DKIM_KEY_UNKNOWN || len == 0 || key == NULL) {
+ g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL,
+ "invalid key type to load: %d", type);
+ return NULL;
+ }
+
+ nkey = g_malloc0(sizeof(*nkey));
+ nkey->mtime = mtime;
+
+ msg_debug_dkim_taskless("got public key with length %z and type %d",
+ len, type);
+
+ /* Load key file if needed */
+ if (type == RSPAMD_DKIM_KEY_FILE) {
+ struct stat st;
+
+ if (stat(key, &st) != 0) {
+ g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL,
+ "cannot stat key file: '%s' %s", key, strerror(errno));
+ g_free(nkey);
+
+ return NULL;
+ }
+
+ nkey->mtime = st.st_mtime;
+ map = rspamd_file_xmap(key, PROT_READ, &maplen, TRUE);
+
+ if (map == NULL) {
+ g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL,
+ "cannot map key file: '%s' %s", key, strerror(errno));
+ g_free(nkey);
+
+ return NULL;
+ }
+
+ key = map;
+ len = maplen;
+
+ if (maplen > sizeof(PEM_SIG) &&
+ strncmp(map, PEM_SIG, sizeof(PEM_SIG) - 1) == 0) {
+ type = RSPAMD_DKIM_KEY_PEM;
+ }
+ else if (rspamd_cryptobox_base64_is_valid(map, maplen)) {
+ type = RSPAMD_DKIM_KEY_BASE64;
+ }
+ else {
+ type = RSPAMD_DKIM_KEY_RAW;
+ }
+ }
+
+ if (type == RSPAMD_DKIM_KEY_UNKNOWN) {
+ if (len > sizeof(PEM_SIG) &&
+ memcmp(key, PEM_SIG, sizeof(PEM_SIG) - 1) == 0) {
+ type = RSPAMD_DKIM_KEY_PEM;
+ }
+ else {
+ type = RSPAMD_DKIM_KEY_RAW;
+ }
+ }
+
+ if (type == RSPAMD_DKIM_KEY_BASE64) {
+ type = RSPAMD_DKIM_KEY_RAW;
+ tmp = g_malloc(len);
+ rspamd_cryptobox_base64_decode(key, len, tmp, &len);
+ key = tmp;
+ }
+
+ if (type == RSPAMD_DKIM_KEY_RAW && (len == 32 ||
+ len == rspamd_cryptobox_sk_sig_bytes(RSPAMD_CRYPTOBOX_MODE_25519))) {
+ if (len == 32) {
+ /* Seeded key, need scalarmult */
+ unsigned char pk[32];
+ nkey->type = RSPAMD_DKIM_KEY_EDDSA;
+ nkey->key.key_eddsa = g_malloc(
+ rspamd_cryptobox_sk_sig_bytes(RSPAMD_CRYPTOBOX_MODE_25519));
+ crypto_sign_ed25519_seed_keypair(pk, nkey->key.key_eddsa, key);
+ nkey->keylen = rspamd_cryptobox_sk_sig_bytes(RSPAMD_CRYPTOBOX_MODE_25519);
+ }
+ else {
+ /* Full ed25519 key */
+ unsigned klen = rspamd_cryptobox_sk_sig_bytes(RSPAMD_CRYPTOBOX_MODE_25519);
+ nkey->type = RSPAMD_DKIM_KEY_EDDSA;
+ nkey->key.key_eddsa = g_malloc(klen);
+ memcpy(nkey->key.key_eddsa, key, klen);
+ nkey->keylen = klen;
+ }
+ }
+ else {
+ nkey->key_bio = BIO_new_mem_buf(key, len);
+
+ if (type == RSPAMD_DKIM_KEY_RAW) {
+ if (d2i_PrivateKey_bio(nkey->key_bio, &nkey->key_evp) == NULL) {
+ g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL,
+ "cannot parse raw private key: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+
+ rspamd_dkim_sign_key_free(nkey);
+ nkey = NULL;
+
+ goto end;
+ }
+ }
+ else {
+ if (!PEM_read_bio_PrivateKey(nkey->key_bio, &nkey->key_evp, NULL, NULL)) {
+ g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL,
+ "cannot parse pem private key: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ rspamd_dkim_sign_key_free(nkey);
+ nkey = NULL;
+
+ goto end;
+ }
+ }
+ nkey->key.key_rsa = EVP_PKEY_get1_RSA(nkey->key_evp);
+ if (nkey->key.key_rsa == NULL) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_KEYFAIL,
+ "cannot extract rsa key from evp key");
+ rspamd_dkim_sign_key_free(nkey);
+ nkey = NULL;
+
+ goto end;
+ }
+ nkey->type = RSPAMD_DKIM_KEY_RSA;
+ }
+
+ REF_INIT_RETAIN(nkey, rspamd_dkim_sign_key_free);
+
+end:
+
+ if (map != NULL) {
+ munmap(map, maplen);
+ }
+
+ if (tmp != NULL) {
+ rspamd_explicit_memzero(tmp, len);
+ g_free(tmp);
+ }
+
+ return nkey;
+}
+
+#undef PEM_SIG
+
+gboolean
+rspamd_dkim_sign_key_maybe_invalidate(rspamd_dkim_sign_key_t *key, time_t mtime)
+{
+ if (mtime > key->mtime) {
+ return TRUE;
+ }
+ return FALSE;
+}
+
+rspamd_dkim_sign_context_t *
+rspamd_create_dkim_sign_context(struct rspamd_task *task,
+ rspamd_dkim_sign_key_t *priv_key,
+ gint headers_canon,
+ gint body_canon,
+ const gchar *headers,
+ enum rspamd_dkim_type type,
+ GError **err)
+{
+ rspamd_dkim_sign_context_t *nctx;
+
+ if (headers_canon != DKIM_CANON_SIMPLE && headers_canon != DKIM_CANON_RELAXED) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_INVALID_HC,
+ "bad headers canonicalisation");
+
+ return NULL;
+ }
+ if (body_canon != DKIM_CANON_SIMPLE && body_canon != DKIM_CANON_RELAXED) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_INVALID_BC,
+ "bad body canonicalisation");
+
+ return NULL;
+ }
+
+ if (!priv_key || (!priv_key->key.key_rsa && !priv_key->key.key_eddsa)) {
+ g_set_error(err,
+ DKIM_ERROR,
+ DKIM_SIGERROR_KEYFAIL,
+ "bad key to sign");
+
+ return NULL;
+ }
+
+ nctx = rspamd_mempool_alloc0(task->task_pool, sizeof(*nctx));
+ nctx->common.pool = task->task_pool;
+ nctx->common.header_canon_type = headers_canon;
+ nctx->common.body_canon_type = body_canon;
+ nctx->common.type = type;
+ nctx->common.is_sign = TRUE;
+
+ if (type != RSPAMD_DKIM_ARC_SEAL) {
+ if (!rspamd_dkim_parse_hdrlist_common(&nctx->common, headers,
+ strlen(headers), TRUE,
+ err)) {
+ return NULL;
+ }
+ }
+ else {
+ rspamd_dkim_add_arc_seal_headers(task->task_pool, &nctx->common);
+ }
+
+ nctx->key = rspamd_dkim_sign_key_ref(priv_key);
+
+ rspamd_mempool_add_destructor(task->task_pool,
+ (rspamd_mempool_destruct_t) rspamd_dkim_sign_key_unref, priv_key);
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
+ nctx->common.body_hash = EVP_MD_CTX_create();
+ EVP_DigestInit_ex(nctx->common.body_hash, EVP_sha256(), NULL);
+ nctx->common.headers_hash = EVP_MD_CTX_create();
+ EVP_DigestInit_ex(nctx->common.headers_hash, EVP_sha256(), NULL);
+ rspamd_mempool_add_destructor(task->task_pool,
+ (rspamd_mempool_destruct_t) EVP_MD_CTX_destroy, nctx->common.body_hash);
+ rspamd_mempool_add_destructor(task->task_pool,
+ (rspamd_mempool_destruct_t) EVP_MD_CTX_destroy, nctx->common.headers_hash);
+#else
+ nctx->common.body_hash = EVP_MD_CTX_new();
+ EVP_DigestInit_ex(nctx->common.body_hash, EVP_sha256(), NULL);
+ nctx->common.headers_hash = EVP_MD_CTX_new();
+ EVP_DigestInit_ex(nctx->common.headers_hash, EVP_sha256(), NULL);
+ rspamd_mempool_add_destructor(task->task_pool,
+ (rspamd_mempool_destruct_t) EVP_MD_CTX_free, nctx->common.body_hash);
+ rspamd_mempool_add_destructor(task->task_pool,
+ (rspamd_mempool_destruct_t) EVP_MD_CTX_free, nctx->common.headers_hash);
+#endif
+
+ return nctx;
+}
+
+
+GString *
+rspamd_dkim_sign(struct rspamd_task *task, const gchar *selector,
+ const gchar *domain, time_t expire, gsize len, guint idx,
+ const gchar *arc_cv, rspamd_dkim_sign_context_t *ctx)
+{
+ GString *hdr;
+ struct rspamd_dkim_header *dh;
+ const gchar *body_end, *body_start, *hname;
+ guchar raw_digest[EVP_MAX_MD_SIZE];
+ struct rspamd_dkim_cached_hash *cached_bh = NULL;
+ gsize dlen = 0;
+ guint i, j;
+ gchar *b64_data;
+ guchar *sig_buf;
+ guint sig_len;
+ guint headers_len = 0, cur_len = 0;
+ union rspamd_dkim_header_stat hstat;
+
+ g_assert(ctx != NULL);
+
+ /* First of all find place of body */
+ body_end = task->msg.begin + task->msg.len;
+ body_start = MESSAGE_FIELD(task, raw_headers_content).body_start;
+
+ if (len > 0) {
+ ctx->common.len = len;
+ }
+
+ if (!body_start) {
+ return NULL;
+ }
+
+ /* Start canonization of body part */
+ if (ctx->common.type != RSPAMD_DKIM_ARC_SEAL) {
+ dlen = EVP_MD_CTX_size(ctx->common.body_hash);
+ cached_bh = rspamd_dkim_check_bh_cached(&ctx->common, task,
+ dlen, TRUE);
+
+ if (!cached_bh->digest_normal) {
+ /* Start canonization of body part */
+ if (!rspamd_dkim_canonize_body(&ctx->common, body_start, body_end,
+ TRUE)) {
+ return NULL;
+ }
+ }
+ }
+
+ hdr = g_string_sized_new(255);
+
+ if (ctx->common.type == RSPAMD_DKIM_NORMAL) {
+ rspamd_printf_gstring(hdr, "v=1; a=%s; c=%s/%s; d=%s; s=%s; ",
+ ctx->key->type == RSPAMD_DKIM_KEY_RSA ? "rsa-sha256" : "ed25519-sha256",
+ ctx->common.header_canon_type == DKIM_CANON_RELAXED ? "relaxed" : "simple",
+ ctx->common.body_canon_type == DKIM_CANON_RELAXED ? "relaxed" : "simple",
+ domain, selector);
+ }
+ else if (ctx->common.type == RSPAMD_DKIM_ARC_SIG) {
+ rspamd_printf_gstring(hdr, "i=%d; a=%s; c=%s/%s; d=%s; s=%s; ",
+ idx,
+ ctx->key->type == RSPAMD_DKIM_KEY_RSA ? "rsa-sha256" : "ed25519-sha256",
+ ctx->common.header_canon_type == DKIM_CANON_RELAXED ? "relaxed" : "simple",
+ ctx->common.body_canon_type == DKIM_CANON_RELAXED ? "relaxed" : "simple",
+ domain, selector);
+ }
+ else {
+ g_assert(arc_cv != NULL);
+ rspamd_printf_gstring(hdr, "i=%d; a=%s; d=%s; s=%s; cv=%s; ",
+ idx,
+ ctx->key->type == RSPAMD_DKIM_KEY_RSA ? "rsa-sha256" : "ed25519-sha256",
+ domain,
+ selector,
+ arc_cv);
+ }
+
+ if (expire > 0) {
+ rspamd_printf_gstring(hdr, "x=%t; ", expire);
+ }
+
+ if (ctx->common.type != RSPAMD_DKIM_ARC_SEAL) {
+ if (len > 0) {
+ rspamd_printf_gstring(hdr, "l=%z; ", len);
+ }
+ }
+
+ rspamd_printf_gstring(hdr, "t=%t; h=", time(NULL));
+
+ /* Now canonize headers */
+ for (i = 0; i < ctx->common.hlist->len; i++) {
+ struct rspamd_mime_header *rh, *cur;
+
+ dh = g_ptr_array_index(ctx->common.hlist, i);
+
+ /* We allow oversigning if dh->count > number of headers with this name */
+ hstat.n = GPOINTER_TO_UINT(g_hash_table_lookup(ctx->common.htable, dh->name));
+
+ if (hstat.s.flags & RSPAMD_DKIM_FLAG_OVERSIGN) {
+ /* Do oversigning */
+ guint count = 0;
+
+ rh = rspamd_message_get_header_array(task, dh->name, FALSE);
+
+ if (rh) {
+ DL_FOREACH(rh, cur)
+ {
+ /* Sign all existing headers */
+ rspamd_dkim_canonize_header(&ctx->common, task, dh->name,
+ count,
+ NULL, NULL);
+ count++;
+ }
+ }
+
+ /* Now add one more entry to oversign */
+ if (count > 0 || !(hstat.s.flags & RSPAMD_DKIM_FLAG_OVERSIGN_EXISTING)) {
+ cur_len = (strlen(dh->name) + 1) * (count + 1);
+ headers_len += cur_len;
+
+ if (headers_len > 70 && i > 0 && i < ctx->common.hlist->len - 1) {
+ rspamd_printf_gstring(hdr, " ");
+ headers_len = cur_len;
+ }
+
+ for (j = 0; j < count + 1; j++) {
+ rspamd_printf_gstring(hdr, "%s:", dh->name);
+ }
+ }
+ }
+ else {
+ rh = rspamd_message_get_header_array(task, dh->name, FALSE);
+
+ if (rh) {
+ if (hstat.s.count > 0) {
+
+ cur_len = (strlen(dh->name) + 1) * (hstat.s.count);
+ headers_len += cur_len;
+ if (headers_len > 70 && i > 0 && i < ctx->common.hlist->len - 1) {
+ rspamd_printf_gstring(hdr, " ");
+ headers_len = cur_len;
+ }
+
+ for (j = 0; j < hstat.s.count; j++) {
+ rspamd_printf_gstring(hdr, "%s:", dh->name);
+ }
+ }
+
+
+ rspamd_dkim_canonize_header(&ctx->common, task,
+ dh->name, dh->count,
+ NULL, NULL);
+ }
+ }
+
+ g_hash_table_remove(ctx->common.htable, dh->name);
+ }
+
+ /* Replace the last ':' with ';' */
+ hdr->str[hdr->len - 1] = ';';
+
+ if (ctx->common.type != RSPAMD_DKIM_ARC_SEAL) {
+ if (!cached_bh->digest_normal) {
+ EVP_DigestFinal_ex(ctx->common.body_hash, raw_digest, NULL);
+ cached_bh->digest_normal = rspamd_mempool_alloc(task->task_pool,
+ sizeof(raw_digest));
+ memcpy(cached_bh->digest_normal, raw_digest, sizeof(raw_digest));
+ }
+
+
+ b64_data = rspamd_encode_base64(cached_bh->digest_normal, dlen, 0, NULL);
+ rspamd_printf_gstring(hdr, " bh=%s; b=", b64_data);
+ g_free(b64_data);
+ }
+ else {
+ rspamd_printf_gstring(hdr, " b=");
+ }
+
+ switch (ctx->common.type) {
+ case RSPAMD_DKIM_NORMAL:
+ default:
+ hname = RSPAMD_DKIM_SIGNHEADER;
+ break;
+ case RSPAMD_DKIM_ARC_SIG:
+ hname = RSPAMD_DKIM_ARC_SIGNHEADER;
+ break;
+ case RSPAMD_DKIM_ARC_SEAL:
+ hname = RSPAMD_DKIM_ARC_SEALHEADER;
+ break;
+ }
+
+ if (ctx->common.header_canon_type == DKIM_CANON_RELAXED) {
+ if (!rspamd_dkim_canonize_header_relaxed(&ctx->common,
+ hdr->str,
+ hname,
+ TRUE,
+ 0,
+ ctx->common.type == RSPAMD_DKIM_ARC_SEAL)) {
+
+ g_string_free(hdr, TRUE);
+ return NULL;
+ }
+ }
+ else {
+ /* Will likely have issues with folding */
+ rspamd_dkim_hash_update(ctx->common.headers_hash, hdr->str,
+ hdr->len);
+ ctx->common.headers_canonicalised += hdr->len;
+ msg_debug_task("update signature with header: %*s",
+ (gint) hdr->len, hdr->str);
+ }
+
+ dlen = EVP_MD_CTX_size(ctx->common.headers_hash);
+ EVP_DigestFinal_ex(ctx->common.headers_hash, raw_digest, NULL);
+ if (ctx->key->type == RSPAMD_DKIM_KEY_RSA) {
+ sig_len = RSA_size(ctx->key->key.key_rsa);
+ sig_buf = g_alloca(sig_len);
+
+ if (RSA_sign(NID_sha256, raw_digest, dlen, sig_buf, &sig_len,
+ ctx->key->key.key_rsa) != 1) {
+ g_string_free(hdr, TRUE);
+ msg_err_task("rsa sign error: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+
+ return NULL;
+ }
+ }
+ else if (ctx->key->type == RSPAMD_DKIM_KEY_EDDSA) {
+ sig_len = rspamd_cryptobox_signature_bytes(RSPAMD_CRYPTOBOX_MODE_25519);
+ sig_buf = g_alloca(sig_len);
+
+ rspamd_cryptobox_sign(sig_buf, NULL, raw_digest, dlen,
+ ctx->key->key.key_eddsa, RSPAMD_CRYPTOBOX_MODE_25519);
+ }
+ else {
+ g_string_free(hdr, TRUE);
+ msg_err_task("unsupported key type for signing");
+
+ return NULL;
+ }
+
+ if (task->protocol_flags & RSPAMD_TASK_PROTOCOL_FLAG_MILTER) {
+ b64_data = rspamd_encode_base64_fold(sig_buf, sig_len, 70, NULL,
+ RSPAMD_TASK_NEWLINES_LF);
+ }
+ else {
+ b64_data = rspamd_encode_base64_fold(sig_buf, sig_len, 70, NULL,
+ MESSAGE_FIELD(task, nlines_type));
+ }
+
+ rspamd_printf_gstring(hdr, "%s", b64_data);
+ g_free(b64_data);
+
+ return hdr;
+}
+
+gboolean
+rspamd_dkim_match_keys(rspamd_dkim_key_t *pk,
+ rspamd_dkim_sign_key_t *sk,
+ GError **err)
+{
+ if (pk == NULL || sk == NULL) {
+ g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL,
+ "missing public or private key");
+ return FALSE;
+ }
+ if (pk->type != sk->type) {
+ g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL,
+ "public and private key types do not match");
+ return FALSE;
+ }
+
+ if (pk->type == RSPAMD_DKIM_KEY_EDDSA) {
+ if (memcmp(sk->key.key_eddsa + 32, pk->key.key_eddsa, 32) != 0) {
+ g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYHASHMISMATCH,
+ "pubkey does not match private key");
+ return FALSE;
+ }
+ }
+ else if (EVP_PKEY_cmp(pk->key_evp, sk->key_evp) != 1) {
+ g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYHASHMISMATCH,
+ "pubkey does not match private key");
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/src/libserver/dkim.h b/src/libserver/dkim.h
new file mode 100644
index 0000000..50703da
--- /dev/null
+++ b/src/libserver/dkim.h
@@ -0,0 +1,298 @@
+/*-
+ * 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 DKIM_H_
+#define DKIM_H_
+
+#include "config.h"
+#include "contrib/libev/ev.h"
+#include "dns.h"
+#include "ref.h"
+
+
+/* Main types and definitions */
+
+#define RSPAMD_DKIM_SIGNHEADER "DKIM-Signature"
+#define RSPAMD_DKIM_ARC_SIGNHEADER "ARC-Message-Signature"
+#define RSPAMD_DKIM_ARC_AUTHHEADER "ARC-Authentication-Results"
+#define RSPAMD_DKIM_ARC_SEALHEADER "ARC-Seal"
+/* DKIM signature header */
+
+
+/* Errors (from OpenDKIM) */
+
+#define DKIM_SIGERROR_UNKNOWN (-1) /* unknown error */
+#define DKIM_SIGERROR_VERSION 1 /* unsupported version */
+#define DKIM_SIGERROR_EXPIRED 3 /* signature expired */
+#define DKIM_SIGERROR_FUTURE 4 /* signature in the future */
+#define DKIM_SIGERROR_NOREC 6 /* No record */
+#define DKIM_SIGERROR_INVALID_HC 7 /* c= invalid (header) */
+#define DKIM_SIGERROR_INVALID_BC 8 /* c= invalid (body) */
+#define DKIM_SIGERROR_INVALID_A 10 /* a= invalid */
+#define DKIM_SIGERROR_INVALID_L 12 /* l= invalid */
+#define DKIM_SIGERROR_EMPTY_D 16 /* d= empty */
+#define DKIM_SIGERROR_EMPTY_S 18 /* s= empty */
+#define DKIM_SIGERROR_EMPTY_B 20 /* b= empty */
+#define DKIM_SIGERROR_NOKEY 22 /* no key found in DNS */
+#define DKIM_SIGERROR_KEYFAIL 24 /* DNS query failed */
+#define DKIM_SIGERROR_EMPTY_BH 26 /* bh= empty */
+#define DKIM_SIGERROR_BADSIG 28 /* signature mismatch */
+#define DKIM_SIGERROR_EMPTY_H 31 /* h= empty */
+#define DKIM_SIGERROR_INVALID_H 32 /* h= missing req'd entries */
+#define DKIM_SIGERROR_KEYHASHMISMATCH 37 /* sig-key hash mismatch */
+#define DKIM_SIGERROR_EMPTY_V 45 /* v= tag empty */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Check results */
+enum rspamd_dkim_check_rcode {
+ DKIM_CONTINUE = 0,
+ DKIM_REJECT,
+ DKIM_TRYAGAIN,
+ DKIM_NOTFOUND,
+ DKIM_RECORD_ERROR,
+ DKIM_PERM_ERROR,
+};
+
+#define DKIM_CANON_SIMPLE 0 /* as specified in DKIM spec */
+#define DKIM_CANON_RELAXED 1 /* as specified in DKIM spec */
+
+struct rspamd_dkim_context_s;
+typedef struct rspamd_dkim_context_s rspamd_dkim_context_t;
+
+struct rspamd_dkim_sign_context_s;
+typedef struct rspamd_dkim_sign_context_s rspamd_dkim_sign_context_t;
+
+struct rspamd_dkim_key_s;
+typedef struct rspamd_dkim_key_s rspamd_dkim_key_t;
+typedef struct rspamd_dkim_key_s rspamd_dkim_sign_key_t;
+
+struct rspamd_task;
+
+enum rspamd_dkim_key_format {
+ RSPAMD_DKIM_KEY_FILE = 0,
+ RSPAMD_DKIM_KEY_PEM,
+ RSPAMD_DKIM_KEY_BASE64,
+ RSPAMD_DKIM_KEY_RAW,
+ RSPAMD_DKIM_KEY_UNKNOWN
+};
+
+enum rspamd_dkim_type {
+ RSPAMD_DKIM_NORMAL,
+ RSPAMD_DKIM_ARC_SIG,
+ RSPAMD_DKIM_ARC_SEAL
+};
+
+/* Signature methods */
+enum rspamd_sign_type {
+ DKIM_SIGN_UNKNOWN = -2,
+ DKIM_SIGN_RSASHA1 = 0,
+ DKIM_SIGN_RSASHA256,
+ DKIM_SIGN_RSASHA512,
+ DKIM_SIGN_ECDSASHA256,
+ DKIM_SIGN_ECDSASHA512,
+ DKIM_SIGN_EDDSASHA256,
+};
+
+enum rspamd_dkim_key_type {
+ RSPAMD_DKIM_KEY_RSA = 0,
+ RSPAMD_DKIM_KEY_ECDSA,
+ RSPAMD_DKIM_KEY_EDDSA
+};
+
+struct rspamd_dkim_check_result {
+ enum rspamd_dkim_check_rcode rcode;
+ rspamd_dkim_context_t *ctx;
+ /* Processed parts */
+ const gchar *selector;
+ const gchar *domain;
+ const gchar *short_b;
+ const gchar *fail_reason;
+};
+
+
+/* Err MUST be freed if it is not NULL, key is allocated by slice allocator */
+typedef void (*dkim_key_handler_f)(rspamd_dkim_key_t *key, gsize keylen,
+ rspamd_dkim_context_t *ctx, gpointer ud, GError *err);
+
+/**
+ * Create new dkim context from signature
+ * @param sig message's signature
+ * @param pool pool to allocate memory from
+ * @param time_jitter jitter in seconds to allow time diff while checking
+ * @param err pointer to error object
+ * @return new context or NULL
+ */
+rspamd_dkim_context_t *rspamd_create_dkim_context(const gchar *sig,
+ rspamd_mempool_t *pool,
+ struct rspamd_dns_resolver *resolver,
+ guint time_jitter,
+ enum rspamd_dkim_type type,
+ GError **err);
+
+/**
+ * Create new dkim context for making a signature
+ * @param task
+ * @param priv_key
+ * @param err
+ * @return
+ */
+rspamd_dkim_sign_context_t *rspamd_create_dkim_sign_context(struct rspamd_task *task,
+ rspamd_dkim_sign_key_t *priv_key,
+ gint headers_canon,
+ gint body_canon,
+ const gchar *dkim_headers,
+ enum rspamd_dkim_type type,
+ GError **err);
+
+/**
+ * Load dkim key
+ * @param path
+ * @param err
+ * @return
+ */
+rspamd_dkim_sign_key_t *rspamd_dkim_sign_key_load(const gchar *what, gsize len,
+ enum rspamd_dkim_key_format type,
+ GError **err);
+
+/**
+ * Invalidate modified sign key
+ * @param key
+ * @return
+*/
+gboolean rspamd_dkim_sign_key_maybe_invalidate(rspamd_dkim_sign_key_t *key,
+ time_t mtime);
+
+/**
+ * Make DNS request for specified context and obtain and parse key
+ * @param ctx dkim context from signature
+ * @param resolver dns resolver object
+ * @param s async session to make request
+ * @return
+ */
+gboolean rspamd_get_dkim_key(rspamd_dkim_context_t *ctx,
+ struct rspamd_task *task,
+ dkim_key_handler_f handler,
+ gpointer ud);
+
+/**
+ * Check task for dkim context using dkim key
+ * @param ctx dkim verify context
+ * @param key dkim key (from cache or from dns request)
+ * @param task task to check
+ * @return
+ */
+struct rspamd_dkim_check_result *rspamd_dkim_check(rspamd_dkim_context_t *ctx,
+ rspamd_dkim_key_t *key,
+ struct rspamd_task *task);
+
+struct rspamd_dkim_check_result *
+rspamd_dkim_create_result(rspamd_dkim_context_t *ctx,
+ enum rspamd_dkim_check_rcode rcode,
+ struct rspamd_task *task);
+
+GString *rspamd_dkim_sign(struct rspamd_task *task,
+ const gchar *selector,
+ const gchar *domain,
+ time_t expire,
+ gsize len,
+ guint idx,
+ const gchar *arc_cv,
+ rspamd_dkim_sign_context_t *ctx);
+
+rspamd_dkim_key_t *rspamd_dkim_key_ref(rspamd_dkim_key_t *k);
+
+void rspamd_dkim_key_unref(rspamd_dkim_key_t *k);
+
+rspamd_dkim_sign_key_t *rspamd_dkim_sign_key_ref(rspamd_dkim_sign_key_t *k);
+
+void rspamd_dkim_sign_key_unref(rspamd_dkim_sign_key_t *k);
+
+const gchar *rspamd_dkim_get_domain(rspamd_dkim_context_t *ctx);
+
+const gchar *rspamd_dkim_get_selector(rspamd_dkim_context_t *ctx);
+
+const gchar *rspamd_dkim_get_dns_key(rspamd_dkim_context_t *ctx);
+
+guint rspamd_dkim_key_get_ttl(rspamd_dkim_key_t *k);
+
+/**
+ * Create DKIM public key from a raw data
+ * @param keydata
+ * @param keylen
+ * @param type
+ * @param err
+ * @return
+ */
+rspamd_dkim_key_t *rspamd_dkim_make_key(const gchar *keydata, guint keylen,
+ enum rspamd_dkim_key_type type,
+ GError **err);
+
+#define RSPAMD_DKIM_KEY_ID_LEN 16
+/**
+ * Returns key id for dkim key (raw md5 of RSPAMD_DKIM_KEY_ID_LEN)
+ * NOT ZERO TERMINATED, use RSPAMD_DKIM_KEY_ID_LEN for length
+ * @param key
+ * @return
+ */
+const guchar *rspamd_dkim_key_id(rspamd_dkim_key_t *key);
+
+/**
+ * Parse DKIM public key from a TXT record
+ * @param txt
+ * @param keylen
+ * @param err
+ * @return
+ */
+rspamd_dkim_key_t *rspamd_dkim_parse_key(const gchar *txt, gsize *keylen,
+ GError **err);
+
+/**
+ * Canonicalise header using relaxed algorithm
+ * @param hname
+ * @param hvalue
+ * @param out
+ * @param outlen
+ * @return
+ */
+goffset rspamd_dkim_canonize_header_relaxed_str(const gchar *hname,
+ const gchar *hvalue,
+ gchar *out,
+ gsize outlen);
+
+/**
+ * Checks public and private keys for match
+ * @param pk
+ * @param sk
+ * @param err
+ * @return
+ */
+gboolean rspamd_dkim_match_keys(rspamd_dkim_key_t *pk,
+ rspamd_dkim_sign_key_t *sk,
+ GError **err);
+
+/**
+ * Free DKIM key
+ * @param key
+ */
+void rspamd_dkim_key_free(rspamd_dkim_key_t *key);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* DKIM_H_ */
diff --git a/src/libserver/dns.c b/src/libserver/dns.c
new file mode 100644
index 0000000..be2d5a3
--- /dev/null
+++ b/src/libserver/dns.c
@@ -0,0 +1,1124 @@
+/*
+ * 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 "contrib/librdns/rdns.h"
+#include "config.h"
+#include "dns.h"
+#include "rspamd.h"
+#include "utlist.h"
+#include "contrib/libev/ev.h"
+#include "contrib/librdns/rdns.h"
+#include "contrib/librdns/dns_private.h"
+#include "contrib/librdns/rdns_ev.h"
+#include "unix-std.h"
+
+#include <unicode/uidna.h>
+
+static const gchar *M = "rspamd dns";
+
+static struct rdns_upstream_elt *rspamd_dns_select_upstream(const char *name,
+ size_t len, void *ups_data);
+static struct rdns_upstream_elt *rspamd_dns_select_upstream_retransmit(
+ const char *name,
+ size_t len,
+ struct rdns_upstream_elt *prev_elt,
+ void *ups_data);
+static void rspamd_dns_upstream_ok(struct rdns_upstream_elt *elt,
+ void *ups_data);
+static void rspamd_dns_upstream_fail(struct rdns_upstream_elt *elt,
+ void *ups_data, const gchar *reason);
+static unsigned int rspamd_dns_upstream_count(void *ups_data);
+
+static struct rdns_upstream_context rspamd_ups_ctx = {
+ .select = rspamd_dns_select_upstream,
+ .select_retransmit = rspamd_dns_select_upstream_retransmit,
+ .ok = rspamd_dns_upstream_ok,
+ .fail = rspamd_dns_upstream_fail,
+ .count = rspamd_dns_upstream_count,
+ .data = NULL};
+
+struct rspamd_dns_request_ud {
+ struct rspamd_async_session *session;
+ dns_callback_type cb;
+ gpointer ud;
+ rspamd_mempool_t *pool;
+ struct rspamd_task *task;
+ struct rspamd_symcache_dynamic_item *item;
+ struct rdns_request *req;
+ struct rdns_reply *reply;
+};
+
+struct rspamd_dns_fail_cache_entry {
+ const char *name;
+ gint32 namelen;
+ enum rdns_request_type type;
+};
+
+static const gint8 ascii_dns_table[128] = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ /* HYPHEN-MINUS..FULL STOP */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 1, -1,
+ /* 0..9 digits */
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1,
+ /* LATIN CAPITAL LETTER A..LATIN CAPITAL LETTER Z */
+ -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ /* _ */
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, 1,
+ /* LATIN SMALL LETTER A..LATIN SMALL LETTER Z */
+ -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1};
+
+static guint
+rspamd_dns_fail_hash(gconstpointer ptr)
+{
+ struct rspamd_dns_fail_cache_entry *elt =
+ (struct rspamd_dns_fail_cache_entry *) ptr;
+
+ /* We don't care about type when doing hashing */
+ return rspamd_cryptobox_fast_hash(elt->name, elt->namelen,
+ rspamd_hash_seed());
+}
+
+static gboolean
+rspamd_dns_fail_equal(gconstpointer p1, gconstpointer p2)
+{
+ struct rspamd_dns_fail_cache_entry *e1 = (struct rspamd_dns_fail_cache_entry *) p1,
+ *e2 = (struct rspamd_dns_fail_cache_entry *) p2;
+
+ if (e1->type == e2->type && e1->namelen == e2->namelen) {
+ return memcmp(e1->name, e2->name, e1->namelen) == 0;
+ }
+
+ return FALSE;
+}
+
+static void
+rspamd_dns_fin_cb(gpointer arg)
+{
+ struct rspamd_dns_request_ud *reqdata = (struct rspamd_dns_request_ud *) arg;
+
+ if (reqdata->item) {
+ rspamd_symcache_set_cur_item(reqdata->task, reqdata->item);
+ }
+
+ if (reqdata->reply) {
+ reqdata->cb(reqdata->reply, reqdata->ud);
+ }
+ else {
+ struct rdns_reply fake_reply;
+
+ memset(&fake_reply, 0, sizeof(fake_reply));
+ fake_reply.code = RDNS_RC_TIMEOUT;
+ fake_reply.request = reqdata->req;
+ fake_reply.resolver = reqdata->req->resolver;
+ fake_reply.requested_name = reqdata->req->requested_names[0].name;
+
+ reqdata->cb(&fake_reply, reqdata->ud);
+ }
+
+ rdns_request_release(reqdata->req);
+
+ if (reqdata->item) {
+ rspamd_symcache_item_async_dec_check(reqdata->task,
+ reqdata->item, M);
+ }
+
+ if (reqdata->pool == NULL) {
+ g_free(reqdata);
+ }
+}
+
+static void
+rspamd_dns_callback(struct rdns_reply *reply, gpointer ud)
+{
+ struct rspamd_dns_request_ud *reqdata = ud;
+
+ reqdata->reply = reply;
+
+
+ if (reqdata->session) {
+ if (reply->code == RDNS_RC_SERVFAIL &&
+ reqdata->task &&
+ reqdata->task->resolver->fails_cache) {
+
+ /* Add to cache... */
+ const gchar *name = reqdata->req->requested_names[0].name;
+ gchar *target;
+ gsize namelen;
+ struct rspamd_dns_fail_cache_entry *nentry;
+
+ /* Allocate in a single entry to allow further free in a single call */
+ namelen = strlen(name);
+ nentry = g_malloc(sizeof(nentry) + namelen + 1);
+ target = ((gchar *) nentry) + sizeof(nentry);
+ rspamd_strlcpy(target, name, namelen + 1);
+ nentry->type = reqdata->req->requested_names[0].type;
+ nentry->name = target;
+ nentry->namelen = namelen;
+
+ /* Rdns request is retained there */
+ rspamd_lru_hash_insert(reqdata->task->resolver->fails_cache,
+ nentry, rdns_request_retain(reply->request),
+ reqdata->task->task_timestamp,
+ reqdata->task->resolver->fails_cache_time);
+ }
+
+ /*
+ * Ref event to avoid double unref by
+ * event removing
+ */
+ rdns_request_retain(reply->request);
+ rspamd_session_remove_event(reqdata->session,
+ rspamd_dns_fin_cb, reqdata);
+ }
+ else {
+ reqdata->cb(reply, reqdata->ud);
+
+ if (reqdata->pool == NULL) {
+ g_free(reqdata);
+ }
+ }
+}
+
+struct rspamd_dns_request_ud *
+rspamd_dns_resolver_request(struct rspamd_dns_resolver *resolver,
+ struct rspamd_async_session *session,
+ rspamd_mempool_t *pool,
+ dns_callback_type cb,
+ gpointer ud,
+ enum rdns_request_type type,
+ const char *name)
+{
+ struct rdns_request *req;
+ struct rspamd_dns_request_ud *reqdata = NULL;
+ guint nlen = strlen(name);
+ gchar *real_name = NULL;
+
+ g_assert(resolver != NULL);
+
+ if (resolver->r == NULL) {
+ return NULL;
+ }
+
+ if (nlen == 0 || nlen > DNS_D_MAXNAME) {
+ return NULL;
+ }
+
+ if (session && rspamd_session_blocked(session)) {
+ return NULL;
+ }
+
+ if (rspamd_str_has_8bit(name, nlen)) {
+ /* Convert to idna using libicu as it follows all the standards */
+ real_name = rspamd_dns_resolver_idna_convert_utf8(resolver, pool,
+ name, nlen, &nlen);
+
+ if (real_name == NULL) {
+ return NULL;
+ }
+
+ name = real_name;
+ }
+
+ /* Name is now in ASCII only */
+ for (gsize i = 0; i < nlen; i++) {
+ if (ascii_dns_table[((unsigned int) name[i]) & 0x7F] == -1) {
+ /* Invalid DNS name requested */
+
+ if (!pool) {
+ g_free(real_name);
+ }
+
+ return NULL;
+ }
+ }
+
+ if (pool != NULL) {
+ reqdata =
+ rspamd_mempool_alloc0(pool, sizeof(struct rspamd_dns_request_ud));
+ }
+ else {
+ reqdata = g_malloc0(sizeof(struct rspamd_dns_request_ud));
+ }
+
+ reqdata->pool = pool;
+ reqdata->session = session;
+ reqdata->cb = cb;
+ reqdata->ud = ud;
+
+ req = rdns_make_request_full(resolver->r, rspamd_dns_callback, reqdata,
+ resolver->request_timeout, resolver->max_retransmits, 1, name,
+ type);
+ reqdata->req = req;
+
+ if (session) {
+ if (req != NULL) {
+ rspamd_session_add_event(session,
+ (event_finalizer_t) rspamd_dns_fin_cb,
+ reqdata,
+ M);
+ }
+ }
+
+ if (req == NULL) {
+ if (pool == NULL) {
+ g_free(reqdata);
+ g_free(real_name);
+ }
+
+ return NULL;
+ }
+
+ if (real_name && pool == NULL) {
+ g_free(real_name);
+ }
+
+ return reqdata;
+}
+
+struct rspamd_dns_cached_delayed_cbdata {
+ struct rspamd_task *task;
+ dns_callback_type cb;
+ gpointer ud;
+ ev_timer tm;
+ struct rdns_request *req;
+};
+
+static void
+rspamd_fail_cache_cb(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_dns_cached_delayed_cbdata *cbd =
+ (struct rspamd_dns_cached_delayed_cbdata *) w->data;
+ struct rdns_reply fake_reply;
+
+ ev_timer_stop(EV_A_ w);
+ memset(&fake_reply, 0, sizeof(fake_reply));
+ fake_reply.code = RDNS_RC_SERVFAIL;
+ fake_reply.request = cbd->req;
+ fake_reply.resolver = cbd->req->resolver;
+ fake_reply.requested_name = cbd->req->requested_names[0].name;
+ cbd->cb(&fake_reply, cbd->ud);
+ rdns_request_release(cbd->req);
+}
+
+static gboolean
+make_dns_request_task_common(struct rspamd_task *task,
+ dns_callback_type cb,
+ gpointer ud,
+ enum rdns_request_type type,
+ const char *name,
+ gboolean forced)
+{
+ struct rspamd_dns_request_ud *reqdata;
+
+ if (!forced && task->dns_requests >= task->cfg->dns_max_requests) {
+ return FALSE;
+ }
+
+ if (task->resolver->fails_cache) {
+ /* Search in failures cache */
+ struct rspamd_dns_fail_cache_entry search;
+ struct rdns_request *req;
+
+ search.name = name;
+ search.namelen = strlen(name);
+ search.type = type;
+
+ if ((req = rspamd_lru_hash_lookup(task->resolver->fails_cache,
+ &search, task->task_timestamp)) != NULL) {
+ /*
+ * We need to reply with SERVFAIL again to the API, so add a special
+ * timer, uh-oh, and fire it
+ */
+ struct rspamd_dns_cached_delayed_cbdata *cbd =
+ rspamd_mempool_alloc0(task->task_pool, sizeof(*cbd));
+
+ ev_timer_init(&cbd->tm, rspamd_fail_cache_cb, 0.0, 0.0);
+ cbd->task = task;
+ cbd->cb = cb;
+ cbd->ud = ud;
+ cbd->req = rdns_request_retain(req);
+ cbd->tm.data = cbd;
+
+ return TRUE;
+ }
+ }
+
+ reqdata = rspamd_dns_resolver_request(
+ task->resolver, task->s, task->task_pool, cb, ud,
+ type, name);
+
+ if (reqdata) {
+ task->dns_requests++;
+
+ reqdata->task = task;
+ reqdata->item = rspamd_symcache_get_cur_item(task);
+
+ if (reqdata->item) {
+ /* We are inside some session */
+ rspamd_symcache_item_async_inc(task, reqdata->item, M);
+ }
+
+ if (!forced && task->dns_requests >= task->cfg->dns_max_requests) {
+ msg_info_task("stop resolving on reaching %ud requests",
+ task->dns_requests);
+ }
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+gboolean
+rspamd_dns_resolver_request_task(struct rspamd_task *task,
+ dns_callback_type cb,
+ gpointer ud,
+ enum rdns_request_type type,
+ const char *name)
+{
+ return make_dns_request_task_common(task, cb, ud, type, name, FALSE);
+}
+
+gboolean
+rspamd_dns_resolver_request_task_forced(struct rspamd_task *task,
+ dns_callback_type cb,
+ gpointer ud,
+ enum rdns_request_type type,
+ const char *name)
+{
+ return make_dns_request_task_common(task, cb, ud, type, name, TRUE);
+}
+
+static void rspamd_rnds_log_bridge(
+ void *log_data,
+ enum rdns_log_level level,
+ const char *function,
+ const char *format,
+ va_list args)
+{
+ rspamd_logger_t *logger = log_data;
+
+ rspamd_common_logv(logger, (GLogLevelFlags) level, "rdns", NULL,
+ function, format, args);
+}
+
+static void
+rspamd_dns_server_init(struct upstream *up, guint idx, gpointer ud)
+{
+ struct rspamd_dns_resolver *r = ud;
+ rspamd_inet_addr_t *addr;
+ void *serv;
+ struct rdns_upstream_elt *elt;
+
+ addr = rspamd_upstream_addr_next(up);
+
+ if (r->cfg) {
+ serv = rdns_resolver_add_server(r->r, rspamd_inet_address_to_string(addr),
+ rspamd_inet_address_get_port(addr), 0, r->cfg->dns_io_per_server);
+
+ elt = rspamd_mempool_alloc0(r->cfg->cfg_pool, sizeof(*elt));
+ elt->server = serv;
+ elt->lib_data = up;
+
+ rspamd_upstream_set_data(up, elt);
+ }
+ else {
+ serv = rdns_resolver_add_server(r->r, rspamd_inet_address_to_string(addr),
+ rspamd_inet_address_get_port(addr), 0, 8);
+ }
+
+ g_assert(serv != NULL);
+}
+
+static void
+rspamd_dns_server_reorder(struct upstream *up, guint idx, gpointer ud)
+{
+ struct rspamd_dns_resolver *r = ud;
+
+ rspamd_upstream_set_weight(up, rspamd_upstreams_count(r->ups) - idx + 1);
+}
+
+static bool
+rspamd_dns_resolv_conf_on_server(struct rdns_resolver *resolver,
+ const char *name, unsigned int port,
+ int priority, unsigned int io_cnt, void *ud)
+{
+ struct rspamd_dns_resolver *dns_resolver = ud;
+ struct rspamd_config *cfg;
+ rspamd_inet_addr_t *addr;
+ gint test_fd;
+
+ cfg = dns_resolver->cfg;
+
+ msg_info_config("parsed nameserver %s from resolv.conf", name);
+
+ /* Try to open a connection */
+ if (!rspamd_parse_inet_address(&addr, name, strlen(name),
+ RSPAMD_INET_ADDRESS_PARSE_DEFAULT)) {
+ msg_warn_config("cannot parse nameserver address %s", name);
+
+ return FALSE;
+ }
+
+ rspamd_inet_address_set_port(addr, port);
+ test_fd = rspamd_inet_address_connect(addr, SOCK_DGRAM, TRUE);
+
+ if (test_fd == -1 && (errno != EINTR || errno != ECONNREFUSED || errno != ECONNRESET)) {
+ msg_info_config("cannot open connection to nameserver at address %s: %s",
+ name, strerror(errno));
+ rspamd_inet_address_free(addr);
+
+ return FALSE;
+ }
+
+ rspamd_inet_address_free(addr);
+ close(test_fd);
+
+ return rspamd_upstreams_add_upstream(dns_resolver->ups, name, port,
+ RSPAMD_UPSTREAM_PARSE_NAMESERVER,
+ NULL);
+}
+
+static void
+rspamd_process_fake_reply(struct rspamd_config *cfg,
+ struct rspamd_dns_resolver *dns_resolver,
+ const ucl_object_t *cur_arr)
+{
+ const ucl_object_t *cur;
+ ucl_object_iter_t it;
+
+ it = ucl_object_iterate_new(cur_arr);
+
+ while ((cur = ucl_object_iterate_safe(it, true))) {
+ const ucl_object_t *type_obj, *name_obj, *code_obj, *replies_obj;
+ enum rdns_request_type rtype = RDNS_REQUEST_A;
+ enum dns_rcode rcode = RDNS_RC_NOERROR;
+ struct rdns_reply_entry *replies = NULL;
+ const gchar *name = NULL;
+
+ if (ucl_object_type(cur) != UCL_OBJECT) {
+ continue;
+ }
+
+ name_obj = ucl_object_lookup(cur, "name");
+ if (name_obj == NULL ||
+ (name = ucl_object_tostring(name_obj)) == NULL) {
+ msg_err_config("no name for fake dns reply");
+ continue;
+ }
+
+ type_obj = ucl_object_lookup(cur, "type");
+ if (type_obj) {
+ rtype = rdns_type_fromstr(ucl_object_tostring(type_obj));
+
+ if (rtype == RDNS_REQUEST_INVALID) {
+ msg_err_config("invalid type for %s: %s", name,
+ ucl_object_tostring(type_obj));
+ continue;
+ }
+ }
+
+ code_obj = ucl_object_lookup_any(cur, "code", "rcode", NULL);
+ if (code_obj) {
+ rcode = rdns_rcode_fromstr(ucl_object_tostring(code_obj));
+
+ if (rcode == RDNS_RC_INVALID) {
+ msg_err_config("invalid rcode for %s: %s", name,
+ ucl_object_tostring(code_obj));
+ continue;
+ }
+ }
+
+ if (rcode == RDNS_RC_NOERROR) {
+ /* We want replies to be set for this rcode */
+ replies_obj = ucl_object_lookup(cur, "replies");
+
+ if (replies_obj == NULL || ucl_object_type(replies_obj) != UCL_ARRAY) {
+ msg_err_config("invalid replies for fake DNS record %s", name);
+ continue;
+ }
+
+ ucl_object_iter_t rep_it;
+ const ucl_object_t *rep_obj;
+
+ rep_it = ucl_object_iterate_new(replies_obj);
+
+ while ((rep_obj = ucl_object_iterate_safe(rep_it, true))) {
+ const gchar *str_rep = ucl_object_tostring(rep_obj);
+ struct rdns_reply_entry *rep;
+ gchar **svec;
+
+ if (str_rep == NULL) {
+ msg_err_config("invalid reply element for fake DNS record %s",
+ name);
+ continue;
+ }
+
+ rep = calloc(1, sizeof(*rep));
+ g_assert(rep != NULL);
+
+ rep->type = rtype;
+ rep->ttl = 0;
+
+ switch (rtype) {
+ case RDNS_REQUEST_A:
+ if (inet_pton(AF_INET, str_rep, &rep->content.a.addr) != 1) {
+ msg_err_config("invalid A reply element for fake "
+ "DNS record %s: %s",
+ name, str_rep);
+ free(rep);
+ }
+ else {
+ DL_APPEND(replies, rep);
+ }
+ break;
+ case RDNS_REQUEST_NS:
+ rep->content.ns.name = strdup(str_rep);
+ DL_APPEND(replies, rep);
+ break;
+ case RDNS_REQUEST_PTR:
+ rep->content.ptr.name = strdup(str_rep);
+ DL_APPEND(replies, rep);
+ break;
+ case RDNS_REQUEST_MX:
+ svec = g_strsplit_set(str_rep, " :", -1);
+
+ if (svec && svec[0] && svec[1]) {
+ rep->content.mx.priority = strtoul(svec[0], NULL, 10);
+ rep->content.mx.name = strdup(svec[1]);
+ DL_APPEND(replies, rep);
+ }
+ else {
+ msg_err_config("invalid MX reply element for fake "
+ "DNS record %s: %s",
+ name, str_rep);
+ free(rep);
+ }
+
+ g_strfreev(svec);
+ break;
+ case RDNS_REQUEST_TXT:
+ rep->content.txt.data = strdup(str_rep);
+ DL_APPEND(replies, rep);
+ break;
+ case RDNS_REQUEST_SOA:
+ svec = g_strsplit_set(str_rep, " :", -1);
+
+ /* 7 elements */
+ if (svec && svec[0] && svec[1] && svec[2] &&
+ svec[3] && svec[4] && svec[5] && svec[6]) {
+ rep->content.soa.mname = strdup(svec[0]);
+ rep->content.soa.admin = strdup(svec[1]);
+ rep->content.soa.serial = strtoul(svec[2], NULL, 10);
+ rep->content.soa.refresh = strtol(svec[3], NULL, 10);
+ rep->content.soa.retry = strtol(svec[4], NULL, 10);
+ rep->content.soa.expire = strtol(svec[5], NULL, 10);
+ rep->content.soa.minimum = strtoul(svec[6], NULL, 10);
+ DL_APPEND(replies, rep);
+ }
+ else {
+ msg_err_config("invalid MX reply element for fake "
+ "DNS record %s: %s",
+ name, str_rep);
+ free(rep);
+ }
+
+ g_strfreev(svec);
+ break;
+ case RDNS_REQUEST_AAAA:
+ if (inet_pton(AF_INET6, str_rep, &rep->content.aaa.addr) != 1) {
+ msg_err_config("invalid AAAA reply element for fake "
+ "DNS record %s: %s",
+ name, str_rep);
+ free(rep);
+ }
+ else {
+ DL_APPEND(replies, rep);
+ }
+ break;
+ case RDNS_REQUEST_SRV:
+ default:
+ msg_err_config("invalid or unsupported reply element "
+ "for fake DNS record %s(%s): %s",
+ name, rdns_str_from_type(rtype), str_rep);
+ free(rep);
+ break;
+ }
+ }
+
+ ucl_object_iterate_free(rep_it);
+
+ if (replies) {
+ struct rdns_reply_entry *tmp_entry;
+ guint i = 0;
+ DL_COUNT(replies, tmp_entry, i);
+
+ msg_info_config("added fake record: %s(%s); %d replies", name,
+ rdns_str_from_type(rtype), i);
+ rdns_resolver_set_fake_reply(dns_resolver->r,
+ name, rtype, rcode, replies);
+ }
+ else {
+ msg_warn_config("record %s has no replies, not adding",
+ name);
+ }
+ }
+ else {
+ /* This entry returns some non valid code, no replies are possible */
+ replies_obj = ucl_object_lookup(cur, "replies");
+
+ if (replies_obj) {
+ msg_warn_config("replies are set for non-successful return "
+ "code for %s(%s), they will be ignored",
+ name, rdns_str_from_type(rtype));
+ }
+
+ rdns_resolver_set_fake_reply(dns_resolver->r,
+ name, rtype, rcode, NULL);
+ }
+ }
+
+ ucl_object_iterate_free(it);
+}
+
+static bool
+rspamd_dns_read_hosts_file(struct rspamd_config *cfg,
+ struct rspamd_dns_resolver *dns_resolver,
+ const gchar *fname)
+{
+ gchar *linebuf = NULL;
+ gsize buflen = 0;
+ gssize r;
+ FILE *fp;
+ guint nadded = 0;
+
+ fp = fopen(fname, "r");
+
+ if (fp == NULL) {
+ /* Hack to reduce noise */
+ if (strcmp(fname, "/etc/hosts") == 0) {
+ msg_info_config("cannot open hosts file %s: %s", fname,
+ strerror(errno));
+ }
+ else {
+ msg_err_config("cannot open hosts file %s: %s", fname,
+ strerror(errno));
+ }
+
+ return false;
+ }
+
+ while ((r = getline(&linebuf, &buflen, fp)) > 0) {
+ if (linebuf[0] == '#' || g_ascii_isspace(linebuf[0])) {
+ /* Skip comment or empty line */
+ continue;
+ }
+
+ g_strchomp(linebuf);
+
+ gchar **elts = g_strsplit_set(linebuf, " \t\v", -1);
+ rspamd_inet_addr_t *addr;
+
+ if (!rspamd_parse_inet_address(&addr, elts[0], strlen(elts[0]),
+ RSPAMD_INET_ADDRESS_PARSE_REMOTE | RSPAMD_INET_ADDRESS_PARSE_NO_UNIX)) {
+ msg_warn_config("bad hosts file line: %s; cannot parse address", linebuf);
+ }
+ else {
+ /* Add all FQDN + aliases if any */
+ gchar **cur_name = &elts[1];
+
+ while (*cur_name) {
+ if (strlen(*cur_name) == 0) {
+ cur_name++;
+ continue;
+ }
+
+ if (*cur_name[0] == '#') {
+ /* Start of the comment */
+ break;
+ }
+
+ struct rdns_reply_entry *rep;
+ rep = calloc(1, sizeof(*rep));
+ g_assert(rep != NULL);
+
+ rep->ttl = 0;
+
+ if (rspamd_inet_address_get_af(addr) == AF_INET) {
+ socklen_t unused;
+ const struct sockaddr_in *sin = (const struct sockaddr_in *)
+ rspamd_inet_address_get_sa(addr, &unused);
+ rep->type = RDNS_REQUEST_A;
+ memcpy(&rep->content.a.addr, &sin->sin_addr,
+ sizeof(rep->content.a.addr));
+ }
+ else {
+ socklen_t unused;
+ const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)
+ rspamd_inet_address_get_sa(addr, &unused);
+ rep->type = RDNS_REQUEST_AAAA;
+ memcpy(&rep->content.aaa.addr, &sin6->sin6_addr,
+ sizeof(rep->content.aaa.addr));
+ }
+
+ rep->next = NULL;
+ rep->prev = rep;
+ rdns_resolver_set_fake_reply(dns_resolver->r,
+ *cur_name, rep->type, RDNS_RC_NOERROR, rep);
+ msg_debug_config("added fake record %s -> %s from hosts file %s",
+ *cur_name, rspamd_inet_address_to_string(addr), fname);
+ cur_name++;
+ nadded++;
+ }
+
+ rspamd_inet_address_free(addr);
+ }
+
+ g_strfreev(elts);
+ }
+
+ if (linebuf) {
+ free(linebuf);
+ }
+
+ msg_info_config("processed host file %s; %d records added", fname, nadded);
+ fclose(fp);
+
+ return true;
+}
+
+static void
+rspamd_dns_resolver_config_ucl(struct rspamd_config *cfg,
+ struct rspamd_dns_resolver *dns_resolver,
+ const ucl_object_t *dns_section)
+{
+ const ucl_object_t *fake_replies, *fails_cache_size, *fails_cache_time,
+ *hosts;
+ static const ev_tstamp default_fails_cache_time = 10.0;
+
+ /* Process fake replies */
+ fake_replies = ucl_object_lookup_any(dns_section, "fake_records",
+ "fake_replies", NULL);
+
+ if (fake_replies && ucl_object_type(fake_replies) == UCL_ARRAY) {
+ const ucl_object_t *cur_arr;
+
+ DL_FOREACH(fake_replies, cur_arr)
+ {
+ rspamd_process_fake_reply(cfg, dns_resolver, cur_arr);
+ }
+ }
+
+ hosts = ucl_object_lookup(dns_section, "hosts");
+
+ if (hosts == NULL) {
+ /* Read normal `/etc/hosts` file */
+ rspamd_dns_read_hosts_file(cfg, dns_resolver, "/etc/hosts");
+ }
+ else if (ucl_object_type(hosts) == UCL_NULL) {
+ /* Do nothing, hosts are explicitly disabled */
+ }
+ else if (ucl_object_type(hosts) == UCL_STRING) {
+ if (!rspamd_dns_read_hosts_file(cfg, dns_resolver, ucl_object_tostring(hosts))) {
+ msg_err_config("cannot read hosts file %s", ucl_object_tostring(hosts));
+ }
+ }
+ else if (ucl_object_type(hosts) == UCL_ARRAY) {
+ const ucl_object_t *cur;
+ ucl_object_iter_t it = NULL;
+
+ while ((cur = ucl_object_iterate(hosts, &it, true)) != NULL) {
+ if (!rspamd_dns_read_hosts_file(cfg, dns_resolver, ucl_object_tostring(cur))) {
+ msg_err_config("cannot read hosts file %s", ucl_object_tostring(cur));
+ }
+ }
+ }
+ else {
+ msg_err_config("invalid type for hosts parameter: %s",
+ ucl_object_type_to_string(ucl_object_type(hosts)));
+ }
+
+ fails_cache_size = ucl_object_lookup(dns_section, "fails_cache_size");
+ if (fails_cache_size && ucl_object_type(fails_cache_size) == UCL_INT) {
+
+ dns_resolver->fails_cache_time = default_fails_cache_time;
+ fails_cache_time = ucl_object_lookup(dns_section, "fails_cache_time");
+
+ if (fails_cache_time) {
+ dns_resolver->fails_cache_time = ucl_object_todouble(fails_cache_time);
+ }
+
+ dns_resolver->fails_cache = rspamd_lru_hash_new_full(
+ ucl_object_toint(fails_cache_size),
+ g_free, (GDestroyNotify) rdns_request_release,
+ rspamd_dns_fail_hash, rspamd_dns_fail_equal);
+ }
+}
+
+struct rspamd_dns_resolver *
+rspamd_dns_resolver_init(rspamd_logger_t *logger,
+ struct ev_loop *ev_base,
+ struct rspamd_config *cfg)
+{
+ struct rspamd_dns_resolver *dns_resolver;
+
+ dns_resolver = g_malloc0(sizeof(struct rspamd_dns_resolver));
+ dns_resolver->event_loop = ev_base;
+
+ if (cfg != NULL) {
+ dns_resolver->request_timeout = cfg->dns_timeout;
+ dns_resolver->max_retransmits = cfg->dns_retransmits;
+ }
+ else {
+ dns_resolver->request_timeout = 1;
+ dns_resolver->max_retransmits = 2;
+ }
+
+ /* IDN translation is performed in Rspamd now */
+ dns_resolver->r = rdns_resolver_new(RDNS_RESOLVER_NOIDN);
+
+ UErrorCode uc_err = U_ZERO_ERROR;
+
+ dns_resolver->uidna = uidna_openUTS46(UIDNA_DEFAULT, &uc_err);
+ g_assert(!U_FAILURE(uc_err));
+ rdns_bind_libev(dns_resolver->r, dns_resolver->event_loop);
+
+ if (cfg != NULL) {
+ rdns_resolver_set_log_level(dns_resolver->r, cfg->log_level);
+ dns_resolver->cfg = cfg;
+ rdns_resolver_set_dnssec(dns_resolver->r, cfg->enable_dnssec);
+
+ if (cfg->nameservers == NULL) {
+ /* Parse resolv.conf */
+ dns_resolver->ups = rspamd_upstreams_create(cfg->ups_ctx);
+ rspamd_upstreams_set_flags(dns_resolver->ups,
+ RSPAMD_UPSTREAM_FLAG_NORESOLVE);
+ rspamd_upstreams_set_rotation(dns_resolver->ups,
+ RSPAMD_UPSTREAM_MASTER_SLAVE);
+
+ if (!rdns_resolver_parse_resolv_conf_cb(dns_resolver->r,
+ "/etc/resolv.conf",
+ rspamd_dns_resolv_conf_on_server,
+ dns_resolver)) {
+ msg_err("cannot parse resolv.conf and no nameservers defined, "
+ "so no ways to resolve addresses");
+ rdns_resolver_release(dns_resolver->r);
+ dns_resolver->r = NULL;
+
+ return dns_resolver;
+ }
+
+ /* Use normal resolv.conf rules */
+ rspamd_upstreams_foreach(dns_resolver->ups, rspamd_dns_server_reorder,
+ dns_resolver);
+ }
+ else {
+ dns_resolver->ups = rspamd_upstreams_create(cfg->ups_ctx);
+ rspamd_upstreams_set_flags(dns_resolver->ups,
+ RSPAMD_UPSTREAM_FLAG_NORESOLVE);
+
+ if (!rspamd_upstreams_from_ucl(dns_resolver->ups, cfg->nameservers,
+ 53, dns_resolver)) {
+ msg_err_config("cannot parse DNS nameservers definitions");
+ rdns_resolver_release(dns_resolver->r);
+ dns_resolver->r = NULL;
+
+ return dns_resolver;
+ }
+ }
+
+ rspamd_upstreams_foreach(dns_resolver->ups, rspamd_dns_server_init,
+ dns_resolver);
+ rdns_resolver_set_upstream_lib(dns_resolver->r, &rspamd_ups_ctx,
+ dns_resolver->ups);
+ cfg->dns_resolver = dns_resolver;
+
+ if (cfg->cfg_ucl_obj) {
+ /* Configure additional options */
+ const ucl_object_t *opts_section, *dns_section, *tmp;
+
+ opts_section = ucl_object_lookup(cfg->cfg_ucl_obj, "options");
+
+ if (opts_section) {
+ /* TODO: implement a more simple merge logic */
+ DL_FOREACH(opts_section, tmp)
+ {
+ dns_section = ucl_object_lookup(opts_section, "dns");
+
+ if (dns_section) {
+ rspamd_dns_resolver_config_ucl(cfg, dns_resolver,
+ dns_section);
+ }
+ }
+ }
+ }
+ }
+
+ rdns_resolver_set_logger(dns_resolver->r, rspamd_rnds_log_bridge, logger);
+ rdns_resolver_init(dns_resolver->r);
+
+ return dns_resolver;
+}
+
+void rspamd_dns_resolver_deinit(struct rspamd_dns_resolver *resolver)
+{
+ if (resolver) {
+ if (resolver->r) {
+ rdns_resolver_release(resolver->r);
+ }
+
+ if (resolver->ups) {
+ rspamd_upstreams_destroy(resolver->ups);
+ }
+
+ if (resolver->fails_cache) {
+ rspamd_lru_hash_destroy(resolver->fails_cache);
+ }
+
+ uidna_close(resolver->uidna);
+
+ g_free(resolver);
+ }
+}
+
+
+static struct rdns_upstream_elt *
+rspamd_dns_select_upstream(const char *name,
+ size_t len, void *ups_data)
+{
+ struct upstream_list *ups = ups_data;
+ struct upstream *up;
+
+ up = rspamd_upstream_get(ups, RSPAMD_UPSTREAM_ROUND_ROBIN, name, len);
+
+ if (up) {
+ msg_debug("select %s", rspamd_upstream_name(up));
+
+ return rspamd_upstream_get_data(up);
+ }
+
+ return NULL;
+}
+
+static struct rdns_upstream_elt *
+rspamd_dns_select_upstream_retransmit(
+ const char *name,
+ size_t len,
+ struct rdns_upstream_elt *prev_elt,
+ void *ups_data)
+{
+ struct upstream_list *ups = ups_data;
+ struct upstream *up;
+
+ if (prev_elt) {
+ up = rspamd_upstream_get_except(ups, (struct upstream *) prev_elt->lib_data,
+ RSPAMD_UPSTREAM_MASTER_SLAVE, name, len);
+ }
+ else {
+ up = rspamd_upstream_get_forced(ups, RSPAMD_UPSTREAM_RANDOM, name, len);
+ }
+
+ if (up) {
+ msg_debug("select forced %s", rspamd_upstream_name(up));
+
+ return rspamd_upstream_get_data(up);
+ }
+
+ return NULL;
+}
+
+static void
+rspamd_dns_upstream_ok(struct rdns_upstream_elt *elt,
+ void *ups_data)
+{
+ struct upstream *up = elt->lib_data;
+
+ rspamd_upstream_ok(up);
+}
+
+static void
+rspamd_dns_upstream_fail(struct rdns_upstream_elt *elt,
+ void *ups_data, const gchar *reason)
+{
+ struct upstream *up = elt->lib_data;
+
+ rspamd_upstream_fail(up, FALSE, reason);
+}
+
+static unsigned int
+rspamd_dns_upstream_count(void *ups_data)
+{
+ struct upstream_list *ups = ups_data;
+
+ return rspamd_upstreams_alive(ups);
+}
+
+gchar *
+rspamd_dns_resolver_idna_convert_utf8(struct rspamd_dns_resolver *resolver,
+ rspamd_mempool_t *pool,
+ const char *name,
+ gint namelen,
+ guint *outlen)
+{
+ if (resolver == NULL || resolver->uidna == NULL || name == NULL || namelen > DNS_D_MAXNAME) {
+ return NULL;
+ }
+
+ guint dest_len;
+ UErrorCode uc_err = U_ZERO_ERROR;
+ UIDNAInfo info = UIDNA_INFO_INITIALIZER;
+ /* Calculate length required */
+ dest_len = uidna_nameToASCII_UTF8(resolver->uidna, name, namelen,
+ NULL, 0, &info, &uc_err);
+
+ if (uc_err == U_BUFFER_OVERFLOW_ERROR) {
+ gchar *dest;
+
+ if (pool) {
+ dest = rspamd_mempool_alloc(pool, dest_len + 1);
+ }
+ else {
+ dest = g_malloc(dest_len + 1);
+ }
+
+ uc_err = U_ZERO_ERROR;
+
+ dest_len = uidna_nameToASCII_UTF8(resolver->uidna, name, namelen,
+ dest, dest_len + 1, &info, &uc_err);
+
+ if (U_FAILURE(uc_err)) {
+
+ if (!pool) {
+ g_free(dest);
+ }
+
+ return NULL;
+ }
+
+ dest[dest_len] = '\0';
+
+ if (outlen) {
+ *outlen = dest_len;
+ }
+
+ return dest;
+ }
+
+ return NULL;
+} \ No newline at end of file
diff --git a/src/libserver/dns.h b/src/libserver/dns.h
new file mode 100644
index 0000000..acf8d09
--- /dev/null
+++ b/src/libserver/dns.h
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+#ifndef RSPAMD_DNS_H
+#define RSPAMD_DNS_H
+
+#include "config.h"
+#include "mem_pool.h"
+#include "async_session.h"
+#include "logger.h"
+#include "rdns.h"
+#include "upstream.h"
+#include "libutil/hash.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_config;
+struct rspamd_task;
+struct event_loop;
+
+struct rspamd_dns_resolver {
+ struct rdns_resolver *r;
+ struct ev_loop *event_loop;
+ rspamd_lru_hash_t *fails_cache;
+ void *uidna;
+ double fails_cache_time;
+ struct upstream_list *ups;
+ struct rspamd_config *cfg;
+ gdouble request_timeout;
+ guint max_retransmits;
+};
+
+/* Rspamd DNS API */
+
+/**
+ * Init DNS resolver, params are obtained from a config file or system file /etc/resolv.conf
+ */
+struct rspamd_dns_resolver *rspamd_dns_resolver_init(rspamd_logger_t *logger,
+ struct ev_loop *ev_base,
+ struct rspamd_config *cfg);
+
+void rspamd_dns_resolver_deinit(struct rspamd_dns_resolver *resolver);
+
+struct rspamd_dns_request_ud;
+
+/**
+ * Make a DNS request
+ * @param resolver resolver object
+ * @param session async session to register event
+ * @param pool memory pool for storage
+ * @param cb callback to call on resolve completing
+ * @param ud user data for callback
+ * @param type request type
+ * @param ... string or ip address based on a request type
+ * @return TRUE if request was sent.
+ */
+struct rspamd_dns_request_ud *rspamd_dns_resolver_request(struct rspamd_dns_resolver *resolver,
+ struct rspamd_async_session *session,
+ rspamd_mempool_t *pool,
+ dns_callback_type cb,
+ gpointer ud,
+ enum rdns_request_type type,
+ const char *name);
+
+gboolean rspamd_dns_resolver_request_task(struct rspamd_task *task,
+ dns_callback_type cb,
+ gpointer ud,
+ enum rdns_request_type type,
+ const char *name);
+
+gboolean rspamd_dns_resolver_request_task_forced(struct rspamd_task *task,
+ dns_callback_type cb,
+ gpointer ud,
+ enum rdns_request_type type,
+ const char *name);
+
+/**
+ * Converts a name into idna from UTF8
+ * @param resolver resolver (must be initialised)
+ * @param pool optional memory pool (can be NULL, then you need to g_free) the result
+ * @param name input name
+ * @param namelen length of input (-1 for zero terminated)
+ * @return encoded string
+ */
+gchar *rspamd_dns_resolver_idna_convert_utf8(struct rspamd_dns_resolver *resolver,
+ rspamd_mempool_t *pool,
+ const char *name,
+ gint namelen,
+ guint *outlen);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/dynamic_cfg.c b/src/libserver/dynamic_cfg.c
new file mode 100644
index 0000000..cd5cc4e
--- /dev/null
+++ b/src/libserver/dynamic_cfg.c
@@ -0,0 +1,743 @@
+/*
+ * 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 "rspamd.h"
+#include "libserver/maps/map.h"
+#include "scan_result.h"
+#include "dynamic_cfg.h"
+#include "unix-std.h"
+#include "lua/lua_common.h"
+
+#include <math.h>
+
+struct config_json_buf {
+ GString *buf;
+ struct rspamd_config *cfg;
+};
+
+/**
+ * Apply configuration to the specified configuration
+ * @param conf_metrics
+ * @param cfg
+ */
+static void
+apply_dynamic_conf(const ucl_object_t *top, struct rspamd_config *cfg)
+{
+ enum rspamd_action_type test_act;
+ const ucl_object_t *cur_elt, *cur_nm, *it_val;
+ ucl_object_iter_t it = NULL;
+ const gchar *name;
+ gdouble nscore;
+ static const guint priority = 3;
+
+ while ((cur_elt = ucl_object_iterate(top, &it, true))) {
+ if (ucl_object_type(cur_elt) != UCL_OBJECT) {
+ msg_err("loaded json array element is not an object");
+ continue;
+ }
+
+ cur_nm = ucl_object_lookup(cur_elt, "metric");
+ if (!cur_nm || ucl_object_type(cur_nm) != UCL_STRING) {
+ msg_err(
+ "loaded json metric object element has no 'metric' attribute");
+ continue;
+ }
+
+ cur_nm = ucl_object_lookup(cur_elt, "symbols");
+ /* Parse symbols */
+ if (cur_nm && ucl_object_type(cur_nm) == UCL_ARRAY) {
+ ucl_object_iter_t nit = NULL;
+
+ while ((it_val = ucl_object_iterate(cur_nm, &nit, true))) {
+ if (ucl_object_lookup(it_val, "name") &&
+ ucl_object_lookup(it_val, "value")) {
+ const ucl_object_t *n =
+ ucl_object_lookup(it_val, "name");
+ const ucl_object_t *v =
+ ucl_object_lookup(it_val, "value");
+
+ nscore = ucl_object_todouble(v);
+
+ /*
+ * We use priority = 3 here
+ */
+ rspamd_config_add_symbol(cfg,
+ ucl_object_tostring(n), nscore, NULL, NULL,
+ 0, priority, cfg->default_max_shots);
+ }
+ else {
+ msg_info(
+ "json symbol object has no mandatory 'name' and 'value' attributes");
+ }
+ }
+ }
+ else {
+ ucl_object_t *arr;
+
+ arr = ucl_object_typed_new(UCL_ARRAY);
+ ucl_object_insert_key((ucl_object_t *) cur_elt, arr, "symbols",
+ sizeof("symbols") - 1, false);
+ }
+ cur_nm = ucl_object_lookup(cur_elt, "actions");
+ /* Parse actions */
+ if (cur_nm && ucl_object_type(cur_nm) == UCL_ARRAY) {
+ ucl_object_iter_t nit = NULL;
+
+ while ((it_val = ucl_object_iterate(cur_nm, &nit, true))) {
+ const ucl_object_t *n = ucl_object_lookup(it_val, "name");
+ const ucl_object_t *v = ucl_object_lookup(it_val, "value");
+
+ if (n != NULL && v != NULL) {
+ name = ucl_object_tostring(n);
+
+ if (!name || !rspamd_action_from_str(name, &test_act)) {
+ msg_err("unknown action: %s",
+ ucl_object_tostring(ucl_object_lookup(it_val,
+ "name")));
+ continue;
+ }
+
+
+ if (ucl_object_type(v) == UCL_NULL) {
+ nscore = NAN;
+ }
+ else {
+ nscore = ucl_object_todouble(v);
+ }
+
+ ucl_object_t *obj_tbl = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(obj_tbl, ucl_object_fromdouble(nscore),
+ "score", 0, false);
+ ucl_object_insert_key(obj_tbl, ucl_object_fromdouble(priority),
+ "priority", 0, false);
+ rspamd_config_set_action_score(cfg, name, obj_tbl);
+ ucl_object_unref(obj_tbl);
+ }
+ else {
+ msg_info(
+ "json action object has no mandatory 'name' and 'value' attributes");
+ }
+ }
+ }
+ else {
+ ucl_object_t *arr;
+
+ arr = ucl_object_typed_new(UCL_ARRAY);
+ ucl_object_insert_key((ucl_object_t *) cur_elt, arr, "actions",
+ sizeof("actions") - 1, false);
+ }
+ }
+}
+
+/* Callbacks for reading json dynamic rules */
+static gchar *
+json_config_read_cb(gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final)
+{
+ struct config_json_buf *jb, *pd;
+
+ pd = data->prev_data;
+
+ g_assert(pd != NULL);
+
+ if (data->cur_data == NULL) {
+ jb = g_malloc0(sizeof(*jb));
+ jb->cfg = pd->cfg;
+ data->cur_data = jb;
+ }
+ else {
+ jb = data->cur_data;
+ }
+
+ if (jb->buf == NULL) {
+ /* Allocate memory for buffer */
+ jb->buf = g_string_sized_new(MAX(len, BUFSIZ));
+ }
+
+ g_string_append_len(jb->buf, chunk, len);
+
+ return NULL;
+}
+
+static void
+json_config_fin_cb(struct map_cb_data *data, void **target)
+{
+ struct config_json_buf *jb;
+ ucl_object_t *top;
+ struct ucl_parser *parser;
+
+ /* Now parse json */
+ if (data->cur_data) {
+ jb = data->cur_data;
+ }
+ else {
+ return;
+ }
+
+ if (jb->buf == NULL) {
+ msg_err("no data read");
+
+ return;
+ }
+
+ parser = ucl_parser_new(0);
+
+ if (!ucl_parser_add_chunk(parser, jb->buf->str, jb->buf->len)) {
+ msg_err("cannot load json data: parse error %s",
+ ucl_parser_get_error(parser));
+ ucl_parser_free(parser);
+ return;
+ }
+
+ top = ucl_parser_get_object(parser);
+ ucl_parser_free(parser);
+
+ if (ucl_object_type(top) != UCL_ARRAY) {
+ ucl_object_unref(top);
+ msg_err("loaded json is not an array");
+ return;
+ }
+
+ ucl_object_unref(jb->cfg->current_dynamic_conf);
+ apply_dynamic_conf(top, jb->cfg);
+ jb->cfg->current_dynamic_conf = top;
+
+ if (target) {
+ *target = data->cur_data;
+ }
+
+ if (data->prev_data) {
+ jb = data->prev_data;
+ /* Clean prev data */
+ if (jb->buf) {
+ g_string_free(jb->buf, TRUE);
+ }
+
+ g_free(jb);
+ }
+}
+
+static void
+json_config_dtor_cb(struct map_cb_data *data)
+{
+ struct config_json_buf *jb;
+
+ if (data->cur_data) {
+ jb = data->cur_data;
+ /* Clean prev data */
+ if (jb->buf) {
+ g_string_free(jb->buf, TRUE);
+ }
+
+ if (jb->cfg && jb->cfg->current_dynamic_conf) {
+ ucl_object_unref(jb->cfg->current_dynamic_conf);
+ }
+
+ g_free(jb);
+ }
+}
+
+/**
+ * Init dynamic configuration using map logic and specific configuration
+ * @param cfg config file
+ */
+void init_dynamic_config(struct rspamd_config *cfg)
+{
+ struct config_json_buf *jb, **pjb;
+
+ if (cfg->dynamic_conf == NULL) {
+ /* No dynamic conf has been specified, so do not try to load it */
+ return;
+ }
+
+ /* Now try to add map with json data */
+ jb = g_malloc(sizeof(struct config_json_buf));
+ pjb = g_malloc(sizeof(struct config_json_buf *));
+ jb->buf = NULL;
+ jb->cfg = cfg;
+ *pjb = jb;
+ cfg->current_dynamic_conf = ucl_object_typed_new(UCL_ARRAY);
+ rspamd_mempool_add_destructor(cfg->cfg_pool,
+ (rspamd_mempool_destruct_t) g_free,
+ pjb);
+
+ if (!rspamd_map_add(cfg,
+ cfg->dynamic_conf,
+ "Dynamic configuration map",
+ json_config_read_cb,
+ json_config_fin_cb,
+ json_config_dtor_cb,
+ (void **) pjb, NULL, RSPAMD_MAP_DEFAULT)) {
+ msg_err("cannot add map for configuration %s", cfg->dynamic_conf);
+ }
+}
+
+/**
+ * Dump dynamic configuration to the disk
+ * @param cfg
+ * @return
+ */
+gboolean
+dump_dynamic_config(struct rspamd_config *cfg)
+{
+ struct stat st;
+ gchar *dir, pathbuf[PATH_MAX];
+ gint fd;
+
+ if (cfg->dynamic_conf == NULL || cfg->current_dynamic_conf == NULL) {
+ /* No dynamic conf has been specified, so do not try to dump it */
+ msg_err("cannot save dynamic conf as it is not specified");
+ return FALSE;
+ }
+
+ dir = g_path_get_dirname(cfg->dynamic_conf);
+ if (dir == NULL) {
+ msg_err("invalid path: %s", cfg->dynamic_conf);
+ return FALSE;
+ }
+
+ if (stat(cfg->dynamic_conf, &st) == -1) {
+ msg_debug("%s is unavailable: %s", cfg->dynamic_conf,
+ strerror(errno));
+ st.st_mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
+ }
+ if (access(dir, W_OK | R_OK) == -1) {
+ msg_warn("%s is inaccessible: %s", dir, strerror(errno));
+ g_free(dir);
+ return FALSE;
+ }
+ rspamd_snprintf(pathbuf,
+ sizeof(pathbuf),
+ "%s%crconf-XXXXXX",
+ dir,
+ G_DIR_SEPARATOR);
+ g_free(dir);
+#ifdef HAVE_MKSTEMP
+ /* Umask is set before */
+ fd = mkstemp(pathbuf);
+#else
+ fd = g_mkstemp_full(pathbuf, O_RDWR, S_IWUSR | S_IRUSR);
+#endif
+ if (fd == -1) {
+ msg_err("mkstemp error: %s", strerror(errno));
+
+ return FALSE;
+ }
+
+ struct ucl_emitter_functions *emitter_functions;
+ FILE *fp;
+
+ fp = fdopen(fd, "w");
+ emitter_functions = ucl_object_emit_file_funcs(fp);
+
+ if (!ucl_object_emit_full(cfg->current_dynamic_conf, UCL_EMIT_JSON,
+ emitter_functions, NULL)) {
+ msg_err("cannot emit ucl object: %s", strerror(errno));
+ ucl_object_emit_funcs_free(emitter_functions);
+ fclose(fp);
+ return FALSE;
+ }
+
+ (void) unlink(cfg->dynamic_conf);
+
+ /* Rename old config */
+ if (rename(pathbuf, cfg->dynamic_conf) == -1) {
+ msg_err("rename error: %s", strerror(errno));
+ fclose(fp);
+ ucl_object_emit_funcs_free(emitter_functions);
+ unlink(pathbuf);
+
+ return FALSE;
+ }
+ /* Set permissions */
+
+ if (chmod(cfg->dynamic_conf, st.st_mode) == -1) {
+ msg_warn("chmod failed: %s", strerror(errno));
+ }
+
+ fclose(fp);
+ ucl_object_emit_funcs_free(emitter_functions);
+
+ return TRUE;
+}
+
+static ucl_object_t *
+new_dynamic_metric(const gchar *metric_name, ucl_object_t *top)
+{
+ ucl_object_t *metric;
+
+ metric = ucl_object_typed_new(UCL_OBJECT);
+
+ ucl_object_insert_key(metric, ucl_object_fromstring(metric_name),
+ "metric", sizeof("metric") - 1, true);
+ ucl_object_insert_key(metric, ucl_object_typed_new(UCL_ARRAY),
+ "actions", sizeof("actions") - 1, false);
+ ucl_object_insert_key(metric, ucl_object_typed_new(UCL_ARRAY),
+ "symbols", sizeof("symbols") - 1, false);
+
+ ucl_array_append(top, metric);
+
+ return metric;
+}
+
+static ucl_object_t *
+dynamic_metric_find_elt(const ucl_object_t *arr, const gchar *name)
+{
+ ucl_object_iter_t it = NULL;
+ const ucl_object_t *cur, *n;
+
+ it = ucl_object_iterate_new(arr);
+
+ while ((cur = ucl_object_iterate_safe(it, true)) != NULL) {
+ if (cur->type == UCL_OBJECT) {
+ n = ucl_object_lookup(cur, "name");
+ if (n && n->type == UCL_STRING &&
+ strcmp(name, ucl_object_tostring(n)) == 0) {
+ ucl_object_iterate_free(it);
+
+ return (ucl_object_t *) ucl_object_lookup(cur, "value");
+ }
+ }
+ }
+
+ ucl_object_iterate_free(it);
+
+ return NULL;
+}
+
+static ucl_object_t *
+dynamic_metric_find_metric(const ucl_object_t *arr, const gchar *metric)
+{
+ ucl_object_iter_t it = NULL;
+ const ucl_object_t *cur, *n;
+
+ it = ucl_object_iterate_new(arr);
+
+ while ((cur = ucl_object_iterate_safe(it, true)) != NULL) {
+ if (cur->type == UCL_OBJECT) {
+ n = ucl_object_lookup(cur, "metric");
+ if (n && n->type == UCL_STRING &&
+ strcmp(metric, ucl_object_tostring(n)) == 0) {
+ ucl_object_iterate_free(it);
+
+ return (ucl_object_t *) cur;
+ }
+ }
+ }
+
+ ucl_object_iterate_free(it);
+
+ return NULL;
+}
+
+static ucl_object_t *
+new_dynamic_elt(ucl_object_t *arr, const gchar *name, gdouble value)
+{
+ ucl_object_t *n;
+
+ n = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(n, ucl_object_fromstring(name), "name",
+ sizeof("name") - 1, false);
+ ucl_object_insert_key(n, ucl_object_fromdouble(value), "value",
+ sizeof("value") - 1, false);
+
+ ucl_array_append(arr, n);
+
+ return n;
+}
+
+static gint
+rspamd_maybe_add_lua_dynsym(struct rspamd_config *cfg,
+ const gchar *sym,
+ gdouble score)
+{
+ lua_State *L = cfg->lua_state;
+ gint ret = -1;
+ struct rspamd_config **pcfg;
+
+ lua_getglobal(L, "rspamd_plugins");
+ if (lua_type(L, -1) == LUA_TTABLE) {
+ lua_pushstring(L, "dynamic_conf");
+ lua_gettable(L, -2);
+
+ if (lua_type(L, -1) == LUA_TTABLE) {
+ lua_pushstring(L, "add_symbol");
+ lua_gettable(L, -2);
+
+ if (lua_type(L, -1) == LUA_TFUNCTION) {
+ pcfg = lua_newuserdata(L, sizeof(*pcfg));
+ *pcfg = cfg;
+ rspamd_lua_setclass(L, "rspamd{config}", -1);
+ lua_pushstring(L, sym);
+ lua_pushnumber(L, score);
+
+ if (lua_pcall(L, 3, 1, 0) != 0) {
+ msg_err_config("cannot execute add_symbol script: %s",
+ lua_tostring(L, -1));
+ }
+ else {
+ ret = lua_toboolean(L, -1);
+ }
+
+ lua_pop(L, 1);
+ }
+ else {
+ lua_pop(L, 1);
+ }
+ }
+
+ lua_pop(L, 1);
+ }
+
+ lua_pop(L, 1);
+
+ return ret;
+}
+
+static gint
+rspamd_maybe_add_lua_dynact(struct rspamd_config *cfg,
+ const gchar *action,
+ gdouble score)
+{
+ lua_State *L = cfg->lua_state;
+ gint ret = -1;
+ struct rspamd_config **pcfg;
+
+ lua_getglobal(L, "rspamd_plugins");
+ if (lua_type(L, -1) == LUA_TTABLE) {
+ lua_pushstring(L, "dynamic_conf");
+ lua_gettable(L, -2);
+
+ if (lua_type(L, -1) == LUA_TTABLE) {
+ lua_pushstring(L, "add_action");
+ lua_gettable(L, -2);
+
+ if (lua_type(L, -1) == LUA_TFUNCTION) {
+ pcfg = lua_newuserdata(L, sizeof(*pcfg));
+ *pcfg = cfg;
+ rspamd_lua_setclass(L, "rspamd{config}", -1);
+ lua_pushstring(L, action);
+ lua_pushnumber(L, score);
+
+ if (lua_pcall(L, 3, 1, 0) != 0) {
+ msg_err_config("cannot execute add_action script: %s",
+ lua_tostring(L, -1));
+ }
+ else {
+ ret = lua_toboolean(L, -1);
+ }
+
+ lua_pop(L, 1);
+ }
+ else {
+ lua_pop(L, 1);
+ }
+ }
+
+ lua_pop(L, 1);
+ }
+
+ lua_pop(L, 1);
+
+ return ret;
+}
+
+/**
+ * Add symbol for specified metric
+ * @param cfg config file object
+ * @param metric metric's name
+ * @param symbol symbol's name
+ * @param value value of symbol
+ * @return
+ */
+gboolean
+add_dynamic_symbol(struct rspamd_config *cfg,
+ const gchar *metric_name,
+ const gchar *symbol,
+ gdouble value)
+{
+ ucl_object_t *metric, *syms;
+ gint ret;
+
+ if ((ret = rspamd_maybe_add_lua_dynsym(cfg, symbol, value)) != -1) {
+ return ret == 0 ? FALSE : TRUE;
+ }
+
+ if (cfg->dynamic_conf == NULL) {
+ msg_info("dynamic conf is disabled");
+ return FALSE;
+ }
+
+ metric = dynamic_metric_find_metric(cfg->current_dynamic_conf,
+ metric_name);
+ if (metric == NULL) {
+ metric = new_dynamic_metric(metric_name, cfg->current_dynamic_conf);
+ }
+
+ syms = (ucl_object_t *) ucl_object_lookup(metric, "symbols");
+ if (syms != NULL) {
+ ucl_object_t *sym;
+
+ sym = dynamic_metric_find_elt(syms, symbol);
+ if (sym) {
+ sym->value.dv = value;
+ }
+ else {
+ new_dynamic_elt(syms, symbol, value);
+ }
+ }
+
+ apply_dynamic_conf(cfg->current_dynamic_conf, cfg);
+
+ return TRUE;
+}
+
+gboolean
+remove_dynamic_symbol(struct rspamd_config *cfg,
+ const gchar *metric_name,
+ const gchar *symbol)
+{
+ ucl_object_t *metric, *syms;
+ gboolean ret = FALSE;
+
+ if (cfg->dynamic_conf == NULL) {
+ msg_info("dynamic conf is disabled");
+ return FALSE;
+ }
+
+ metric = dynamic_metric_find_metric(cfg->current_dynamic_conf,
+ metric_name);
+ if (metric == NULL) {
+ return FALSE;
+ }
+
+ syms = (ucl_object_t *) ucl_object_lookup(metric, "symbols");
+ if (syms != NULL) {
+ ucl_object_t *sym;
+
+ sym = dynamic_metric_find_elt(syms, symbol);
+
+ if (sym) {
+ ret = ucl_array_delete((ucl_object_t *) syms, sym) != NULL;
+
+ if (ret) {
+ ucl_object_unref(sym);
+ }
+ }
+ }
+
+ if (ret) {
+ apply_dynamic_conf(cfg->current_dynamic_conf, cfg);
+ }
+
+ return ret;
+}
+
+
+/**
+ * Add action for specified metric
+ * @param cfg config file object
+ * @param metric metric's name
+ * @param action action's name
+ * @param value value of symbol
+ * @return
+ */
+gboolean
+add_dynamic_action(struct rspamd_config *cfg,
+ const gchar *metric_name,
+ guint action,
+ gdouble value)
+{
+ ucl_object_t *metric, *acts;
+ const gchar *action_name = rspamd_action_to_str(action);
+ gint ret;
+
+ if ((ret = rspamd_maybe_add_lua_dynact(cfg, action_name, value)) != -1) {
+ return ret == 0 ? FALSE : TRUE;
+ }
+
+ if (cfg->dynamic_conf == NULL) {
+ msg_info("dynamic conf is disabled");
+ return FALSE;
+ }
+
+ metric = dynamic_metric_find_metric(cfg->current_dynamic_conf,
+ metric_name);
+ if (metric == NULL) {
+ metric = new_dynamic_metric(metric_name, cfg->current_dynamic_conf);
+ }
+
+ acts = (ucl_object_t *) ucl_object_lookup(metric, "actions");
+ if (acts != NULL) {
+ ucl_object_t *act;
+
+ act = dynamic_metric_find_elt(acts, action_name);
+ if (act) {
+ act->value.dv = value;
+ }
+ else {
+ new_dynamic_elt(acts, action_name, value);
+ }
+ }
+
+ apply_dynamic_conf(cfg->current_dynamic_conf, cfg);
+
+ return TRUE;
+}
+
+gboolean
+remove_dynamic_action(struct rspamd_config *cfg,
+ const gchar *metric_name,
+ guint action)
+{
+ ucl_object_t *metric, *acts;
+ const gchar *action_name = rspamd_action_to_str(action);
+ gboolean ret = FALSE;
+
+ if (cfg->dynamic_conf == NULL) {
+ msg_info("dynamic conf is disabled");
+ return FALSE;
+ }
+
+ metric = dynamic_metric_find_metric(cfg->current_dynamic_conf,
+ metric_name);
+ if (metric == NULL) {
+ return FALSE;
+ }
+
+ acts = (ucl_object_t *) ucl_object_lookup(metric, "actions");
+
+ if (acts != NULL) {
+ ucl_object_t *act;
+
+ act = dynamic_metric_find_elt(acts, action_name);
+
+ if (act) {
+ ret = ucl_array_delete(acts, act) != NULL;
+ }
+ if (ret) {
+ ucl_object_unref(act);
+ }
+ }
+
+ if (ret) {
+ apply_dynamic_conf(cfg->current_dynamic_conf, cfg);
+ }
+
+ return ret;
+}
diff --git a/src/libserver/dynamic_cfg.h b/src/libserver/dynamic_cfg.h
new file mode 100644
index 0000000..bb386ca
--- /dev/null
+++ b/src/libserver/dynamic_cfg.h
@@ -0,0 +1,81 @@
+/*-
+ * 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 DYNAMIC_CFG_H_
+#define DYNAMIC_CFG_H_
+
+#include "config.h"
+#include "cfg_file.h"
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Init dynamic configuration using map logic and specific configuration
+ * @param cfg config file
+ */
+void init_dynamic_config(struct rspamd_config *cfg);
+
+/**
+ * Dump dynamic configuration to the disk
+ * @param cfg
+ * @return
+ */
+gboolean dump_dynamic_config(struct rspamd_config *cfg);
+
+/**
+ * Add symbol for specified metric
+ * @param cfg config file object
+ * @param metric metric's name
+ * @param symbol symbol's name
+ * @param value value of symbol
+ * @return
+ */
+gboolean add_dynamic_symbol(struct rspamd_config *cfg,
+ const gchar *metric,
+ const gchar *symbol,
+ gdouble value);
+
+gboolean remove_dynamic_symbol(struct rspamd_config *cfg,
+ const gchar *metric,
+ const gchar *symbol);
+
+/**
+ * Add action for specified metric
+ * @param cfg config file object
+ * @param metric metric's name
+ * @param action action's name
+ * @param value value of symbol
+ * @return
+ */
+gboolean add_dynamic_action(struct rspamd_config *cfg,
+ const gchar *metric,
+ guint action,
+ gdouble value);
+
+/**
+ * Removes dynamic action
+ */
+gboolean remove_dynamic_action(struct rspamd_config *cfg,
+ const gchar *metric,
+ guint action);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* DYNAMIC_CFG_H_ */
diff --git a/src/libserver/fuzzy_backend/fuzzy_backend.c b/src/libserver/fuzzy_backend/fuzzy_backend.c
new file mode 100644
index 0000000..9099f38
--- /dev/null
+++ b/src/libserver/fuzzy_backend/fuzzy_backend.c
@@ -0,0 +1,560 @@
+/*-
+ * 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 "fuzzy_backend.h"
+#include "fuzzy_backend_sqlite.h"
+#include "fuzzy_backend_redis.h"
+#include "cfg_file.h"
+#include "fuzzy_wire.h"
+
+#define DEFAULT_EXPIRE 172800L
+
+enum rspamd_fuzzy_backend_type {
+ RSPAMD_FUZZY_BACKEND_SQLITE = 0,
+ RSPAMD_FUZZY_BACKEND_REDIS = 1,
+};
+
+static void *rspamd_fuzzy_backend_init_sqlite(struct rspamd_fuzzy_backend *bk,
+ const ucl_object_t *obj, struct rspamd_config *cfg, GError **err);
+static void rspamd_fuzzy_backend_check_sqlite(struct rspamd_fuzzy_backend *bk,
+ const struct rspamd_fuzzy_cmd *cmd,
+ rspamd_fuzzy_check_cb cb, void *ud,
+ void *subr_ud);
+static void rspamd_fuzzy_backend_update_sqlite(struct rspamd_fuzzy_backend *bk,
+ GArray *updates, const gchar *src,
+ rspamd_fuzzy_update_cb cb, void *ud,
+ void *subr_ud);
+static void rspamd_fuzzy_backend_count_sqlite(struct rspamd_fuzzy_backend *bk,
+ rspamd_fuzzy_count_cb cb, void *ud,
+ void *subr_ud);
+static void rspamd_fuzzy_backend_version_sqlite(struct rspamd_fuzzy_backend *bk,
+ const gchar *src,
+ rspamd_fuzzy_version_cb cb, void *ud,
+ void *subr_ud);
+static const gchar *rspamd_fuzzy_backend_id_sqlite(struct rspamd_fuzzy_backend *bk,
+ void *subr_ud);
+static void rspamd_fuzzy_backend_expire_sqlite(struct rspamd_fuzzy_backend *bk,
+ void *subr_ud);
+static void rspamd_fuzzy_backend_close_sqlite(struct rspamd_fuzzy_backend *bk,
+ void *subr_ud);
+
+struct rspamd_fuzzy_backend_subr {
+ void *(*init)(struct rspamd_fuzzy_backend *bk, const ucl_object_t *obj,
+ struct rspamd_config *cfg,
+ GError **err);
+ void (*check)(struct rspamd_fuzzy_backend *bk,
+ const struct rspamd_fuzzy_cmd *cmd,
+ rspamd_fuzzy_check_cb cb, void *ud,
+ void *subr_ud);
+ void (*update)(struct rspamd_fuzzy_backend *bk,
+ GArray *updates, const gchar *src,
+ rspamd_fuzzy_update_cb cb, void *ud,
+ void *subr_ud);
+ void (*count)(struct rspamd_fuzzy_backend *bk,
+ rspamd_fuzzy_count_cb cb, void *ud,
+ void *subr_ud);
+ void (*version)(struct rspamd_fuzzy_backend *bk,
+ const gchar *src,
+ rspamd_fuzzy_version_cb cb, void *ud,
+ void *subr_ud);
+ const gchar *(*id)(struct rspamd_fuzzy_backend *bk, void *subr_ud);
+ void (*periodic)(struct rspamd_fuzzy_backend *bk, void *subr_ud);
+ void (*close)(struct rspamd_fuzzy_backend *bk, void *subr_ud);
+};
+
+static const struct rspamd_fuzzy_backend_subr fuzzy_subrs[] = {
+ [RSPAMD_FUZZY_BACKEND_SQLITE] = {
+ .init = rspamd_fuzzy_backend_init_sqlite,
+ .check = rspamd_fuzzy_backend_check_sqlite,
+ .update = rspamd_fuzzy_backend_update_sqlite,
+ .count = rspamd_fuzzy_backend_count_sqlite,
+ .version = rspamd_fuzzy_backend_version_sqlite,
+ .id = rspamd_fuzzy_backend_id_sqlite,
+ .periodic = rspamd_fuzzy_backend_expire_sqlite,
+ .close = rspamd_fuzzy_backend_close_sqlite,
+ },
+ [RSPAMD_FUZZY_BACKEND_REDIS] = {
+ .init = rspamd_fuzzy_backend_init_redis,
+ .check = rspamd_fuzzy_backend_check_redis,
+ .update = rspamd_fuzzy_backend_update_redis,
+ .count = rspamd_fuzzy_backend_count_redis,
+ .version = rspamd_fuzzy_backend_version_redis,
+ .id = rspamd_fuzzy_backend_id_redis,
+ .periodic = rspamd_fuzzy_backend_expire_redis,
+ .close = rspamd_fuzzy_backend_close_redis,
+ }};
+
+struct rspamd_fuzzy_backend {
+ enum rspamd_fuzzy_backend_type type;
+ gdouble expire;
+ gdouble sync;
+ struct ev_loop *event_loop;
+ rspamd_fuzzy_periodic_cb periodic_cb;
+ void *periodic_ud;
+ const struct rspamd_fuzzy_backend_subr *subr;
+ void *subr_ud;
+ ev_timer periodic_event;
+};
+
+static GQuark
+rspamd_fuzzy_backend_quark(void)
+{
+ return g_quark_from_static_string("fuzzy-backend");
+}
+
+static void *
+rspamd_fuzzy_backend_init_sqlite(struct rspamd_fuzzy_backend *bk,
+ const ucl_object_t *obj, struct rspamd_config *cfg, GError **err)
+{
+ const ucl_object_t *elt;
+
+ elt = ucl_object_lookup_any(obj, "hashfile", "hash_file", "file",
+ "database", NULL);
+
+ if (elt == NULL || ucl_object_type(elt) != UCL_STRING) {
+ g_set_error(err, rspamd_fuzzy_backend_quark(),
+ EINVAL, "missing sqlite3 path");
+ return NULL;
+ }
+
+ return rspamd_fuzzy_backend_sqlite_open(ucl_object_tostring(elt),
+ FALSE, err);
+}
+
+static void
+rspamd_fuzzy_backend_check_sqlite(struct rspamd_fuzzy_backend *bk,
+ const struct rspamd_fuzzy_cmd *cmd,
+ rspamd_fuzzy_check_cb cb, void *ud,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_sqlite *sq = subr_ud;
+ struct rspamd_fuzzy_reply rep;
+
+ rep = rspamd_fuzzy_backend_sqlite_check(sq, cmd, bk->expire);
+
+ if (cb) {
+ cb(&rep, ud);
+ }
+}
+
+static void
+rspamd_fuzzy_backend_update_sqlite(struct rspamd_fuzzy_backend *bk,
+ GArray *updates, const gchar *src,
+ rspamd_fuzzy_update_cb cb, void *ud,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_sqlite *sq = subr_ud;
+ gboolean success = FALSE;
+ guint i;
+ struct fuzzy_peer_cmd *io_cmd;
+ struct rspamd_fuzzy_cmd *cmd;
+ gpointer ptr;
+ guint nupdates = 0, nadded = 0, ndeleted = 0, nextended = 0, nignored = 0;
+
+ if (rspamd_fuzzy_backend_sqlite_prepare_update(sq, src)) {
+ for (i = 0; i < updates->len; i++) {
+ io_cmd = &g_array_index(updates, struct fuzzy_peer_cmd, i);
+
+ if (io_cmd->is_shingle) {
+ cmd = &io_cmd->cmd.shingle.basic;
+ ptr = &io_cmd->cmd.shingle;
+ }
+ else {
+ cmd = &io_cmd->cmd.normal;
+ ptr = &io_cmd->cmd.normal;
+ }
+
+ if (cmd->cmd == FUZZY_WRITE) {
+ rspamd_fuzzy_backend_sqlite_add(sq, ptr);
+ nadded++;
+ nupdates++;
+ }
+ else if (cmd->cmd == FUZZY_DEL) {
+ rspamd_fuzzy_backend_sqlite_del(sq, ptr);
+ ndeleted++;
+ nupdates++;
+ }
+ else {
+ if (cmd->cmd == FUZZY_REFRESH) {
+ nextended++;
+ }
+ else {
+ nignored++;
+ }
+ }
+ }
+
+ if (rspamd_fuzzy_backend_sqlite_finish_update(sq, src,
+ nupdates > 0)) {
+ success = TRUE;
+ }
+ }
+
+ if (cb) {
+ cb(success, nadded, ndeleted, nextended, nignored, ud);
+ }
+}
+
+static void
+rspamd_fuzzy_backend_count_sqlite(struct rspamd_fuzzy_backend *bk,
+ rspamd_fuzzy_count_cb cb, void *ud,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_sqlite *sq = subr_ud;
+ guint64 nhashes;
+
+ nhashes = rspamd_fuzzy_backend_sqlite_count(sq);
+
+ if (cb) {
+ cb(nhashes, ud);
+ }
+}
+
+static void
+rspamd_fuzzy_backend_version_sqlite(struct rspamd_fuzzy_backend *bk,
+ const gchar *src,
+ rspamd_fuzzy_version_cb cb, void *ud,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_sqlite *sq = subr_ud;
+ guint64 rev;
+
+ rev = rspamd_fuzzy_backend_sqlite_version(sq, src);
+
+ if (cb) {
+ cb(rev, ud);
+ }
+}
+
+static const gchar *
+rspamd_fuzzy_backend_id_sqlite(struct rspamd_fuzzy_backend *bk,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_sqlite *sq = subr_ud;
+
+ return rspamd_fuzzy_sqlite_backend_id(sq);
+}
+static void
+rspamd_fuzzy_backend_expire_sqlite(struct rspamd_fuzzy_backend *bk,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_sqlite *sq = subr_ud;
+
+ rspamd_fuzzy_backend_sqlite_sync(sq, bk->expire, TRUE);
+}
+
+static void
+rspamd_fuzzy_backend_close_sqlite(struct rspamd_fuzzy_backend *bk,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_sqlite *sq = subr_ud;
+
+ rspamd_fuzzy_backend_sqlite_close(sq);
+}
+
+
+struct rspamd_fuzzy_backend *
+rspamd_fuzzy_backend_create(struct ev_loop *ev_base,
+ const ucl_object_t *config,
+ struct rspamd_config *cfg,
+ GError **err)
+{
+ struct rspamd_fuzzy_backend *bk;
+ enum rspamd_fuzzy_backend_type type = RSPAMD_FUZZY_BACKEND_SQLITE;
+ const ucl_object_t *elt;
+ gdouble expire = DEFAULT_EXPIRE;
+
+ if (config != NULL) {
+ elt = ucl_object_lookup(config, "backend");
+
+ if (elt != NULL && ucl_object_type(elt) == UCL_STRING) {
+ if (strcmp(ucl_object_tostring(elt), "sqlite") == 0) {
+ type = RSPAMD_FUZZY_BACKEND_SQLITE;
+ }
+ else if (strcmp(ucl_object_tostring(elt), "redis") == 0) {
+ type = RSPAMD_FUZZY_BACKEND_REDIS;
+ }
+ else {
+ g_set_error(err, rspamd_fuzzy_backend_quark(),
+ EINVAL, "invalid backend type: %s",
+ ucl_object_tostring(elt));
+ return NULL;
+ }
+ }
+
+ elt = ucl_object_lookup(config, "expire");
+
+ if (elt != NULL) {
+ expire = ucl_object_todouble(elt);
+ }
+ }
+
+ bk = g_malloc0(sizeof(*bk));
+ bk->event_loop = ev_base;
+ bk->expire = expire;
+ bk->type = type;
+ bk->subr = &fuzzy_subrs[type];
+
+ if ((bk->subr_ud = bk->subr->init(bk, config, cfg, err)) == NULL) {
+ g_free(bk);
+
+ return NULL;
+ }
+
+ return bk;
+}
+
+
+void rspamd_fuzzy_backend_check(struct rspamd_fuzzy_backend *bk,
+ const struct rspamd_fuzzy_cmd *cmd,
+ rspamd_fuzzy_check_cb cb, void *ud)
+{
+ g_assert(bk != NULL);
+
+ bk->subr->check(bk, cmd, cb, ud, bk->subr_ud);
+}
+
+static guint
+rspamd_fuzzy_digest_hash(gconstpointer key)
+{
+ guint ret;
+
+ /* Distributed uniformly already */
+ memcpy(&ret, key, sizeof(ret));
+
+ return ret;
+}
+
+static gboolean
+rspamd_fuzzy_digest_equal(gconstpointer v, gconstpointer v2)
+{
+ return memcmp(v, v2, rspamd_cryptobox_HASHBYTES) == 0;
+}
+
+static void
+rspamd_fuzzy_backend_deduplicate_queue(GArray *updates)
+{
+ GHashTable *seen = g_hash_table_new(rspamd_fuzzy_digest_hash,
+ rspamd_fuzzy_digest_equal);
+ struct fuzzy_peer_cmd *io_cmd, *found;
+ struct rspamd_fuzzy_cmd *cmd;
+ guchar *digest;
+ guint i;
+
+ for (i = 0; i < updates->len; i++) {
+ io_cmd = &g_array_index(updates, struct fuzzy_peer_cmd, i);
+
+ if (io_cmd->is_shingle) {
+ cmd = &io_cmd->cmd.shingle.basic;
+ }
+ else {
+ cmd = &io_cmd->cmd.normal;
+ }
+
+ digest = cmd->digest;
+
+ found = g_hash_table_lookup(seen, digest);
+
+ if (found == NULL) {
+ /* Add to the seen list, if not a duplicate (huh?) */
+ if (cmd->cmd != FUZZY_DUP) {
+ g_hash_table_insert(seen, digest, io_cmd);
+ }
+ }
+ else {
+ if (found->cmd.normal.flag != cmd->flag) {
+ /* TODO: deal with flags better at some point */
+ continue;
+ }
+
+ /* Apply heuristic */
+ switch (cmd->cmd) {
+ case FUZZY_WRITE:
+ if (found->cmd.normal.cmd == FUZZY_WRITE) {
+ /* Already seen */
+ found->cmd.normal.value += cmd->value;
+ cmd->cmd = FUZZY_DUP; /* Ignore this one */
+ }
+ else if (found->cmd.normal.cmd == FUZZY_REFRESH) {
+ /* Seen refresh command, remove it as write has higher priority */
+ g_hash_table_replace(seen, digest, io_cmd);
+ found->cmd.normal.cmd = FUZZY_DUP;
+ }
+ else if (found->cmd.normal.cmd == FUZZY_DEL) {
+ /* Request delete + add, weird, but ignore add */
+ cmd->cmd = FUZZY_DUP; /* Ignore this one */
+ }
+ break;
+ case FUZZY_REFRESH:
+ if (found->cmd.normal.cmd == FUZZY_WRITE) {
+ /* No need to expire, handled by addition */
+ cmd->cmd = FUZZY_DUP; /* Ignore this one */
+ }
+ else if (found->cmd.normal.cmd == FUZZY_DEL) {
+ /* Request delete + expire, ignore expire */
+ cmd->cmd = FUZZY_DUP; /* Ignore this one */
+ }
+ else if (found->cmd.normal.cmd == FUZZY_REFRESH) {
+ /* Already handled */
+ cmd->cmd = FUZZY_DUP; /* Ignore this one */
+ }
+ break;
+ case FUZZY_DEL:
+ /* Delete has priority over all other commands */
+ g_hash_table_replace(seen, digest, io_cmd);
+ found->cmd.normal.cmd = FUZZY_DUP;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ g_hash_table_unref(seen);
+}
+
+void rspamd_fuzzy_backend_process_updates(struct rspamd_fuzzy_backend *bk,
+ GArray *updates, const gchar *src, rspamd_fuzzy_update_cb cb,
+ void *ud)
+{
+ g_assert(bk != NULL);
+ g_assert(updates != NULL);
+
+ if (updates) {
+ rspamd_fuzzy_backend_deduplicate_queue(updates);
+ bk->subr->update(bk, updates, src, cb, ud, bk->subr_ud);
+ }
+ else if (cb) {
+ cb(TRUE, 0, 0, 0, 0, ud);
+ }
+}
+
+
+void rspamd_fuzzy_backend_count(struct rspamd_fuzzy_backend *bk,
+ rspamd_fuzzy_count_cb cb, void *ud)
+{
+ g_assert(bk != NULL);
+
+ bk->subr->count(bk, cb, ud, bk->subr_ud);
+}
+
+
+void rspamd_fuzzy_backend_version(struct rspamd_fuzzy_backend *bk,
+ const gchar *src,
+ rspamd_fuzzy_version_cb cb, void *ud)
+{
+ g_assert(bk != NULL);
+
+ bk->subr->version(bk, src, cb, ud, bk->subr_ud);
+}
+
+const gchar *
+rspamd_fuzzy_backend_id(struct rspamd_fuzzy_backend *bk)
+{
+ g_assert(bk != NULL);
+
+ if (bk->subr->id) {
+ return bk->subr->id(bk, bk->subr_ud);
+ }
+
+ return NULL;
+}
+
+static inline void
+rspamd_fuzzy_backend_periodic_sync(struct rspamd_fuzzy_backend *bk)
+{
+ if (bk->periodic_cb) {
+ if (bk->periodic_cb(bk->periodic_ud)) {
+ if (bk->subr->periodic) {
+ bk->subr->periodic(bk, bk->subr_ud);
+ }
+ }
+ }
+ else {
+ if (bk->subr->periodic) {
+ bk->subr->periodic(bk, bk->subr_ud);
+ }
+ }
+}
+
+static void
+rspamd_fuzzy_backend_periodic_cb(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_fuzzy_backend *bk = (struct rspamd_fuzzy_backend *) w->data;
+ gdouble jittered;
+
+ jittered = rspamd_time_jitter(bk->sync, bk->sync / 2.0);
+ w->repeat = jittered;
+ rspamd_fuzzy_backend_periodic_sync(bk);
+ ev_timer_again(EV_A_ w);
+}
+
+void rspamd_fuzzy_backend_start_update(struct rspamd_fuzzy_backend *bk,
+ gdouble timeout,
+ rspamd_fuzzy_periodic_cb cb,
+ void *ud)
+{
+ gdouble jittered;
+
+ g_assert(bk != NULL);
+
+ if (bk->subr->periodic) {
+ if (bk->sync > 0.0) {
+ ev_timer_stop(bk->event_loop, &bk->periodic_event);
+ }
+
+ if (cb) {
+ bk->periodic_cb = cb;
+ bk->periodic_ud = ud;
+ }
+
+ rspamd_fuzzy_backend_periodic_sync(bk);
+ bk->sync = timeout;
+ jittered = rspamd_time_jitter(timeout, timeout / 2.0);
+
+ bk->periodic_event.data = bk;
+ ev_timer_init(&bk->periodic_event, rspamd_fuzzy_backend_periodic_cb,
+ jittered, 0.0);
+ ev_timer_start(bk->event_loop, &bk->periodic_event);
+ }
+}
+
+void rspamd_fuzzy_backend_close(struct rspamd_fuzzy_backend *bk)
+{
+ g_assert(bk != NULL);
+
+ if (bk->sync > 0.0) {
+ rspamd_fuzzy_backend_periodic_sync(bk);
+ ev_timer_stop(bk->event_loop, &bk->periodic_event);
+ }
+
+ bk->subr->close(bk, bk->subr_ud);
+
+ g_free(bk);
+}
+
+struct ev_loop *
+rspamd_fuzzy_backend_event_base(struct rspamd_fuzzy_backend *backend)
+{
+ return backend->event_loop;
+}
+
+gdouble
+rspamd_fuzzy_backend_get_expire(struct rspamd_fuzzy_backend *backend)
+{
+ return backend->expire;
+}
diff --git a/src/libserver/fuzzy_backend/fuzzy_backend.h b/src/libserver/fuzzy_backend/fuzzy_backend.h
new file mode 100644
index 0000000..a1b74bc
--- /dev/null
+++ b/src/libserver/fuzzy_backend/fuzzy_backend.h
@@ -0,0 +1,131 @@
+/*-
+ * 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 SRC_LIBSERVER_FUZZY_BACKEND_H_
+#define SRC_LIBSERVER_FUZZY_BACKEND_H_
+
+#include "config.h"
+#include "contrib/libev/ev.h"
+#include "fuzzy_wire.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_fuzzy_backend;
+struct rspamd_config;
+
+/*
+ * Callbacks for fuzzy methods
+ */
+typedef void (*rspamd_fuzzy_check_cb)(struct rspamd_fuzzy_reply *rep, void *ud);
+
+typedef void (*rspamd_fuzzy_update_cb)(gboolean success,
+ guint nadded,
+ guint ndeleted,
+ guint nextended,
+ guint nignored,
+ void *ud);
+
+typedef void (*rspamd_fuzzy_version_cb)(guint64 rev, void *ud);
+
+typedef void (*rspamd_fuzzy_count_cb)(guint64 count, void *ud);
+
+typedef gboolean (*rspamd_fuzzy_periodic_cb)(void *ud);
+
+/**
+ * Open fuzzy backend
+ * @param ev_base
+ * @param config
+ * @param err
+ * @return
+ */
+struct rspamd_fuzzy_backend *rspamd_fuzzy_backend_create(struct ev_loop *ev_base,
+ const ucl_object_t *config,
+ struct rspamd_config *cfg,
+ GError **err);
+
+
+/**
+ * Check a specific hash in storage
+ * @param cmd
+ * @param cb
+ * @param ud
+ */
+void rspamd_fuzzy_backend_check(struct rspamd_fuzzy_backend *bk,
+ const struct rspamd_fuzzy_cmd *cmd,
+ rspamd_fuzzy_check_cb cb, void *ud);
+
+/**
+ * Process updates for a specific queue
+ * @param bk
+ * @param updates queue of struct fuzzy_peer_cmd
+ * @param src
+ */
+void rspamd_fuzzy_backend_process_updates(struct rspamd_fuzzy_backend *bk,
+ GArray *updates, const gchar *src, rspamd_fuzzy_update_cb cb,
+ void *ud);
+
+/**
+ * Gets number of hashes from the backend
+ * @param bk
+ * @param cb
+ * @param ud
+ */
+void rspamd_fuzzy_backend_count(struct rspamd_fuzzy_backend *bk,
+ rspamd_fuzzy_count_cb cb, void *ud);
+
+/**
+ * Returns number of revision for a specific source
+ * @param bk
+ * @param src
+ * @param cb
+ * @param ud
+ */
+void rspamd_fuzzy_backend_version(struct rspamd_fuzzy_backend *bk,
+ const gchar *src,
+ rspamd_fuzzy_version_cb cb, void *ud);
+
+/**
+ * Returns unique id for backend
+ * @param backend
+ * @return
+ */
+const gchar *rspamd_fuzzy_backend_id(struct rspamd_fuzzy_backend *backend);
+
+/**
+ * Starts expire process for the backend
+ * @param backend
+ */
+void rspamd_fuzzy_backend_start_update(struct rspamd_fuzzy_backend *backend,
+ gdouble timeout,
+ rspamd_fuzzy_periodic_cb cb,
+ void *ud);
+
+struct ev_loop *rspamd_fuzzy_backend_event_base(struct rspamd_fuzzy_backend *backend);
+
+gdouble rspamd_fuzzy_backend_get_expire(struct rspamd_fuzzy_backend *backend);
+
+/**
+ * Closes backend
+ * @param backend
+ */
+void rspamd_fuzzy_backend_close(struct rspamd_fuzzy_backend *backend);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SRC_LIBSERVER_FUZZY_BACKEND_H_ */
diff --git a/src/libserver/fuzzy_backend/fuzzy_backend_redis.c b/src/libserver/fuzzy_backend/fuzzy_backend_redis.c
new file mode 100644
index 0000000..7ab7ca6
--- /dev/null
+++ b/src/libserver/fuzzy_backend/fuzzy_backend_redis.c
@@ -0,0 +1,1666 @@
+/*
+ * 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 "ref.h"
+#include "fuzzy_backend.h"
+#include "fuzzy_backend_redis.h"
+#include "redis_pool.h"
+#include "cryptobox.h"
+#include "str_util.h"
+#include "upstream.h"
+#include "contrib/hiredis/hiredis.h"
+#include "contrib/hiredis/async.h"
+#include "lua/lua_common.h"
+
+#define REDIS_DEFAULT_PORT 6379
+#define REDIS_DEFAULT_OBJECT "fuzzy"
+#define REDIS_DEFAULT_TIMEOUT 2.0
+
+#define msg_err_redis_session(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "fuzzy_redis", session->backend->id, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+#define msg_warn_redis_session(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ "fuzzy_redis", session->backend->id, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+#define msg_info_redis_session(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ "fuzzy_redis", session->backend->id, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+#define msg_debug_redis_session(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_fuzzy_redis_log_id, "fuzzy_redis", session->backend->id, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+
+INIT_LOG_MODULE(fuzzy_redis)
+
+struct rspamd_fuzzy_backend_redis {
+ lua_State *L;
+ const gchar *redis_object;
+ const gchar *username;
+ const gchar *password;
+ const gchar *dbname;
+ gchar *id;
+ struct rspamd_redis_pool *pool;
+ gdouble timeout;
+ gint conf_ref;
+ bool terminated;
+ ref_entry_t ref;
+};
+
+enum rspamd_fuzzy_redis_command {
+ RSPAMD_FUZZY_REDIS_COMMAND_COUNT,
+ RSPAMD_FUZZY_REDIS_COMMAND_VERSION,
+ RSPAMD_FUZZY_REDIS_COMMAND_UPDATES,
+ RSPAMD_FUZZY_REDIS_COMMAND_CHECK
+};
+
+struct rspamd_fuzzy_redis_session {
+ struct rspamd_fuzzy_backend_redis *backend;
+ redisAsyncContext *ctx;
+ ev_timer timeout;
+ const struct rspamd_fuzzy_cmd *cmd;
+ struct ev_loop *event_loop;
+ float prob;
+ gboolean shingles_checked;
+
+ enum rspamd_fuzzy_redis_command command;
+ guint nargs;
+
+ guint nadded;
+ guint ndeleted;
+ guint nextended;
+ guint nignored;
+
+ union {
+ rspamd_fuzzy_check_cb cb_check;
+ rspamd_fuzzy_update_cb cb_update;
+ rspamd_fuzzy_version_cb cb_version;
+ rspamd_fuzzy_count_cb cb_count;
+ } callback;
+ void *cbdata;
+
+ gchar **argv;
+ gsize *argv_lens;
+ struct upstream *up;
+ guchar found_digest[rspamd_cryptobox_HASHBYTES];
+};
+
+static inline struct upstream_list *
+rspamd_redis_get_servers(struct rspamd_fuzzy_backend_redis *ctx,
+ const gchar *what)
+{
+ lua_State *L = ctx->L;
+ struct upstream_list *res = NULL;
+
+ lua_rawgeti(L, LUA_REGISTRYINDEX, ctx->conf_ref);
+ lua_pushstring(L, what);
+ lua_gettable(L, -2);
+
+ if (lua_type(L, -1) == LUA_TUSERDATA) {
+ res = *((struct upstream_list **) lua_touserdata(L, -1));
+ }
+ else {
+ struct lua_logger_trace tr;
+ gchar outbuf[8192];
+
+ memset(&tr, 0, sizeof(tr));
+ lua_logger_out_type(L, -2, outbuf, sizeof(outbuf) - 1, &tr,
+ LUA_ESCAPE_UNPRINTABLE);
+
+ msg_err("cannot get %s upstreams for Redis fuzzy storage %s; table content: %s",
+ what, ctx->id, outbuf);
+ }
+
+ lua_settop(L, 0);
+
+ return res;
+}
+
+static inline void
+rspamd_fuzzy_redis_session_free_args(struct rspamd_fuzzy_redis_session *session)
+{
+ guint i;
+
+ if (session->argv) {
+ for (i = 0; i < session->nargs; i++) {
+ g_free(session->argv[i]);
+ }
+
+ g_free(session->argv);
+ g_free(session->argv_lens);
+ }
+}
+static void
+rspamd_fuzzy_redis_session_dtor(struct rspamd_fuzzy_redis_session *session,
+ gboolean is_fatal)
+{
+ redisAsyncContext *ac;
+
+
+ if (session->ctx) {
+ ac = session->ctx;
+ session->ctx = NULL;
+ rspamd_redis_pool_release_connection(session->backend->pool,
+ ac,
+ is_fatal ? RSPAMD_REDIS_RELEASE_FATAL : RSPAMD_REDIS_RELEASE_DEFAULT);
+ }
+
+ ev_timer_stop(session->event_loop, &session->timeout);
+ rspamd_fuzzy_redis_session_free_args(session);
+
+ REF_RELEASE(session->backend);
+ rspamd_upstream_unref(session->up);
+ g_free(session);
+}
+
+static void
+rspamd_fuzzy_backend_redis_dtor(struct rspamd_fuzzy_backend_redis *backend)
+{
+ if (!backend->terminated && backend->conf_ref != -1) {
+ luaL_unref(backend->L, LUA_REGISTRYINDEX, backend->conf_ref);
+ }
+
+ if (backend->id) {
+ g_free(backend->id);
+ }
+
+ g_free(backend);
+}
+
+void *
+rspamd_fuzzy_backend_init_redis(struct rspamd_fuzzy_backend *bk,
+ const ucl_object_t *obj, struct rspamd_config *cfg, GError **err)
+{
+ struct rspamd_fuzzy_backend_redis *backend;
+ const ucl_object_t *elt;
+ gboolean ret = FALSE;
+ guchar id_hash[rspamd_cryptobox_HASHBYTES];
+ rspamd_cryptobox_hash_state_t st;
+ lua_State *L = (lua_State *) cfg->lua_state;
+ gint conf_ref = -1;
+
+ backend = g_malloc0(sizeof(*backend));
+
+ backend->timeout = REDIS_DEFAULT_TIMEOUT;
+ backend->redis_object = REDIS_DEFAULT_OBJECT;
+ backend->L = L;
+
+ ret = rspamd_lua_try_load_redis(L, obj, cfg, &conf_ref);
+
+ /* Now try global redis settings */
+ if (!ret) {
+ elt = ucl_object_lookup(cfg->cfg_ucl_obj, "redis");
+
+ if (elt) {
+ const ucl_object_t *specific_obj;
+
+ specific_obj = ucl_object_lookup_any(elt, "fuzzy", "fuzzy_storage",
+ NULL);
+
+ if (specific_obj) {
+ ret = rspamd_lua_try_load_redis(L, specific_obj, cfg, &conf_ref);
+ }
+ else {
+ ret = rspamd_lua_try_load_redis(L, elt, cfg, &conf_ref);
+ }
+ }
+ }
+
+ if (!ret) {
+ msg_err_config("cannot init redis backend for fuzzy storage");
+ g_free(backend);
+
+ return NULL;
+ }
+
+ elt = ucl_object_lookup(obj, "prefix");
+ if (elt == NULL || ucl_object_type(elt) != UCL_STRING) {
+ backend->redis_object = REDIS_DEFAULT_OBJECT;
+ }
+ else {
+ backend->redis_object = ucl_object_tostring(elt);
+ }
+
+ backend->conf_ref = conf_ref;
+
+ /* Check some common table values */
+ lua_rawgeti(L, LUA_REGISTRYINDEX, conf_ref);
+
+ lua_pushstring(L, "timeout");
+ lua_gettable(L, -2);
+ if (lua_type(L, -1) == LUA_TNUMBER) {
+ backend->timeout = lua_tonumber(L, -1);
+ }
+ lua_pop(L, 1);
+
+ lua_pushstring(L, "db");
+ lua_gettable(L, -2);
+ if (lua_type(L, -1) == LUA_TSTRING) {
+ backend->dbname = rspamd_mempool_strdup(cfg->cfg_pool,
+ lua_tostring(L, -1));
+ }
+ lua_pop(L, 1);
+
+ lua_pushstring(L, "username");
+ lua_gettable(L, -2);
+ if (lua_type(L, -1) == LUA_TSTRING) {
+ backend->username = rspamd_mempool_strdup(cfg->cfg_pool,
+ lua_tostring(L, -1));
+ }
+ lua_pop(L, 1);
+
+ lua_pushstring(L, "password");
+ lua_gettable(L, -2);
+ if (lua_type(L, -1) == LUA_TSTRING) {
+ backend->password = rspamd_mempool_strdup(cfg->cfg_pool,
+ lua_tostring(L, -1));
+ }
+ lua_pop(L, 1);
+
+ lua_settop(L, 0);
+
+ REF_INIT_RETAIN(backend, rspamd_fuzzy_backend_redis_dtor);
+ backend->pool = cfg->redis_pool;
+ rspamd_cryptobox_hash_init(&st, NULL, 0);
+ rspamd_cryptobox_hash_update(&st, backend->redis_object,
+ strlen(backend->redis_object));
+
+ if (backend->dbname) {
+ rspamd_cryptobox_hash_update(&st, backend->dbname,
+ strlen(backend->dbname));
+ }
+
+ if (backend->username) {
+ rspamd_cryptobox_hash_update(&st, backend->username,
+ strlen(backend->username));
+ }
+
+ if (backend->password) {
+ rspamd_cryptobox_hash_update(&st, backend->password,
+ strlen(backend->password));
+ }
+
+ rspamd_cryptobox_hash_final(&st, id_hash);
+ backend->id = rspamd_encode_base32(id_hash, sizeof(id_hash), RSPAMD_BASE32_DEFAULT);
+
+ return backend;
+}
+
+static void
+rspamd_fuzzy_redis_timeout(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_fuzzy_redis_session *session =
+ (struct rspamd_fuzzy_redis_session *) w->data;
+ redisAsyncContext *ac;
+ static char errstr[128];
+
+ if (session->ctx) {
+ ac = session->ctx;
+ session->ctx = NULL;
+ ac->err = REDIS_ERR_IO;
+ /* Should be safe as in hiredis it is char[128] */
+ rspamd_snprintf(errstr, sizeof(errstr), "%s", strerror(ETIMEDOUT));
+ ac->errstr = errstr;
+
+ /* This will cause session closing */
+ rspamd_redis_pool_release_connection(session->backend->pool,
+ ac, RSPAMD_REDIS_RELEASE_FATAL);
+ }
+}
+
+static void rspamd_fuzzy_redis_check_callback(redisAsyncContext *c, gpointer r,
+ gpointer priv);
+
+struct _rspamd_fuzzy_shingles_helper {
+ guchar digest[64];
+ guint found;
+};
+
+static gint
+rspamd_fuzzy_backend_redis_shingles_cmp(const void *a, const void *b)
+{
+ const struct _rspamd_fuzzy_shingles_helper *sha = a,
+ *shb = b;
+
+ return memcmp(sha->digest, shb->digest, sizeof(sha->digest));
+}
+
+static void
+rspamd_fuzzy_redis_shingles_callback(redisAsyncContext *c, gpointer r,
+ gpointer priv)
+{
+ struct rspamd_fuzzy_redis_session *session = priv;
+ redisReply *reply = r, *cur;
+ struct rspamd_fuzzy_reply rep;
+ GString *key;
+ struct _rspamd_fuzzy_shingles_helper *shingles, *prev = NULL, *sel = NULL;
+ guint i, found = 0, max_found = 0, cur_found = 0;
+
+ ev_timer_stop(session->event_loop, &session->timeout);
+ memset(&rep, 0, sizeof(rep));
+
+ if (c->err == 0 && reply != NULL) {
+ rspamd_upstream_ok(session->up);
+
+ if (reply->type == REDIS_REPLY_ARRAY &&
+ reply->elements == RSPAMD_SHINGLE_SIZE) {
+ shingles = g_alloca(sizeof(struct _rspamd_fuzzy_shingles_helper) *
+ RSPAMD_SHINGLE_SIZE);
+
+ for (i = 0; i < RSPAMD_SHINGLE_SIZE; i++) {
+ cur = reply->element[i];
+
+ if (cur->type == REDIS_REPLY_STRING) {
+ shingles[i].found = 1;
+ memcpy(shingles[i].digest, cur->str, MIN(64, cur->len));
+ found++;
+ }
+ else {
+ memset(shingles[i].digest, 0, sizeof(shingles[i].digest));
+ shingles[i].found = 0;
+ }
+ }
+
+ if (found > RSPAMD_SHINGLE_SIZE / 2) {
+ /* Now sort to find the most frequent element */
+ qsort(shingles, RSPAMD_SHINGLE_SIZE,
+ sizeof(struct _rspamd_fuzzy_shingles_helper),
+ rspamd_fuzzy_backend_redis_shingles_cmp);
+
+ prev = &shingles[0];
+
+ for (i = 1; i < RSPAMD_SHINGLE_SIZE; i++) {
+ if (!shingles[i].found) {
+ continue;
+ }
+
+ if (memcmp(shingles[i].digest, prev->digest, 64) == 0) {
+ cur_found++;
+
+ if (cur_found > max_found) {
+ max_found = cur_found;
+ sel = &shingles[i];
+ }
+ }
+ else {
+ cur_found = 1;
+ prev = &shingles[i];
+ }
+ }
+
+ if (max_found > RSPAMD_SHINGLE_SIZE / 2) {
+ session->prob = ((float) max_found) / RSPAMD_SHINGLE_SIZE;
+ rep.v1.prob = session->prob;
+
+ g_assert(sel != NULL);
+
+ /* Prepare new check command */
+ rspamd_fuzzy_redis_session_free_args(session);
+ session->nargs = 5;
+ session->argv = g_malloc(sizeof(gchar *) * session->nargs);
+ session->argv_lens = g_malloc(sizeof(gsize) * session->nargs);
+
+ key = g_string_new(session->backend->redis_object);
+ g_string_append_len(key, sel->digest, sizeof(sel->digest));
+ session->argv[0] = g_strdup("HMGET");
+ session->argv_lens[0] = 5;
+ session->argv[1] = key->str;
+ session->argv_lens[1] = key->len;
+ session->argv[2] = g_strdup("V");
+ session->argv_lens[2] = 1;
+ session->argv[3] = g_strdup("F");
+ session->argv_lens[3] = 1;
+ session->argv[4] = g_strdup("C");
+ session->argv_lens[4] = 1;
+ g_string_free(key, FALSE); /* Do not free underlying array */
+ memcpy(session->found_digest, sel->digest,
+ sizeof(session->cmd->digest));
+
+ g_assert(session->ctx != NULL);
+ if (redisAsyncCommandArgv(session->ctx,
+ rspamd_fuzzy_redis_check_callback,
+ session, session->nargs,
+ (const gchar **) session->argv,
+ session->argv_lens) != REDIS_OK) {
+
+ if (session->callback.cb_check) {
+ memset(&rep, 0, sizeof(rep));
+ session->callback.cb_check(&rep, session->cbdata);
+ }
+
+ rspamd_fuzzy_redis_session_dtor(session, TRUE);
+ }
+ else {
+ /* Add timeout */
+ session->timeout.data = session;
+ ev_now_update_if_cheap((struct ev_loop *) session->event_loop);
+ ev_timer_init(&session->timeout,
+ rspamd_fuzzy_redis_timeout,
+ session->backend->timeout, 0.0);
+ ev_timer_start(session->event_loop, &session->timeout);
+ }
+
+ return;
+ }
+ }
+ }
+ else if (reply->type == REDIS_REPLY_ERROR) {
+ msg_err_redis_session("fuzzy backend redis error: \"%s\"",
+ reply->str);
+ }
+
+ if (session->callback.cb_check) {
+ session->callback.cb_check(&rep, session->cbdata);
+ }
+ }
+ else {
+ if (session->callback.cb_check) {
+ session->callback.cb_check(&rep, session->cbdata);
+ }
+
+ if (c->errstr) {
+ msg_err_redis_session("error getting shingles: %s", c->errstr);
+ rspamd_upstream_fail(session->up, FALSE, c->errstr);
+ }
+ }
+
+ rspamd_fuzzy_redis_session_dtor(session, FALSE);
+}
+
+static void
+rspamd_fuzzy_backend_check_shingles(struct rspamd_fuzzy_redis_session *session)
+{
+ struct rspamd_fuzzy_reply rep;
+ const struct rspamd_fuzzy_shingle_cmd *shcmd;
+ GString *key;
+ guint i, init_len;
+
+ rspamd_fuzzy_redis_session_free_args(session);
+ /* First of all check digest */
+ session->nargs = RSPAMD_SHINGLE_SIZE + 1;
+ session->argv = g_malloc(sizeof(gchar *) * session->nargs);
+ session->argv_lens = g_malloc(sizeof(gsize) * session->nargs);
+ shcmd = (const struct rspamd_fuzzy_shingle_cmd *) session->cmd;
+
+ session->argv[0] = g_strdup("MGET");
+ session->argv_lens[0] = 4;
+ init_len = strlen(session->backend->redis_object);
+
+ for (i = 0; i < RSPAMD_SHINGLE_SIZE; i++) {
+
+ key = g_string_sized_new(init_len + 2 + 2 + sizeof("18446744073709551616"));
+ rspamd_printf_gstring(key, "%s_%d_%uL", session->backend->redis_object,
+ i, shcmd->sgl.hashes[i]);
+ session->argv[i + 1] = key->str;
+ session->argv_lens[i + 1] = key->len;
+ g_string_free(key, FALSE); /* Do not free underlying array */
+ }
+
+ session->shingles_checked = TRUE;
+
+ g_assert(session->ctx != NULL);
+
+ if (redisAsyncCommandArgv(session->ctx, rspamd_fuzzy_redis_shingles_callback,
+ session, session->nargs,
+ (const gchar **) session->argv, session->argv_lens) != REDIS_OK) {
+ msg_err("cannot execute redis command on %s: %s",
+ rspamd_inet_address_to_string_pretty(rspamd_upstream_addr_cur(session->up)),
+ session->ctx->errstr);
+
+ if (session->callback.cb_check) {
+ memset(&rep, 0, sizeof(rep));
+ session->callback.cb_check(&rep, session->cbdata);
+ }
+
+ rspamd_fuzzy_redis_session_dtor(session, TRUE);
+ }
+ else {
+ /* Add timeout */
+ session->timeout.data = session;
+ ev_now_update_if_cheap((struct ev_loop *) session->event_loop);
+ ev_timer_init(&session->timeout,
+ rspamd_fuzzy_redis_timeout,
+ session->backend->timeout, 0.0);
+ ev_timer_start(session->event_loop, &session->timeout);
+ }
+}
+
+static void
+rspamd_fuzzy_redis_check_callback(redisAsyncContext *c, gpointer r,
+ gpointer priv)
+{
+ struct rspamd_fuzzy_redis_session *session = priv;
+ redisReply *reply = r, *cur;
+ struct rspamd_fuzzy_reply rep;
+ gulong value;
+ guint found_elts = 0;
+
+ ev_timer_stop(session->event_loop, &session->timeout);
+ memset(&rep, 0, sizeof(rep));
+
+ if (c->err == 0 && reply != NULL) {
+ rspamd_upstream_ok(session->up);
+
+ if (reply->type == REDIS_REPLY_ARRAY && reply->elements >= 2) {
+ cur = reply->element[0];
+
+ if (cur->type == REDIS_REPLY_STRING) {
+ value = strtoul(cur->str, NULL, 10);
+ rep.v1.value = value;
+ found_elts++;
+ }
+
+ cur = reply->element[1];
+
+ if (cur->type == REDIS_REPLY_STRING) {
+ value = strtoul(cur->str, NULL, 10);
+ rep.v1.flag = value;
+ found_elts++;
+ }
+
+ if (found_elts >= 2) {
+ rep.v1.prob = session->prob;
+ memcpy(rep.digest, session->found_digest, sizeof(rep.digest));
+ }
+
+ rep.ts = 0;
+
+ if (reply->elements > 2) {
+ cur = reply->element[2];
+
+ if (cur->type == REDIS_REPLY_STRING) {
+ rep.ts = strtoul(cur->str, NULL, 10);
+ }
+ }
+ }
+ else if (reply->type == REDIS_REPLY_ERROR) {
+ msg_err_redis_session("fuzzy backend redis error: \"%s\"",
+ reply->str);
+ }
+
+ if (found_elts < 2) {
+ if (session->cmd->shingles_count > 0 && !session->shingles_checked) {
+ /* We also need to check all shingles here */
+ rspamd_fuzzy_backend_check_shingles(session);
+ /* Do not free session */
+ return;
+ }
+ else {
+ if (session->callback.cb_check) {
+ session->callback.cb_check(&rep, session->cbdata);
+ }
+ }
+ }
+ else {
+ if (session->callback.cb_check) {
+ session->callback.cb_check(&rep, session->cbdata);
+ }
+ }
+ }
+ else {
+ if (session->callback.cb_check) {
+ session->callback.cb_check(&rep, session->cbdata);
+ }
+
+ if (c->errstr) {
+ msg_err_redis_session("error getting hashes on %s: %s",
+ rspamd_inet_address_to_string_pretty(rspamd_upstream_addr_cur(session->up)),
+ c->errstr);
+ rspamd_upstream_fail(session->up, FALSE, c->errstr);
+ }
+ }
+
+ rspamd_fuzzy_redis_session_dtor(session, FALSE);
+}
+
+void rspamd_fuzzy_backend_check_redis(struct rspamd_fuzzy_backend *bk,
+ const struct rspamd_fuzzy_cmd *cmd,
+ rspamd_fuzzy_check_cb cb, void *ud,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_redis *backend = subr_ud;
+ struct rspamd_fuzzy_redis_session *session;
+ struct upstream *up;
+ struct upstream_list *ups;
+ rspamd_inet_addr_t *addr;
+ struct rspamd_fuzzy_reply rep;
+ GString *key;
+
+ g_assert(backend != NULL);
+
+ ups = rspamd_redis_get_servers(backend, "read_servers");
+ if (!ups) {
+ if (cb) {
+ memset(&rep, 0, sizeof(rep));
+ cb(&rep, ud);
+ }
+
+ return;
+ }
+
+ session = g_malloc0(sizeof(*session));
+ session->backend = backend;
+ REF_RETAIN(session->backend);
+
+ session->callback.cb_check = cb;
+ session->cbdata = ud;
+ session->command = RSPAMD_FUZZY_REDIS_COMMAND_CHECK;
+ session->cmd = cmd;
+ session->prob = 1.0;
+ memcpy(rep.digest, session->cmd->digest, sizeof(rep.digest));
+ memcpy(session->found_digest, session->cmd->digest, sizeof(rep.digest));
+ session->event_loop = rspamd_fuzzy_backend_event_base(bk);
+
+ /* First of all check digest */
+ session->nargs = 5;
+ session->argv = g_malloc(sizeof(gchar *) * session->nargs);
+ session->argv_lens = g_malloc(sizeof(gsize) * session->nargs);
+
+ key = g_string_new(backend->redis_object);
+ g_string_append_len(key, cmd->digest, sizeof(cmd->digest));
+ session->argv[0] = g_strdup("HMGET");
+ session->argv_lens[0] = 5;
+ session->argv[1] = key->str;
+ session->argv_lens[1] = key->len;
+ session->argv[2] = g_strdup("V");
+ session->argv_lens[2] = 1;
+ session->argv[3] = g_strdup("F");
+ session->argv_lens[3] = 1;
+ session->argv[4] = g_strdup("C");
+ session->argv_lens[4] = 1;
+ g_string_free(key, FALSE); /* Do not free underlying array */
+
+ up = rspamd_upstream_get(ups,
+ RSPAMD_UPSTREAM_ROUND_ROBIN,
+ NULL,
+ 0);
+
+ session->up = rspamd_upstream_ref(up);
+ addr = rspamd_upstream_addr_next(up);
+ g_assert(addr != NULL);
+ session->ctx = rspamd_redis_pool_connect(backend->pool,
+ backend->dbname,
+ backend->username, backend->password,
+ rspamd_inet_address_to_string(addr),
+ rspamd_inet_address_get_port(addr));
+
+ if (session->ctx == NULL) {
+ rspamd_upstream_fail(up, TRUE, strerror(errno));
+ rspamd_fuzzy_redis_session_dtor(session, TRUE);
+
+ if (cb) {
+ memset(&rep, 0, sizeof(rep));
+ cb(&rep, ud);
+ }
+ }
+ else {
+ if (redisAsyncCommandArgv(session->ctx, rspamd_fuzzy_redis_check_callback,
+ session, session->nargs,
+ (const gchar **) session->argv, session->argv_lens) != REDIS_OK) {
+ rspamd_fuzzy_redis_session_dtor(session, TRUE);
+
+ if (cb) {
+ memset(&rep, 0, sizeof(rep));
+ cb(&rep, ud);
+ }
+ }
+ else {
+ /* Add timeout */
+ session->timeout.data = session;
+ ev_now_update_if_cheap((struct ev_loop *) session->event_loop);
+ ev_timer_init(&session->timeout,
+ rspamd_fuzzy_redis_timeout,
+ session->backend->timeout, 0.0);
+ ev_timer_start(session->event_loop, &session->timeout);
+ }
+ }
+}
+
+static void
+rspamd_fuzzy_redis_count_callback(redisAsyncContext *c, gpointer r,
+ gpointer priv)
+{
+ struct rspamd_fuzzy_redis_session *session = priv;
+ redisReply *reply = r;
+ gulong nelts;
+
+ ev_timer_stop(session->event_loop, &session->timeout);
+
+ if (c->err == 0 && reply != NULL) {
+ rspamd_upstream_ok(session->up);
+
+ if (reply->type == REDIS_REPLY_INTEGER) {
+ if (session->callback.cb_count) {
+ session->callback.cb_count(reply->integer, session->cbdata);
+ }
+ }
+ else if (reply->type == REDIS_REPLY_STRING) {
+ nelts = strtoul(reply->str, NULL, 10);
+
+ if (session->callback.cb_count) {
+ session->callback.cb_count(nelts, session->cbdata);
+ }
+ }
+ else {
+ if (reply->type == REDIS_REPLY_ERROR) {
+ msg_err_redis_session("fuzzy backend redis error: \"%s\"",
+ reply->str);
+ }
+ if (session->callback.cb_count) {
+ session->callback.cb_count(0, session->cbdata);
+ }
+ }
+ }
+ else {
+ if (session->callback.cb_count) {
+ session->callback.cb_count(0, session->cbdata);
+ }
+
+ if (c->errstr) {
+ msg_err_redis_session("error getting count on %s: %s",
+ rspamd_inet_address_to_string_pretty(rspamd_upstream_addr_cur(session->up)),
+ c->errstr);
+ rspamd_upstream_fail(session->up, FALSE, c->errstr);
+ }
+ }
+
+ rspamd_fuzzy_redis_session_dtor(session, FALSE);
+}
+
+void rspamd_fuzzy_backend_count_redis(struct rspamd_fuzzy_backend *bk,
+ rspamd_fuzzy_count_cb cb, void *ud,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_redis *backend = subr_ud;
+ struct rspamd_fuzzy_redis_session *session;
+ struct upstream *up;
+ struct upstream_list *ups;
+ rspamd_inet_addr_t *addr;
+ GString *key;
+
+ g_assert(backend != NULL);
+
+ ups = rspamd_redis_get_servers(backend, "read_servers");
+ if (!ups) {
+ if (cb) {
+ cb(0, ud);
+ }
+
+ return;
+ }
+
+ session = g_malloc0(sizeof(*session));
+ session->backend = backend;
+ REF_RETAIN(session->backend);
+
+ session->callback.cb_count = cb;
+ session->cbdata = ud;
+ session->command = RSPAMD_FUZZY_REDIS_COMMAND_COUNT;
+ session->event_loop = rspamd_fuzzy_backend_event_base(bk);
+
+ session->nargs = 2;
+ session->argv = g_malloc(sizeof(gchar *) * 2);
+ session->argv_lens = g_malloc(sizeof(gsize) * 2);
+ key = g_string_new(backend->redis_object);
+ g_string_append(key, "_count");
+ session->argv[0] = g_strdup("GET");
+ session->argv_lens[0] = 3;
+ session->argv[1] = key->str;
+ session->argv_lens[1] = key->len;
+ g_string_free(key, FALSE); /* Do not free underlying array */
+
+ up = rspamd_upstream_get(ups,
+ RSPAMD_UPSTREAM_ROUND_ROBIN,
+ NULL,
+ 0);
+
+ session->up = rspamd_upstream_ref(up);
+ addr = rspamd_upstream_addr_next(up);
+ g_assert(addr != NULL);
+ session->ctx = rspamd_redis_pool_connect(backend->pool,
+ backend->dbname,
+ backend->username, backend->password,
+ rspamd_inet_address_to_string(addr),
+ rspamd_inet_address_get_port(addr));
+
+ if (session->ctx == NULL) {
+ rspamd_upstream_fail(up, TRUE, strerror(errno));
+ rspamd_fuzzy_redis_session_dtor(session, TRUE);
+
+ if (cb) {
+ cb(0, ud);
+ }
+ }
+ else {
+ if (redisAsyncCommandArgv(session->ctx, rspamd_fuzzy_redis_count_callback,
+ session, session->nargs,
+ (const gchar **) session->argv, session->argv_lens) != REDIS_OK) {
+ rspamd_fuzzy_redis_session_dtor(session, TRUE);
+
+ if (cb) {
+ cb(0, ud);
+ }
+ }
+ else {
+ /* Add timeout */
+ session->timeout.data = session;
+ ev_now_update_if_cheap((struct ev_loop *) session->event_loop);
+ ev_timer_init(&session->timeout,
+ rspamd_fuzzy_redis_timeout,
+ session->backend->timeout, 0.0);
+ ev_timer_start(session->event_loop, &session->timeout);
+ }
+ }
+}
+
+static void
+rspamd_fuzzy_redis_version_callback(redisAsyncContext *c, gpointer r,
+ gpointer priv)
+{
+ struct rspamd_fuzzy_redis_session *session = priv;
+ redisReply *reply = r;
+ gulong nelts;
+
+ ev_timer_stop(session->event_loop, &session->timeout);
+
+ if (c->err == 0 && reply != NULL) {
+ rspamd_upstream_ok(session->up);
+
+ if (reply->type == REDIS_REPLY_INTEGER) {
+ if (session->callback.cb_version) {
+ session->callback.cb_version(reply->integer, session->cbdata);
+ }
+ }
+ else if (reply->type == REDIS_REPLY_STRING) {
+ nelts = strtoul(reply->str, NULL, 10);
+
+ if (session->callback.cb_version) {
+ session->callback.cb_version(nelts, session->cbdata);
+ }
+ }
+ else {
+ if (reply->type == REDIS_REPLY_ERROR) {
+ msg_err_redis_session("fuzzy backend redis error: \"%s\"",
+ reply->str);
+ }
+ if (session->callback.cb_version) {
+ session->callback.cb_version(0, session->cbdata);
+ }
+ }
+ }
+ else {
+ if (session->callback.cb_version) {
+ session->callback.cb_version(0, session->cbdata);
+ }
+
+ if (c->errstr) {
+ msg_err_redis_session("error getting version on %s: %s",
+ rspamd_inet_address_to_string_pretty(rspamd_upstream_addr_cur(session->up)),
+ c->errstr);
+ rspamd_upstream_fail(session->up, FALSE, c->errstr);
+ }
+ }
+
+ rspamd_fuzzy_redis_session_dtor(session, FALSE);
+}
+
+void rspamd_fuzzy_backend_version_redis(struct rspamd_fuzzy_backend *bk,
+ const gchar *src,
+ rspamd_fuzzy_version_cb cb, void *ud,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_redis *backend = subr_ud;
+ struct rspamd_fuzzy_redis_session *session;
+ struct upstream *up;
+ struct upstream_list *ups;
+ rspamd_inet_addr_t *addr;
+ GString *key;
+
+ g_assert(backend != NULL);
+
+ ups = rspamd_redis_get_servers(backend, "read_servers");
+ if (!ups) {
+ if (cb) {
+ cb(0, ud);
+ }
+
+ return;
+ }
+
+ session = g_malloc0(sizeof(*session));
+ session->backend = backend;
+ REF_RETAIN(session->backend);
+
+ session->callback.cb_version = cb;
+ session->cbdata = ud;
+ session->command = RSPAMD_FUZZY_REDIS_COMMAND_VERSION;
+ session->event_loop = rspamd_fuzzy_backend_event_base(bk);
+
+ session->nargs = 2;
+ session->argv = g_malloc(sizeof(gchar *) * 2);
+ session->argv_lens = g_malloc(sizeof(gsize) * 2);
+ key = g_string_new(backend->redis_object);
+ g_string_append(key, src);
+ session->argv[0] = g_strdup("GET");
+ session->argv_lens[0] = 3;
+ session->argv[1] = key->str;
+ session->argv_lens[1] = key->len;
+ g_string_free(key, FALSE); /* Do not free underlying array */
+
+ up = rspamd_upstream_get(ups,
+ RSPAMD_UPSTREAM_ROUND_ROBIN,
+ NULL,
+ 0);
+
+ session->up = rspamd_upstream_ref(up);
+ addr = rspamd_upstream_addr_next(up);
+ g_assert(addr != NULL);
+ session->ctx = rspamd_redis_pool_connect(backend->pool,
+ backend->dbname,
+ backend->username, backend->password,
+ rspamd_inet_address_to_string(addr),
+ rspamd_inet_address_get_port(addr));
+
+ if (session->ctx == NULL) {
+ rspamd_upstream_fail(up, FALSE, strerror(errno));
+ rspamd_fuzzy_redis_session_dtor(session, TRUE);
+
+ if (cb) {
+ cb(0, ud);
+ }
+ }
+ else {
+ if (redisAsyncCommandArgv(session->ctx, rspamd_fuzzy_redis_version_callback,
+ session, session->nargs,
+ (const gchar **) session->argv, session->argv_lens) != REDIS_OK) {
+ rspamd_fuzzy_redis_session_dtor(session, TRUE);
+
+ if (cb) {
+ cb(0, ud);
+ }
+ }
+ else {
+ /* Add timeout */
+ session->timeout.data = session;
+ ev_now_update_if_cheap((struct ev_loop *) session->event_loop);
+ ev_timer_init(&session->timeout,
+ rspamd_fuzzy_redis_timeout,
+ session->backend->timeout, 0.0);
+ ev_timer_start(session->event_loop, &session->timeout);
+ }
+ }
+}
+
+const gchar *
+rspamd_fuzzy_backend_id_redis(struct rspamd_fuzzy_backend *bk,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_redis *backend = subr_ud;
+ g_assert(backend != NULL);
+
+ return backend->id;
+}
+
+void rspamd_fuzzy_backend_expire_redis(struct rspamd_fuzzy_backend *bk,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_redis *backend = subr_ud;
+
+ g_assert(backend != NULL);
+}
+
+static gboolean
+rspamd_fuzzy_update_append_command(struct rspamd_fuzzy_backend *bk,
+ struct rspamd_fuzzy_redis_session *session,
+ struct fuzzy_peer_cmd *io_cmd, guint *shift)
+{
+ GString *key, *value;
+ guint cur_shift = *shift;
+ guint i, klen;
+ struct rspamd_fuzzy_cmd *cmd;
+
+ if (io_cmd->is_shingle) {
+ cmd = &io_cmd->cmd.shingle.basic;
+ }
+ else {
+ cmd = &io_cmd->cmd.normal;
+ }
+
+ if (cmd->cmd == FUZZY_WRITE) {
+ /*
+ * For each normal hash addition we do 5 redis commands:
+ * HSET <key> F <flag>
+ * HSETNX <key> C <time>
+ * HINCRBY <key> V <weight>
+ * EXPIRE <key> <expire>
+ * Where <key> is <prefix> || <digest>
+ */
+
+ /* HSET */
+ klen = strlen(session->backend->redis_object) +
+ sizeof(cmd->digest) + 1;
+ key = g_string_sized_new(klen);
+ g_string_append(key, session->backend->redis_object);
+ g_string_append_len(key, cmd->digest, sizeof(cmd->digest));
+ value = g_string_sized_new(sizeof("4294967296"));
+ rspamd_printf_gstring(value, "%d", cmd->flag);
+
+ if (cmd->version & RSPAMD_FUZZY_FLAG_WEAK) {
+ session->argv[cur_shift] = g_strdup("HSETNX");
+ session->argv_lens[cur_shift++] = sizeof("HSETNX") - 1;
+ }
+ else {
+ session->argv[cur_shift] = g_strdup("HSET");
+ session->argv_lens[cur_shift++] = sizeof("HSET") - 1;
+ }
+
+ session->argv[cur_shift] = key->str;
+ session->argv_lens[cur_shift++] = key->len;
+ session->argv[cur_shift] = g_strdup("F");
+ session->argv_lens[cur_shift++] = sizeof("F") - 1;
+ session->argv[cur_shift] = value->str;
+ session->argv_lens[cur_shift++] = value->len;
+ g_string_free(key, FALSE);
+ g_string_free(value, FALSE);
+
+ if (redisAsyncCommandArgv(session->ctx, NULL, NULL,
+ 4,
+ (const gchar **) &session->argv[cur_shift - 4],
+ &session->argv_lens[cur_shift - 4]) != REDIS_OK) {
+
+ return FALSE;
+ }
+
+ /* HSETNX */
+ klen = strlen(session->backend->redis_object) +
+ sizeof(cmd->digest) + 1;
+ key = g_string_sized_new(klen);
+ g_string_append(key, session->backend->redis_object);
+ g_string_append_len(key, cmd->digest, sizeof(cmd->digest));
+ value = g_string_sized_new(sizeof("18446744073709551616"));
+ rspamd_printf_gstring(value, "%L", (gint64) rspamd_get_calendar_ticks());
+ session->argv[cur_shift] = g_strdup("HSETNX");
+ session->argv_lens[cur_shift++] = sizeof("HSETNX") - 1;
+ session->argv[cur_shift] = key->str;
+ session->argv_lens[cur_shift++] = key->len;
+ session->argv[cur_shift] = g_strdup("C");
+ session->argv_lens[cur_shift++] = sizeof("C") - 1;
+ session->argv[cur_shift] = value->str;
+ session->argv_lens[cur_shift++] = value->len;
+ g_string_free(key, FALSE);
+ g_string_free(value, FALSE);
+
+ if (redisAsyncCommandArgv(session->ctx, NULL, NULL,
+ 4,
+ (const gchar **) &session->argv[cur_shift - 4],
+ &session->argv_lens[cur_shift - 4]) != REDIS_OK) {
+
+ return FALSE;
+ }
+
+ /* HINCRBY */
+ key = g_string_sized_new(klen);
+ g_string_append(key, session->backend->redis_object);
+ g_string_append_len(key, cmd->digest, sizeof(cmd->digest));
+ value = g_string_sized_new(sizeof("4294967296"));
+ rspamd_printf_gstring(value, "%d", cmd->value);
+ session->argv[cur_shift] = g_strdup("HINCRBY");
+ session->argv_lens[cur_shift++] = sizeof("HINCRBY") - 1;
+ session->argv[cur_shift] = key->str;
+ session->argv_lens[cur_shift++] = key->len;
+ session->argv[cur_shift] = g_strdup("V");
+ session->argv_lens[cur_shift++] = sizeof("V") - 1;
+ session->argv[cur_shift] = value->str;
+ session->argv_lens[cur_shift++] = value->len;
+ g_string_free(key, FALSE);
+ g_string_free(value, FALSE);
+
+ if (redisAsyncCommandArgv(session->ctx, NULL, NULL,
+ 4,
+ (const gchar **) &session->argv[cur_shift - 4],
+ &session->argv_lens[cur_shift - 4]) != REDIS_OK) {
+
+ return FALSE;
+ }
+
+ /* EXPIRE */
+ key = g_string_sized_new(klen);
+ g_string_append(key, session->backend->redis_object);
+ g_string_append_len(key, cmd->digest, sizeof(cmd->digest));
+ value = g_string_sized_new(sizeof("4294967296"));
+ rspamd_printf_gstring(value, "%d",
+ (gint) rspamd_fuzzy_backend_get_expire(bk));
+ session->argv[cur_shift] = g_strdup("EXPIRE");
+ session->argv_lens[cur_shift++] = sizeof("EXPIRE") - 1;
+ session->argv[cur_shift] = key->str;
+ session->argv_lens[cur_shift++] = key->len;
+ session->argv[cur_shift] = value->str;
+ session->argv_lens[cur_shift++] = value->len;
+ g_string_free(key, FALSE);
+ g_string_free(value, FALSE);
+
+ if (redisAsyncCommandArgv(session->ctx, NULL, NULL,
+ 3,
+ (const gchar **) &session->argv[cur_shift - 3],
+ &session->argv_lens[cur_shift - 3]) != REDIS_OK) {
+
+ return FALSE;
+ }
+
+ /* INCR */
+ key = g_string_sized_new(klen);
+ g_string_append(key, session->backend->redis_object);
+ g_string_append(key, "_count");
+ session->argv[cur_shift] = g_strdup("INCR");
+ session->argv_lens[cur_shift++] = sizeof("INCR") - 1;
+ session->argv[cur_shift] = key->str;
+ session->argv_lens[cur_shift++] = key->len;
+ g_string_free(key, FALSE);
+
+ if (redisAsyncCommandArgv(session->ctx, NULL, NULL,
+ 2,
+ (const gchar **) &session->argv[cur_shift - 2],
+ &session->argv_lens[cur_shift - 2]) != REDIS_OK) {
+
+ return FALSE;
+ }
+ }
+ else if (cmd->cmd == FUZZY_DEL) {
+ /* DEL */
+ klen = strlen(session->backend->redis_object) +
+ sizeof(cmd->digest) + 1;
+
+ key = g_string_sized_new(klen);
+ g_string_append(key, session->backend->redis_object);
+ g_string_append_len(key, cmd->digest, sizeof(cmd->digest));
+ session->argv[cur_shift] = g_strdup("DEL");
+ session->argv_lens[cur_shift++] = sizeof("DEL") - 1;
+ session->argv[cur_shift] = key->str;
+ session->argv_lens[cur_shift++] = key->len;
+ g_string_free(key, FALSE);
+
+ if (redisAsyncCommandArgv(session->ctx, NULL, NULL,
+ 2,
+ (const gchar **) &session->argv[cur_shift - 2],
+ &session->argv_lens[cur_shift - 2]) != REDIS_OK) {
+
+ return FALSE;
+ }
+
+ /* DECR */
+ key = g_string_sized_new(klen);
+ g_string_append(key, session->backend->redis_object);
+ g_string_append(key, "_count");
+ session->argv[cur_shift] = g_strdup("DECR");
+ session->argv_lens[cur_shift++] = sizeof("DECR") - 1;
+ session->argv[cur_shift] = key->str;
+ session->argv_lens[cur_shift++] = key->len;
+ g_string_free(key, FALSE);
+
+ if (redisAsyncCommandArgv(session->ctx, NULL, NULL,
+ 2,
+ (const gchar **) &session->argv[cur_shift - 2],
+ &session->argv_lens[cur_shift - 2]) != REDIS_OK) {
+
+ return FALSE;
+ }
+ }
+ else if (cmd->cmd == FUZZY_REFRESH) {
+ /*
+ * Issue refresh command by just EXPIRE command
+ * EXPIRE <key> <expire>
+ * Where <key> is <prefix> || <digest>
+ */
+
+ klen = strlen(session->backend->redis_object) +
+ sizeof(cmd->digest) + 1;
+
+ /* EXPIRE */
+ key = g_string_sized_new(klen);
+ g_string_append(key, session->backend->redis_object);
+ g_string_append_len(key, cmd->digest, sizeof(cmd->digest));
+ value = g_string_sized_new(sizeof("4294967296"));
+ rspamd_printf_gstring(value, "%d",
+ (gint) rspamd_fuzzy_backend_get_expire(bk));
+ session->argv[cur_shift] = g_strdup("EXPIRE");
+ session->argv_lens[cur_shift++] = sizeof("EXPIRE") - 1;
+ session->argv[cur_shift] = key->str;
+ session->argv_lens[cur_shift++] = key->len;
+ session->argv[cur_shift] = value->str;
+ session->argv_lens[cur_shift++] = value->len;
+ g_string_free(key, FALSE);
+ g_string_free(value, FALSE);
+
+ if (redisAsyncCommandArgv(session->ctx, NULL, NULL,
+ 3,
+ (const gchar **) &session->argv[cur_shift - 3],
+ &session->argv_lens[cur_shift - 3]) != REDIS_OK) {
+
+ return FALSE;
+ }
+ }
+ else if (cmd->cmd == FUZZY_DUP) {
+ /* Ignore */
+ }
+ else {
+ g_assert_not_reached();
+ }
+
+ if (io_cmd->is_shingle) {
+ if (cmd->cmd == FUZZY_WRITE) {
+ klen = strlen(session->backend->redis_object) +
+ 64 + 1;
+
+ for (i = 0; i < RSPAMD_SHINGLE_SIZE; i++) {
+ guchar *hval;
+ /*
+ * For each command with shingles we additionally emit 32 commands:
+ * SETEX <prefix>_<number>_<value> <expire> <digest>
+ */
+
+ /* SETEX */
+ key = g_string_sized_new(klen);
+ rspamd_printf_gstring(key, "%s_%d_%uL",
+ session->backend->redis_object,
+ i,
+ io_cmd->cmd.shingle.sgl.hashes[i]);
+ value = g_string_sized_new(sizeof("4294967296"));
+ rspamd_printf_gstring(value, "%d",
+ (gint) rspamd_fuzzy_backend_get_expire(bk));
+ hval = g_malloc(sizeof(io_cmd->cmd.shingle.basic.digest));
+ memcpy(hval, io_cmd->cmd.shingle.basic.digest,
+ sizeof(io_cmd->cmd.shingle.basic.digest));
+ session->argv[cur_shift] = g_strdup("SETEX");
+ session->argv_lens[cur_shift++] = sizeof("SETEX") - 1;
+ session->argv[cur_shift] = key->str;
+ session->argv_lens[cur_shift++] = key->len;
+ session->argv[cur_shift] = value->str;
+ session->argv_lens[cur_shift++] = value->len;
+ session->argv[cur_shift] = hval;
+ session->argv_lens[cur_shift++] = sizeof(io_cmd->cmd.shingle.basic.digest);
+ g_string_free(key, FALSE);
+ g_string_free(value, FALSE);
+
+ if (redisAsyncCommandArgv(session->ctx, NULL, NULL,
+ 4,
+ (const gchar **) &session->argv[cur_shift - 4],
+ &session->argv_lens[cur_shift - 4]) != REDIS_OK) {
+
+ return FALSE;
+ }
+ }
+ }
+ else if (cmd->cmd == FUZZY_DEL) {
+ klen = strlen(session->backend->redis_object) +
+ 64 + 1;
+
+ for (i = 0; i < RSPAMD_SHINGLE_SIZE; i++) {
+ key = g_string_sized_new(klen);
+ rspamd_printf_gstring(key, "%s_%d_%uL",
+ session->backend->redis_object,
+ i,
+ io_cmd->cmd.shingle.sgl.hashes[i]);
+ session->argv[cur_shift] = g_strdup("DEL");
+ session->argv_lens[cur_shift++] = sizeof("DEL") - 1;
+ session->argv[cur_shift] = key->str;
+ session->argv_lens[cur_shift++] = key->len;
+ g_string_free(key, FALSE);
+
+ if (redisAsyncCommandArgv(session->ctx, NULL, NULL,
+ 2,
+ (const gchar **) &session->argv[cur_shift - 2],
+ &session->argv_lens[cur_shift - 2]) != REDIS_OK) {
+
+ return FALSE;
+ }
+ }
+ }
+ else if (cmd->cmd == FUZZY_REFRESH) {
+ klen = strlen(session->backend->redis_object) +
+ 64 + 1;
+
+ for (i = 0; i < RSPAMD_SHINGLE_SIZE; i++) {
+ /*
+ * For each command with shingles we additionally emit 32 commands:
+ * EXPIRE <prefix>_<number>_<value> <expire>
+ */
+
+ /* Expire */
+ key = g_string_sized_new(klen);
+ rspamd_printf_gstring(key, "%s_%d_%uL",
+ session->backend->redis_object,
+ i,
+ io_cmd->cmd.shingle.sgl.hashes[i]);
+ value = g_string_sized_new(sizeof("18446744073709551616"));
+ rspamd_printf_gstring(value, "%d",
+ (gint) rspamd_fuzzy_backend_get_expire(bk));
+ session->argv[cur_shift] = g_strdup("EXPIRE");
+ session->argv_lens[cur_shift++] = sizeof("EXPIRE") - 1;
+ session->argv[cur_shift] = key->str;
+ session->argv_lens[cur_shift++] = key->len;
+ session->argv[cur_shift] = value->str;
+ session->argv_lens[cur_shift++] = value->len;
+ g_string_free(key, FALSE);
+ g_string_free(value, FALSE);
+
+ if (redisAsyncCommandArgv(session->ctx, NULL, NULL,
+ 3,
+ (const gchar **) &session->argv[cur_shift - 3],
+ &session->argv_lens[cur_shift - 3]) != REDIS_OK) {
+
+ return FALSE;
+ }
+ }
+ }
+ else if (cmd->cmd == FUZZY_DUP) {
+ /* Ignore */
+ }
+ else {
+ g_assert_not_reached();
+ }
+ }
+
+ *shift = cur_shift;
+
+ return TRUE;
+}
+
+static void
+rspamd_fuzzy_redis_update_callback(redisAsyncContext *c, gpointer r,
+ gpointer priv)
+{
+ struct rspamd_fuzzy_redis_session *session = priv;
+ redisReply *reply = r;
+
+ ev_timer_stop(session->event_loop, &session->timeout);
+
+ if (c->err == 0 && reply != NULL) {
+ rspamd_upstream_ok(session->up);
+
+ if (reply->type == REDIS_REPLY_ARRAY) {
+ /* TODO: check all replies somehow */
+ if (session->callback.cb_update) {
+ session->callback.cb_update(TRUE,
+ session->nadded,
+ session->ndeleted,
+ session->nextended,
+ session->nignored,
+ session->cbdata);
+ }
+ }
+ else {
+ if (reply->type == REDIS_REPLY_ERROR) {
+ msg_err_redis_session("fuzzy backend redis error: \"%s\"",
+ reply->str);
+ }
+ if (session->callback.cb_update) {
+ session->callback.cb_update(FALSE, 0, 0, 0, 0, session->cbdata);
+ }
+ }
+ }
+ else {
+ if (session->callback.cb_update) {
+ session->callback.cb_update(FALSE, 0, 0, 0, 0, session->cbdata);
+ }
+
+ if (c->errstr) {
+ msg_err_redis_session("error sending update to redis %s: %s",
+ rspamd_inet_address_to_string_pretty(rspamd_upstream_addr_cur(session->up)),
+ c->errstr);
+ rspamd_upstream_fail(session->up, FALSE, c->errstr);
+ }
+ }
+
+ rspamd_fuzzy_redis_session_dtor(session, FALSE);
+}
+
+void rspamd_fuzzy_backend_update_redis(struct rspamd_fuzzy_backend *bk,
+ GArray *updates, const gchar *src,
+ rspamd_fuzzy_update_cb cb, void *ud,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_redis *backend = subr_ud;
+ struct rspamd_fuzzy_redis_session *session;
+ struct upstream *up;
+ struct upstream_list *ups;
+ rspamd_inet_addr_t *addr;
+ guint i;
+ GString *key;
+ struct fuzzy_peer_cmd *io_cmd;
+ struct rspamd_fuzzy_cmd *cmd = NULL;
+ guint nargs, cur_shift;
+
+ g_assert(backend != NULL);
+
+ ups = rspamd_redis_get_servers(backend, "write_servers");
+ if (!ups) {
+ if (cb) {
+ cb(FALSE, 0, 0, 0, 0, ud);
+ }
+
+ return;
+ }
+
+ session = g_malloc0(sizeof(*session));
+ session->backend = backend;
+ REF_RETAIN(session->backend);
+
+ /*
+ * For each normal hash addition we do 3 redis commands:
+ * HSET <key> F <flag> **OR** HSETNX <key> F <flag> when flag is weak
+ * HINCRBY <key> V <weight>
+ * EXPIRE <key> <expire>
+ * INCR <prefix||fuzzy_count>
+ *
+ * Where <key> is <prefix> || <digest>
+ *
+ * For each command with shingles we additionally emit 32 commands:
+ * SETEX <prefix>_<number>_<value> <expire> <digest>
+ *
+ * For each delete command we emit:
+ * DEL <key>
+ *
+ * For each delete command with shingles we emit also 32 commands:
+ * DEL <prefix>_<number>_<value>
+ * DECR <prefix||fuzzy_count>
+ */
+
+ nargs = 4;
+
+ for (i = 0; i < updates->len; i++) {
+ io_cmd = &g_array_index(updates, struct fuzzy_peer_cmd, i);
+
+ if (io_cmd->is_shingle) {
+ cmd = &io_cmd->cmd.shingle.basic;
+ }
+ else {
+ cmd = &io_cmd->cmd.normal;
+ }
+
+ if (cmd->cmd == FUZZY_WRITE) {
+ nargs += 17;
+ session->nadded++;
+
+ if (io_cmd->is_shingle) {
+ nargs += RSPAMD_SHINGLE_SIZE * 4;
+ }
+ }
+ else if (cmd->cmd == FUZZY_DEL) {
+ nargs += 4;
+ session->ndeleted++;
+
+ if (io_cmd->is_shingle) {
+ nargs += RSPAMD_SHINGLE_SIZE * 2;
+ }
+ }
+ else if (cmd->cmd == FUZZY_REFRESH) {
+ nargs += 3;
+ session->nextended++;
+
+ if (io_cmd->is_shingle) {
+ nargs += RSPAMD_SHINGLE_SIZE * 3;
+ }
+ }
+ else {
+ session->nignored++;
+ }
+ }
+
+ /* Now we need to create a new request */
+ session->callback.cb_update = cb;
+ session->cbdata = ud;
+ session->command = RSPAMD_FUZZY_REDIS_COMMAND_UPDATES;
+ session->cmd = cmd;
+ session->prob = 1.0f;
+ session->event_loop = rspamd_fuzzy_backend_event_base(bk);
+
+ /* First of all check digest */
+ session->nargs = nargs;
+ session->argv = g_malloc0(sizeof(gchar *) * session->nargs);
+ session->argv_lens = g_malloc0(sizeof(gsize) * session->nargs);
+
+ up = rspamd_upstream_get(ups,
+ RSPAMD_UPSTREAM_MASTER_SLAVE,
+ NULL,
+ 0);
+
+ session->up = rspamd_upstream_ref(up);
+ addr = rspamd_upstream_addr_next(up);
+ g_assert(addr != NULL);
+ session->ctx = rspamd_redis_pool_connect(backend->pool,
+ backend->dbname,
+ backend->username, backend->password,
+ rspamd_inet_address_to_string(addr),
+ rspamd_inet_address_get_port(addr));
+
+ if (session->ctx == NULL) {
+ rspamd_upstream_fail(up, TRUE, strerror(errno));
+ rspamd_fuzzy_redis_session_dtor(session, TRUE);
+
+ if (cb) {
+ cb(FALSE, 0, 0, 0, 0, ud);
+ }
+ }
+ else {
+ /* Start with MULTI command */
+ session->argv[0] = g_strdup("MULTI");
+ session->argv_lens[0] = 5;
+
+ if (redisAsyncCommandArgv(session->ctx, NULL, NULL,
+ 1,
+ (const gchar **) session->argv,
+ session->argv_lens) != REDIS_OK) {
+
+ if (cb) {
+ cb(FALSE, 0, 0, 0, 0, ud);
+ }
+ rspamd_fuzzy_redis_session_dtor(session, TRUE);
+
+ return;
+ }
+
+ /* Now split the rest of commands in packs and emit them command by command */
+ cur_shift = 1;
+
+ for (i = 0; i < updates->len; i++) {
+ io_cmd = &g_array_index(updates, struct fuzzy_peer_cmd, i);
+
+ if (!rspamd_fuzzy_update_append_command(bk, session, io_cmd,
+ &cur_shift)) {
+ if (cb) {
+ cb(FALSE, 0, 0, 0, 0, ud);
+ }
+ rspamd_fuzzy_redis_session_dtor(session, TRUE);
+
+ return;
+ }
+ }
+
+ /* Now INCR command for the source */
+ key = g_string_new(backend->redis_object);
+ g_string_append(key, src);
+ session->argv[cur_shift] = g_strdup("INCR");
+ session->argv_lens[cur_shift++] = 4;
+ session->argv[cur_shift] = key->str;
+ session->argv_lens[cur_shift++] = key->len;
+ g_string_free(key, FALSE);
+
+ if (redisAsyncCommandArgv(session->ctx, NULL, NULL,
+ 2,
+ (const gchar **) &session->argv[cur_shift - 2],
+ &session->argv_lens[cur_shift - 2]) != REDIS_OK) {
+
+ if (cb) {
+ cb(FALSE, 0, 0, 0, 0, ud);
+ }
+ rspamd_fuzzy_redis_session_dtor(session, TRUE);
+
+ return;
+ }
+
+ /* Finally we call EXEC with a specific callback */
+ session->argv[cur_shift] = g_strdup("EXEC");
+ session->argv_lens[cur_shift] = 4;
+
+ if (redisAsyncCommandArgv(session->ctx,
+ rspamd_fuzzy_redis_update_callback, session,
+ 1,
+ (const gchar **) &session->argv[cur_shift],
+ &session->argv_lens[cur_shift]) != REDIS_OK) {
+
+ if (cb) {
+ cb(FALSE, 0, 0, 0, 0, ud);
+ }
+ rspamd_fuzzy_redis_session_dtor(session, TRUE);
+
+ return;
+ }
+ else {
+ /* Add timeout */
+ session->timeout.data = session;
+ ev_now_update_if_cheap((struct ev_loop *) session->event_loop);
+ ev_timer_init(&session->timeout,
+ rspamd_fuzzy_redis_timeout,
+ session->backend->timeout, 0.0);
+ ev_timer_start(session->event_loop, &session->timeout);
+ }
+ }
+}
+
+void rspamd_fuzzy_backend_close_redis(struct rspamd_fuzzy_backend *bk,
+ void *subr_ud)
+{
+ struct rspamd_fuzzy_backend_redis *backend = subr_ud;
+
+ g_assert(backend != NULL);
+
+ /*
+ * XXX: we leak lua registry element there to avoid crashing
+ * due to chicken-egg problem between lua state termination and
+ * redis pool termination.
+ * Here, we assume that redis pool is destroyed AFTER lua_state,
+ * so all connections pending will release references but due to
+ * `terminated` hack they will not try to access Lua stuff
+ * This is enabled merely if we have connections pending (e.g. refcount > 1)
+ */
+ if (backend->ref.refcount > 1) {
+ backend->terminated = true;
+ }
+ REF_RELEASE(backend);
+}
diff --git a/src/libserver/fuzzy_backend/fuzzy_backend_redis.h b/src/libserver/fuzzy_backend/fuzzy_backend_redis.h
new file mode 100644
index 0000000..3cfa162
--- /dev/null
+++ b/src/libserver/fuzzy_backend/fuzzy_backend_redis.h
@@ -0,0 +1,67 @@
+/*-
+ * 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 SRC_LIBSERVER_FUZZY_BACKEND_REDIS_H_
+#define SRC_LIBSERVER_FUZZY_BACKEND_REDIS_H_
+
+#include "config.h"
+#include "fuzzy_backend.h"
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Subroutines for fuzzy_backend
+ */
+void *rspamd_fuzzy_backend_init_redis(struct rspamd_fuzzy_backend *bk,
+ const ucl_object_t *obj,
+ struct rspamd_config *cfg,
+ GError **err);
+
+void rspamd_fuzzy_backend_check_redis(struct rspamd_fuzzy_backend *bk,
+ const struct rspamd_fuzzy_cmd *cmd,
+ rspamd_fuzzy_check_cb cb, void *ud,
+ void *subr_ud);
+
+void rspamd_fuzzy_backend_update_redis(struct rspamd_fuzzy_backend *bk,
+ GArray *updates, const gchar *src,
+ rspamd_fuzzy_update_cb cb, void *ud,
+ void *subr_ud);
+
+void rspamd_fuzzy_backend_count_redis(struct rspamd_fuzzy_backend *bk,
+ rspamd_fuzzy_count_cb cb, void *ud,
+ void *subr_ud);
+
+void rspamd_fuzzy_backend_version_redis(struct rspamd_fuzzy_backend *bk,
+ const gchar *src,
+ rspamd_fuzzy_version_cb cb, void *ud,
+ void *subr_ud);
+
+const gchar *rspamd_fuzzy_backend_id_redis(struct rspamd_fuzzy_backend *bk,
+ void *subr_ud);
+
+void rspamd_fuzzy_backend_expire_redis(struct rspamd_fuzzy_backend *bk,
+ void *subr_ud);
+
+void rspamd_fuzzy_backend_close_redis(struct rspamd_fuzzy_backend *bk,
+ void *subr_ud);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SRC_LIBSERVER_FUZZY_BACKEND_REDIS_H_ */
diff --git a/src/libserver/fuzzy_backend/fuzzy_backend_sqlite.c b/src/libserver/fuzzy_backend/fuzzy_backend_sqlite.c
new file mode 100644
index 0000000..9ec448e
--- /dev/null
+++ b/src/libserver/fuzzy_backend/fuzzy_backend_sqlite.c
@@ -0,0 +1,1029 @@
+/*-
+ * 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 "rspamd.h"
+#include "fuzzy_backend.h"
+#include "fuzzy_backend_sqlite.h"
+#include "unix-std.h"
+
+#include <sqlite3.h>
+#include "libutil/sqlite_utils.h"
+
+struct rspamd_fuzzy_backend_sqlite {
+ sqlite3 *db;
+ char *path;
+ gchar id[MEMPOOL_UID_LEN];
+ gsize count;
+ gsize expired;
+ rspamd_mempool_t *pool;
+};
+
+static const gdouble sql_sleep_time = 0.1;
+static const guint max_retries = 10;
+
+#define msg_err_fuzzy_backend(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ backend->pool->tag.tagname, backend->pool->tag.uid, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+#define msg_warn_fuzzy_backend(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ backend->pool->tag.tagname, backend->pool->tag.uid, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+#define msg_info_fuzzy_backend(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ backend->pool->tag.tagname, backend->pool->tag.uid, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+#define msg_debug_fuzzy_backend(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_fuzzy_sqlite_log_id, backend->pool->tag.tagname, backend->pool->tag.uid, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+
+INIT_LOG_MODULE(fuzzy_sqlite)
+
+static const char *create_tables_sql =
+ "BEGIN;"
+ "CREATE TABLE IF NOT EXISTS digests("
+ " id INTEGER PRIMARY KEY,"
+ " flag INTEGER NOT NULL,"
+ " digest TEXT NOT NULL,"
+ " value INTEGER,"
+ " time INTEGER);"
+ "CREATE TABLE IF NOT EXISTS shingles("
+ " value INTEGER NOT NULL,"
+ " number INTEGER NOT NULL,"
+ " digest_id INTEGER REFERENCES digests(id) ON DELETE CASCADE "
+ " ON UPDATE CASCADE);"
+ "CREATE TABLE IF NOT EXISTS sources("
+ " name TEXT UNIQUE,"
+ " version INTEGER,"
+ " last INTEGER);"
+ "CREATE UNIQUE INDEX IF NOT EXISTS d ON digests(digest);"
+ "CREATE INDEX IF NOT EXISTS t ON digests(time);"
+ "CREATE INDEX IF NOT EXISTS dgst_id ON shingles(digest_id);"
+ "CREATE UNIQUE INDEX IF NOT EXISTS s ON shingles(value, number);"
+ "COMMIT;";
+#if 0
+static const char *create_index_sql =
+ "BEGIN;"
+ "CREATE UNIQUE INDEX IF NOT EXISTS d ON digests(digest);"
+ "CREATE INDEX IF NOT EXISTS t ON digests(time);"
+ "CREATE INDEX IF NOT EXISTS dgst_id ON shingles(digest_id);"
+ "CREATE UNIQUE INDEX IF NOT EXISTS s ON shingles(value, number);"
+ "COMMIT;";
+#endif
+enum rspamd_fuzzy_statement_idx {
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_START = 0,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_COMMIT,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_ROLLBACK,
+ RSPAMD_FUZZY_BACKEND_INSERT,
+ RSPAMD_FUZZY_BACKEND_UPDATE,
+ RSPAMD_FUZZY_BACKEND_UPDATE_FLAG,
+ RSPAMD_FUZZY_BACKEND_INSERT_SHINGLE,
+ RSPAMD_FUZZY_BACKEND_CHECK,
+ RSPAMD_FUZZY_BACKEND_CHECK_SHINGLE,
+ RSPAMD_FUZZY_BACKEND_GET_DIGEST_BY_ID,
+ RSPAMD_FUZZY_BACKEND_DELETE,
+ RSPAMD_FUZZY_BACKEND_COUNT,
+ RSPAMD_FUZZY_BACKEND_EXPIRE,
+ RSPAMD_FUZZY_BACKEND_VACUUM,
+ RSPAMD_FUZZY_BACKEND_DELETE_ORPHANED,
+ RSPAMD_FUZZY_BACKEND_ADD_SOURCE,
+ RSPAMD_FUZZY_BACKEND_VERSION,
+ RSPAMD_FUZZY_BACKEND_SET_VERSION,
+ RSPAMD_FUZZY_BACKEND_MAX
+};
+static struct rspamd_fuzzy_stmts {
+ enum rspamd_fuzzy_statement_idx idx;
+ const gchar *sql;
+ const gchar *args;
+ sqlite3_stmt *stmt;
+ gint result;
+} prepared_stmts[RSPAMD_FUZZY_BACKEND_MAX] =
+ {
+ {.idx = RSPAMD_FUZZY_BACKEND_TRANSACTION_START,
+ .sql = "BEGIN TRANSACTION;",
+ .args = "",
+ .stmt = NULL,
+ .result = SQLITE_DONE},
+ {.idx = RSPAMD_FUZZY_BACKEND_TRANSACTION_COMMIT,
+ .sql = "COMMIT;",
+ .args = "",
+ .stmt = NULL,
+ .result = SQLITE_DONE},
+ {.idx = RSPAMD_FUZZY_BACKEND_TRANSACTION_ROLLBACK,
+ .sql = "ROLLBACK;",
+ .args = "",
+ .stmt = NULL,
+ .result = SQLITE_DONE},
+ {.idx = RSPAMD_FUZZY_BACKEND_INSERT,
+ .sql = "INSERT INTO digests(flag, digest, value, time) VALUES"
+ "(?1, ?2, ?3, strftime('%s','now'));",
+ .args = "SDI",
+ .stmt = NULL,
+ .result = SQLITE_DONE},
+ {.idx = RSPAMD_FUZZY_BACKEND_UPDATE,
+ .sql = "UPDATE digests SET value = value + ?1, time = strftime('%s','now') WHERE "
+ "digest==?2;",
+ .args = "ID",
+ .stmt = NULL,
+ .result = SQLITE_DONE},
+ {.idx = RSPAMD_FUZZY_BACKEND_UPDATE_FLAG,
+ .sql = "UPDATE digests SET value = ?1, flag = ?2, time = strftime('%s','now') WHERE "
+ "digest==?3;",
+ .args = "IID",
+ .stmt = NULL,
+ .result = SQLITE_DONE},
+ {.idx = RSPAMD_FUZZY_BACKEND_INSERT_SHINGLE,
+ .sql = "INSERT OR REPLACE INTO shingles(value, number, digest_id) "
+ "VALUES (?1, ?2, ?3);",
+ .args = "III",
+ .stmt = NULL,
+ .result = SQLITE_DONE},
+ {.idx = RSPAMD_FUZZY_BACKEND_CHECK,
+ .sql = "SELECT value, time, flag FROM digests WHERE digest==?1;",
+ .args = "D",
+ .stmt = NULL,
+ .result = SQLITE_ROW},
+ {.idx = RSPAMD_FUZZY_BACKEND_CHECK_SHINGLE,
+ .sql = "SELECT digest_id FROM shingles WHERE value=?1 AND number=?2",
+ .args = "IS",
+ .stmt = NULL,
+ .result = SQLITE_ROW},
+ {.idx = RSPAMD_FUZZY_BACKEND_GET_DIGEST_BY_ID,
+ .sql = "SELECT digest, value, time, flag FROM digests WHERE id=?1",
+ .args = "I",
+ .stmt = NULL,
+ .result = SQLITE_ROW},
+ {.idx = RSPAMD_FUZZY_BACKEND_DELETE,
+ .sql = "DELETE FROM digests WHERE digest==?1;",
+ .args = "D",
+ .stmt = NULL,
+ .result = SQLITE_DONE},
+ {.idx = RSPAMD_FUZZY_BACKEND_COUNT,
+ .sql = "SELECT COUNT(*) FROM digests;",
+ .args = "",
+ .stmt = NULL,
+ .result = SQLITE_ROW},
+ {.idx = RSPAMD_FUZZY_BACKEND_EXPIRE,
+ .sql = "DELETE FROM digests WHERE id IN (SELECT id FROM digests WHERE time < ?1 LIMIT ?2);",
+ .args = "II",
+ .stmt = NULL,
+ .result = SQLITE_DONE},
+ {.idx = RSPAMD_FUZZY_BACKEND_VACUUM,
+ .sql = "VACUUM;",
+ .args = "",
+ .stmt = NULL,
+ .result = SQLITE_DONE},
+ {.idx = RSPAMD_FUZZY_BACKEND_DELETE_ORPHANED,
+ .sql = "DELETE FROM shingles WHERE value=?1 AND number=?2;",
+ .args = "II",
+ .stmt = NULL,
+ .result = SQLITE_DONE},
+ {.idx = RSPAMD_FUZZY_BACKEND_ADD_SOURCE,
+ .sql = "INSERT OR IGNORE INTO sources(name, version, last) VALUES (?1, ?2, ?3);",
+ .args = "TII",
+ .stmt = NULL,
+ .result = SQLITE_DONE},
+ {.idx = RSPAMD_FUZZY_BACKEND_VERSION,
+ .sql = "SELECT version FROM sources WHERE name=?1;",
+ .args = "T",
+ .stmt = NULL,
+ .result = SQLITE_ROW},
+ {.idx = RSPAMD_FUZZY_BACKEND_SET_VERSION,
+ .sql = "INSERT OR REPLACE INTO sources (name, version, last) VALUES (?3, ?1, ?2);",
+ .args = "IIT",
+ .stmt = NULL,
+ .result = SQLITE_DONE},
+};
+
+static GQuark
+rspamd_fuzzy_backend_sqlite_quark(void)
+{
+ return g_quark_from_static_string("fuzzy-backend-sqlite");
+}
+
+static gboolean
+rspamd_fuzzy_backend_sqlite_prepare_stmts(struct rspamd_fuzzy_backend_sqlite *bk, GError **err)
+{
+ int i;
+
+ for (i = 0; i < RSPAMD_FUZZY_BACKEND_MAX; i++) {
+ if (prepared_stmts[i].stmt != NULL) {
+ /* Skip already prepared statements */
+ continue;
+ }
+ if (sqlite3_prepare_v2(bk->db, prepared_stmts[i].sql, -1,
+ &prepared_stmts[i].stmt, NULL) != SQLITE_OK) {
+ g_set_error(err, rspamd_fuzzy_backend_sqlite_quark(),
+ -1, "Cannot initialize prepared sql `%s`: %s",
+ prepared_stmts[i].sql, sqlite3_errmsg(bk->db));
+
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+static int
+rspamd_fuzzy_backend_sqlite_cleanup_stmt(struct rspamd_fuzzy_backend_sqlite *backend,
+ int idx)
+{
+ sqlite3_stmt *stmt;
+
+ if (idx < 0 || idx >= RSPAMD_FUZZY_BACKEND_MAX) {
+
+ return -1;
+ }
+
+ msg_debug_fuzzy_backend("resetting `%s`", prepared_stmts[idx].sql);
+ stmt = prepared_stmts[idx].stmt;
+ sqlite3_clear_bindings(stmt);
+ sqlite3_reset(stmt);
+
+ return SQLITE_OK;
+}
+
+static int
+rspamd_fuzzy_backend_sqlite_run_stmt(struct rspamd_fuzzy_backend_sqlite *backend,
+ gboolean auto_cleanup,
+ int idx, ...)
+{
+ int retcode;
+ va_list ap;
+ sqlite3_stmt *stmt;
+ int i;
+ const char *argtypes;
+ guint retries = 0;
+ struct timespec ts;
+
+ if (idx < 0 || idx >= RSPAMD_FUZZY_BACKEND_MAX) {
+
+ return -1;
+ }
+
+ stmt = prepared_stmts[idx].stmt;
+ g_assert((int) prepared_stmts[idx].idx == idx);
+
+ if (stmt == NULL) {
+ if ((retcode = sqlite3_prepare_v2(backend->db, prepared_stmts[idx].sql, -1,
+ &prepared_stmts[idx].stmt, NULL)) != SQLITE_OK) {
+ msg_err_fuzzy_backend("Cannot initialize prepared sql `%s`: %s",
+ prepared_stmts[idx].sql, sqlite3_errmsg(backend->db));
+
+ return retcode;
+ }
+ stmt = prepared_stmts[idx].stmt;
+ }
+
+ msg_debug_fuzzy_backend("executing `%s` %s auto cleanup",
+ prepared_stmts[idx].sql, auto_cleanup ? "with" : "without");
+ argtypes = prepared_stmts[idx].args;
+ sqlite3_clear_bindings(stmt);
+ sqlite3_reset(stmt);
+ va_start(ap, idx);
+
+ for (i = 0; argtypes[i] != '\0'; i++) {
+ switch (argtypes[i]) {
+ case 'T':
+ sqlite3_bind_text(stmt, i + 1, va_arg(ap, const char *), -1,
+ SQLITE_STATIC);
+ break;
+ case 'I':
+ sqlite3_bind_int64(stmt, i + 1, va_arg(ap, gint64));
+ break;
+ case 'S':
+ sqlite3_bind_int(stmt, i + 1, va_arg(ap, gint));
+ break;
+ case 'D':
+ /* Special case for digests variable */
+ sqlite3_bind_text(stmt, i + 1, va_arg(ap, const char *), 64,
+ SQLITE_STATIC);
+ break;
+ }
+ }
+
+ va_end(ap);
+
+retry:
+ retcode = sqlite3_step(stmt);
+
+ if (retcode == prepared_stmts[idx].result) {
+ retcode = SQLITE_OK;
+ }
+ else {
+ if ((retcode == SQLITE_BUSY ||
+ retcode == SQLITE_LOCKED) &&
+ retries++ < max_retries) {
+ double_to_ts(sql_sleep_time, &ts);
+ nanosleep(&ts, NULL);
+ goto retry;
+ }
+
+ msg_debug_fuzzy_backend("failed to execute query %s: %d, %s", prepared_stmts[idx].sql,
+ retcode, sqlite3_errmsg(backend->db));
+ }
+
+ if (auto_cleanup) {
+ sqlite3_clear_bindings(stmt);
+ sqlite3_reset(stmt);
+ }
+
+ return retcode;
+}
+
+static void
+rspamd_fuzzy_backend_sqlite_close_stmts(struct rspamd_fuzzy_backend_sqlite *bk)
+{
+ int i;
+
+ for (i = 0; i < RSPAMD_FUZZY_BACKEND_MAX; i++) {
+ if (prepared_stmts[i].stmt != NULL) {
+ sqlite3_finalize(prepared_stmts[i].stmt);
+ prepared_stmts[i].stmt = NULL;
+ }
+ }
+
+ return;
+}
+
+static gboolean
+rspamd_fuzzy_backend_sqlite_run_sql(const gchar *sql, struct rspamd_fuzzy_backend_sqlite *bk,
+ GError **err)
+{
+ guint retries = 0;
+ struct timespec ts;
+ gint ret;
+
+ do {
+ ret = sqlite3_exec(bk->db, sql, NULL, NULL, NULL);
+ double_to_ts(sql_sleep_time, &ts);
+ } while (ret == SQLITE_BUSY && retries++ < max_retries &&
+ nanosleep(&ts, NULL) == 0);
+
+ if (ret != SQLITE_OK) {
+ g_set_error(err, rspamd_fuzzy_backend_sqlite_quark(),
+ -1, "Cannot execute raw sql `%s`: %s",
+ sql, sqlite3_errmsg(bk->db));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static struct rspamd_fuzzy_backend_sqlite *
+rspamd_fuzzy_backend_sqlite_open_db(const gchar *path, GError **err)
+{
+ struct rspamd_fuzzy_backend_sqlite *bk;
+ rspamd_cryptobox_hash_state_t st;
+ guchar hash_out[rspamd_cryptobox_HASHBYTES];
+
+ g_assert(path != NULL);
+
+ bk = g_malloc0(sizeof(*bk));
+ bk->path = g_strdup(path);
+ bk->expired = 0;
+ bk->pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ "fuzzy_backend", 0);
+ bk->db = rspamd_sqlite3_open_or_create(bk->pool, bk->path,
+ create_tables_sql, 1, err);
+
+ if (bk->db == NULL) {
+ rspamd_fuzzy_backend_sqlite_close(bk);
+
+ return NULL;
+ }
+
+ if (!rspamd_fuzzy_backend_sqlite_prepare_stmts(bk, err)) {
+ rspamd_fuzzy_backend_sqlite_close(bk);
+
+ return NULL;
+ }
+
+ /* Set id for the backend */
+ rspamd_cryptobox_hash_init(&st, NULL, 0);
+ rspamd_cryptobox_hash_update(&st, path, strlen(path));
+ rspamd_cryptobox_hash_final(&st, hash_out);
+ rspamd_snprintf(bk->id, sizeof(bk->id), "%xs", hash_out);
+ memcpy(bk->pool->tag.uid, bk->id, sizeof(bk->pool->tag.uid));
+
+ return bk;
+}
+
+struct rspamd_fuzzy_backend_sqlite *
+rspamd_fuzzy_backend_sqlite_open(const gchar *path,
+ gboolean vacuum,
+ GError **err)
+{
+ struct rspamd_fuzzy_backend_sqlite *backend;
+
+ if (path == NULL) {
+ g_set_error(err, rspamd_fuzzy_backend_sqlite_quark(),
+ ENOENT, "Path has not been specified");
+ return NULL;
+ }
+
+ /* Open database */
+ if ((backend = rspamd_fuzzy_backend_sqlite_open_db(path, err)) == NULL) {
+ return NULL;
+ }
+
+ if (rspamd_fuzzy_backend_sqlite_run_stmt(backend, FALSE, RSPAMD_FUZZY_BACKEND_COUNT) == SQLITE_OK) {
+ backend->count = sqlite3_column_int64(
+ prepared_stmts[RSPAMD_FUZZY_BACKEND_COUNT].stmt, 0);
+ }
+
+ rspamd_fuzzy_backend_sqlite_cleanup_stmt(backend, RSPAMD_FUZZY_BACKEND_COUNT);
+
+ return backend;
+}
+
+static gint
+rspamd_fuzzy_backend_sqlite_int64_cmp(const void *a, const void *b)
+{
+ gint64 ia = *(gint64 *) a, ib = *(gint64 *) b;
+
+ return (ia - ib);
+}
+
+struct rspamd_fuzzy_reply
+rspamd_fuzzy_backend_sqlite_check(struct rspamd_fuzzy_backend_sqlite *backend,
+ const struct rspamd_fuzzy_cmd *cmd, gint64 expire)
+{
+ struct rspamd_fuzzy_reply rep;
+ const struct rspamd_fuzzy_shingle_cmd *shcmd;
+ int rc;
+ gint64 timestamp;
+ gint64 shingle_values[RSPAMD_SHINGLE_SIZE], i, sel_id, cur_id,
+ cur_cnt, max_cnt;
+
+ memset(&rep, 0, sizeof(rep));
+ memcpy(rep.digest, cmd->digest, sizeof(rep.digest));
+
+ if (backend == NULL) {
+ return rep;
+ }
+
+ /* Try direct match first of all */
+ rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_START);
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, FALSE,
+ RSPAMD_FUZZY_BACKEND_CHECK,
+ cmd->digest);
+
+ if (rc == SQLITE_OK) {
+ timestamp = sqlite3_column_int64(
+ prepared_stmts[RSPAMD_FUZZY_BACKEND_CHECK].stmt, 1);
+ if (time(NULL) - timestamp > expire) {
+ /* Expire element */
+ msg_debug_fuzzy_backend("requested hash has been expired");
+ }
+ else {
+ rep.v1.value = sqlite3_column_int64(
+ prepared_stmts[RSPAMD_FUZZY_BACKEND_CHECK].stmt, 0);
+ rep.v1.prob = 1.0;
+ rep.v1.flag = sqlite3_column_int(
+ prepared_stmts[RSPAMD_FUZZY_BACKEND_CHECK].stmt, 2);
+ }
+ }
+ else if (cmd->shingles_count > 0) {
+ /* Fuzzy match */
+
+ rspamd_fuzzy_backend_sqlite_cleanup_stmt(backend, RSPAMD_FUZZY_BACKEND_CHECK);
+ shcmd = (const struct rspamd_fuzzy_shingle_cmd *) cmd;
+
+ for (i = 0; i < RSPAMD_SHINGLE_SIZE; i++) {
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, FALSE,
+ RSPAMD_FUZZY_BACKEND_CHECK_SHINGLE,
+ shcmd->sgl.hashes[i], i);
+ if (rc == SQLITE_OK) {
+ shingle_values[i] = sqlite3_column_int64(
+ prepared_stmts[RSPAMD_FUZZY_BACKEND_CHECK_SHINGLE].stmt,
+ 0);
+ }
+ else {
+ shingle_values[i] = -1;
+ }
+ msg_debug_fuzzy_backend("looking for shingle %L -> %L: %d", i,
+ shcmd->sgl.hashes[i], rc);
+ }
+
+ rspamd_fuzzy_backend_sqlite_cleanup_stmt(backend,
+ RSPAMD_FUZZY_BACKEND_CHECK_SHINGLE);
+
+ qsort(shingle_values, RSPAMD_SHINGLE_SIZE, sizeof(gint64),
+ rspamd_fuzzy_backend_sqlite_int64_cmp);
+ sel_id = -1;
+ cur_id = -1;
+ cur_cnt = 0;
+ max_cnt = 0;
+
+ for (i = 0; i < RSPAMD_SHINGLE_SIZE; i++) {
+ if (shingle_values[i] == -1) {
+ continue;
+ }
+
+ /* We have some value here, so we need to check it */
+ if (shingle_values[i] == cur_id) {
+ cur_cnt++;
+ }
+ else {
+ cur_id = shingle_values[i];
+ if (cur_cnt >= max_cnt) {
+ max_cnt = cur_cnt;
+ sel_id = cur_id;
+ }
+ cur_cnt = 0;
+ }
+ }
+
+ if (cur_cnt > max_cnt) {
+ max_cnt = cur_cnt;
+ }
+
+ if (sel_id != -1) {
+ /* We have some id selected here */
+ rep.v1.prob = (float) max_cnt / (float) RSPAMD_SHINGLE_SIZE;
+
+ if (rep.v1.prob > 0.5) {
+ msg_debug_fuzzy_backend(
+ "found fuzzy hash with probability %.2f",
+ rep.v1.prob);
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, FALSE,
+ RSPAMD_FUZZY_BACKEND_GET_DIGEST_BY_ID, sel_id);
+ if (rc == SQLITE_OK) {
+ timestamp = sqlite3_column_int64(
+ prepared_stmts[RSPAMD_FUZZY_BACKEND_GET_DIGEST_BY_ID].stmt,
+ 2);
+ if (time(NULL) - timestamp > expire) {
+ /* Expire element */
+ msg_debug_fuzzy_backend(
+ "requested hash has been expired");
+ rep.v1.prob = 0.0;
+ }
+ else {
+ rep.ts = timestamp;
+ memcpy(rep.digest, sqlite3_column_blob(prepared_stmts[RSPAMD_FUZZY_BACKEND_GET_DIGEST_BY_ID].stmt, 0), sizeof(rep.digest));
+ rep.v1.value = sqlite3_column_int64(
+ prepared_stmts[RSPAMD_FUZZY_BACKEND_GET_DIGEST_BY_ID].stmt,
+ 1);
+ rep.v1.flag = sqlite3_column_int(
+ prepared_stmts[RSPAMD_FUZZY_BACKEND_GET_DIGEST_BY_ID].stmt,
+ 3);
+ }
+ }
+ }
+ else {
+ /* Otherwise we assume that as error */
+ rep.v1.value = 0;
+ }
+
+ rspamd_fuzzy_backend_sqlite_cleanup_stmt(backend,
+ RSPAMD_FUZZY_BACKEND_GET_DIGEST_BY_ID);
+ }
+ }
+
+ rspamd_fuzzy_backend_sqlite_cleanup_stmt(backend, RSPAMD_FUZZY_BACKEND_CHECK);
+ rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_COMMIT);
+
+ return rep;
+}
+
+gboolean
+rspamd_fuzzy_backend_sqlite_prepare_update(struct rspamd_fuzzy_backend_sqlite *backend,
+ const gchar *source)
+{
+ gint rc;
+
+ if (backend == NULL) {
+ return FALSE;
+ }
+
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_START);
+
+ if (rc != SQLITE_OK) {
+ msg_warn_fuzzy_backend("cannot start transaction for updates: %s",
+ sqlite3_errmsg(backend->db));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_fuzzy_backend_sqlite_add(struct rspamd_fuzzy_backend_sqlite *backend,
+ const struct rspamd_fuzzy_cmd *cmd)
+{
+ int rc, i;
+ gint64 id, flag;
+ const struct rspamd_fuzzy_shingle_cmd *shcmd;
+
+ if (backend == NULL) {
+ return FALSE;
+ }
+
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, FALSE,
+ RSPAMD_FUZZY_BACKEND_CHECK,
+ cmd->digest);
+
+ if (rc == SQLITE_OK) {
+ /* Check flag */
+ flag = sqlite3_column_int64(
+ prepared_stmts[RSPAMD_FUZZY_BACKEND_CHECK].stmt,
+ 2);
+ rspamd_fuzzy_backend_sqlite_cleanup_stmt(backend, RSPAMD_FUZZY_BACKEND_CHECK);
+
+ if (flag == cmd->flag) {
+ /* We need to increase weight */
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_UPDATE,
+ (gint64) cmd->value,
+ cmd->digest);
+ if (rc != SQLITE_OK) {
+ msg_warn_fuzzy_backend("cannot update hash to %d -> "
+ "%*xs: %s",
+ (gint) cmd->flag,
+ (gint) sizeof(cmd->digest), cmd->digest,
+ sqlite3_errmsg(backend->db));
+ }
+ }
+ else {
+ /* We need to relearn actually */
+
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_UPDATE_FLAG,
+ (gint64) cmd->value,
+ (gint64) cmd->flag,
+ cmd->digest);
+
+ if (rc != SQLITE_OK) {
+ msg_warn_fuzzy_backend("cannot update hash to %d -> "
+ "%*xs: %s",
+ (gint) cmd->flag,
+ (gint) sizeof(cmd->digest), cmd->digest,
+ sqlite3_errmsg(backend->db));
+ }
+ }
+ }
+ else {
+ rspamd_fuzzy_backend_sqlite_cleanup_stmt(backend, RSPAMD_FUZZY_BACKEND_CHECK);
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, FALSE,
+ RSPAMD_FUZZY_BACKEND_INSERT,
+ (gint) cmd->flag,
+ cmd->digest,
+ (gint64) cmd->value);
+
+ if (rc == SQLITE_OK) {
+ if (cmd->shingles_count > 0) {
+ id = sqlite3_last_insert_rowid(backend->db);
+ shcmd = (const struct rspamd_fuzzy_shingle_cmd *) cmd;
+
+ for (i = 0; i < RSPAMD_SHINGLE_SIZE; i++) {
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_INSERT_SHINGLE,
+ shcmd->sgl.hashes[i], (gint64) i, id);
+ msg_debug_fuzzy_backend("add shingle %d -> %L: %L",
+ i,
+ shcmd->sgl.hashes[i],
+ id);
+
+ if (rc != SQLITE_OK) {
+ msg_warn_fuzzy_backend("cannot add shingle %d -> "
+ "%L: %L: %s",
+ i,
+ shcmd->sgl.hashes[i],
+ id, sqlite3_errmsg(backend->db));
+ }
+ }
+ }
+ }
+ else {
+ msg_warn_fuzzy_backend("cannot add hash to %d -> "
+ "%*xs: %s",
+ (gint) cmd->flag,
+ (gint) sizeof(cmd->digest), cmd->digest,
+ sqlite3_errmsg(backend->db));
+ }
+
+ rspamd_fuzzy_backend_sqlite_cleanup_stmt(backend,
+ RSPAMD_FUZZY_BACKEND_INSERT);
+ }
+
+ return (rc == SQLITE_OK);
+}
+
+gboolean
+rspamd_fuzzy_backend_sqlite_finish_update(struct rspamd_fuzzy_backend_sqlite *backend,
+ const gchar *source, gboolean version_bump)
+{
+ gint rc = SQLITE_OK, wal_frames, wal_checkpointed, ver;
+
+ /* Get and update version */
+ if (version_bump) {
+ ver = rspamd_fuzzy_backend_sqlite_version(backend, source);
+ ++ver;
+
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_SET_VERSION,
+ (gint64) ver, (gint64) time(NULL), source);
+ }
+
+ if (rc == SQLITE_OK) {
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_COMMIT);
+
+ if (rc != SQLITE_OK) {
+ msg_warn_fuzzy_backend("cannot commit updates: %s",
+ sqlite3_errmsg(backend->db));
+ rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_ROLLBACK);
+ return FALSE;
+ }
+ else {
+ if (!rspamd_sqlite3_sync(backend->db, &wal_frames, &wal_checkpointed)) {
+ msg_warn_fuzzy_backend("cannot commit checkpoint: %s",
+ sqlite3_errmsg(backend->db));
+ }
+ else if (wal_checkpointed > 0) {
+ msg_info_fuzzy_backend("total number of frames in the wal file: "
+ "%d, checkpointed: %d",
+ wal_frames, wal_checkpointed);
+ }
+ }
+ }
+ else {
+ msg_warn_fuzzy_backend("cannot update version for %s: %s", source,
+ sqlite3_errmsg(backend->db));
+ rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_ROLLBACK);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_fuzzy_backend_sqlite_del(struct rspamd_fuzzy_backend_sqlite *backend,
+ const struct rspamd_fuzzy_cmd *cmd)
+{
+ int rc = -1;
+
+ if (backend == NULL) {
+ return FALSE;
+ }
+
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, FALSE,
+ RSPAMD_FUZZY_BACKEND_CHECK,
+ cmd->digest);
+
+ if (rc == SQLITE_OK) {
+ rspamd_fuzzy_backend_sqlite_cleanup_stmt(backend, RSPAMD_FUZZY_BACKEND_CHECK);
+
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_DELETE,
+ cmd->digest);
+ if (rc != SQLITE_OK) {
+ msg_warn_fuzzy_backend("cannot update hash to %d -> "
+ "%*xs: %s",
+ (gint) cmd->flag,
+ (gint) sizeof(cmd->digest), cmd->digest,
+ sqlite3_errmsg(backend->db));
+ }
+ }
+ else {
+ /* Hash is missing */
+ rspamd_fuzzy_backend_sqlite_cleanup_stmt(backend, RSPAMD_FUZZY_BACKEND_CHECK);
+ }
+
+ return (rc == SQLITE_OK);
+}
+
+gboolean
+rspamd_fuzzy_backend_sqlite_sync(struct rspamd_fuzzy_backend_sqlite *backend,
+ gint64 expire,
+ gboolean clean_orphaned)
+{
+ struct orphaned_shingle_elt {
+ gint64 value;
+ gint64 number;
+ };
+
+ /* Do not do more than 5k ops per step */
+ const guint64 max_changes = 5000;
+ gboolean ret = FALSE;
+ gint64 expire_lim, expired;
+ gint rc, i, orphaned_cnt = 0;
+ GError *err = NULL;
+ static const gchar orphaned_shingles[] = "SELECT shingles.value,shingles.number "
+ "FROM shingles "
+ "LEFT JOIN digests ON "
+ "shingles.digest_id=digests.id WHERE "
+ "digests.id IS NULL;";
+ sqlite3_stmt *stmt;
+ GArray *orphaned;
+ struct orphaned_shingle_elt orphaned_elt, *pelt;
+
+
+ if (backend == NULL) {
+ return FALSE;
+ }
+
+ /* Perform expire */
+ if (expire > 0) {
+ expire_lim = time(NULL) - expire;
+
+ if (expire_lim > 0) {
+ ret = rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_START);
+
+ if (ret == SQLITE_OK) {
+
+ rc = rspamd_fuzzy_backend_sqlite_run_stmt(backend, FALSE,
+ RSPAMD_FUZZY_BACKEND_EXPIRE, expire_lim, max_changes);
+
+ if (rc == SQLITE_OK) {
+ expired = sqlite3_changes(backend->db);
+
+ if (expired > 0) {
+ backend->expired += expired;
+ msg_info_fuzzy_backend("expired %L hashes", expired);
+ }
+ }
+ else {
+ msg_warn_fuzzy_backend(
+ "cannot execute expired statement: %s",
+ sqlite3_errmsg(backend->db));
+ }
+
+ rspamd_fuzzy_backend_sqlite_cleanup_stmt(backend,
+ RSPAMD_FUZZY_BACKEND_EXPIRE);
+
+ ret = rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_COMMIT);
+
+ if (ret != SQLITE_OK) {
+ rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_ROLLBACK);
+ }
+ }
+ if (ret != SQLITE_OK) {
+ msg_warn_fuzzy_backend("cannot expire db: %s",
+ sqlite3_errmsg(backend->db));
+ }
+ }
+ }
+
+ /* Cleanup database */
+ if (clean_orphaned) {
+ ret = rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_START);
+
+ if (ret == SQLITE_OK) {
+ if ((rc = sqlite3_prepare_v2(backend->db,
+ orphaned_shingles,
+ -1,
+ &stmt,
+ NULL)) != SQLITE_OK) {
+ msg_warn_fuzzy_backend("cannot cleanup shingles: %s",
+ sqlite3_errmsg(backend->db));
+ }
+ else {
+ orphaned = g_array_new(FALSE,
+ FALSE,
+ sizeof(struct orphaned_shingle_elt));
+
+ while (sqlite3_step(stmt) == SQLITE_ROW) {
+ orphaned_elt.value = sqlite3_column_int64(stmt, 0);
+ orphaned_elt.number = sqlite3_column_int64(stmt, 1);
+ g_array_append_val(orphaned, orphaned_elt);
+
+ if (orphaned->len > max_changes) {
+ break;
+ }
+ }
+
+ sqlite3_finalize(stmt);
+ orphaned_cnt = orphaned->len;
+
+ if (orphaned_cnt > 0) {
+ msg_info_fuzzy_backend(
+ "going to delete %ud orphaned shingles",
+ orphaned_cnt);
+ /* Need to delete orphaned elements */
+ for (i = 0; i < (gint) orphaned_cnt; i++) {
+ pelt = &g_array_index(orphaned,
+ struct orphaned_shingle_elt,
+ i);
+ rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_DELETE_ORPHANED,
+ pelt->value, pelt->number);
+ }
+ }
+
+
+ g_array_free(orphaned, TRUE);
+ }
+
+ ret = rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_COMMIT);
+
+ if (ret == SQLITE_OK) {
+ msg_info_fuzzy_backend(
+ "deleted %ud orphaned shingles",
+ orphaned_cnt);
+ }
+ else {
+ msg_warn_fuzzy_backend(
+ "cannot synchronize fuzzy backend: %e",
+ err);
+ rspamd_fuzzy_backend_sqlite_run_stmt(backend, TRUE,
+ RSPAMD_FUZZY_BACKEND_TRANSACTION_ROLLBACK);
+ }
+ }
+ }
+
+ return ret;
+}
+
+
+void rspamd_fuzzy_backend_sqlite_close(struct rspamd_fuzzy_backend_sqlite *backend)
+{
+ if (backend != NULL) {
+ if (backend->db != NULL) {
+ rspamd_fuzzy_backend_sqlite_close_stmts(backend);
+ sqlite3_close(backend->db);
+ }
+
+ if (backend->path != NULL) {
+ g_free(backend->path);
+ }
+
+ if (backend->pool) {
+ rspamd_mempool_delete(backend->pool);
+ }
+
+ g_free(backend);
+ }
+}
+
+
+gsize rspamd_fuzzy_backend_sqlite_count(struct rspamd_fuzzy_backend_sqlite *backend)
+{
+ if (backend) {
+ if (rspamd_fuzzy_backend_sqlite_run_stmt(backend, FALSE,
+ RSPAMD_FUZZY_BACKEND_COUNT) == SQLITE_OK) {
+ backend->count = sqlite3_column_int64(
+ prepared_stmts[RSPAMD_FUZZY_BACKEND_COUNT].stmt, 0);
+ }
+
+ rspamd_fuzzy_backend_sqlite_cleanup_stmt(backend, RSPAMD_FUZZY_BACKEND_COUNT);
+
+ return backend->count;
+ }
+
+ return 0;
+}
+
+gint rspamd_fuzzy_backend_sqlite_version(struct rspamd_fuzzy_backend_sqlite *backend,
+ const gchar *source)
+{
+ gint ret = 0;
+
+ if (backend) {
+ if (rspamd_fuzzy_backend_sqlite_run_stmt(backend, FALSE,
+ RSPAMD_FUZZY_BACKEND_VERSION, source) == SQLITE_OK) {
+ ret = sqlite3_column_int64(
+ prepared_stmts[RSPAMD_FUZZY_BACKEND_VERSION].stmt, 0);
+ }
+
+ rspamd_fuzzy_backend_sqlite_cleanup_stmt(backend, RSPAMD_FUZZY_BACKEND_VERSION);
+ }
+
+ return ret;
+}
+
+gsize rspamd_fuzzy_backend_sqlite_expired(struct rspamd_fuzzy_backend_sqlite *backend)
+{
+ return backend != NULL ? backend->expired : 0;
+}
+
+const gchar *
+rspamd_fuzzy_sqlite_backend_id(struct rspamd_fuzzy_backend_sqlite *backend)
+{
+ return backend != NULL ? backend->id : 0;
+}
diff --git a/src/libserver/fuzzy_backend/fuzzy_backend_sqlite.h b/src/libserver/fuzzy_backend/fuzzy_backend_sqlite.h
new file mode 100644
index 0000000..766f7c9
--- /dev/null
+++ b/src/libserver/fuzzy_backend/fuzzy_backend_sqlite.h
@@ -0,0 +1,107 @@
+/*-
+ * 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 FUZZY_BACKEND_H_
+#define FUZZY_BACKEND_H_
+
+#include "config.h"
+#include "fuzzy_wire.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_fuzzy_backend_sqlite;
+
+/**
+ * Open fuzzy backend
+ * @param path file to open (legacy file will be converted automatically)
+ * @param err error pointer
+ * @return backend structure or NULL
+ */
+struct rspamd_fuzzy_backend_sqlite *rspamd_fuzzy_backend_sqlite_open(const gchar *path,
+ gboolean vacuum,
+ GError **err);
+
+/**
+ * Check specified fuzzy in the backend
+ * @param backend
+ * @param cmd
+ * @return reply with probability and weight
+ */
+struct rspamd_fuzzy_reply rspamd_fuzzy_backend_sqlite_check(
+ struct rspamd_fuzzy_backend_sqlite *backend,
+ const struct rspamd_fuzzy_cmd *cmd,
+ gint64 expire);
+
+/**
+ * Prepare storage for updates (by starting transaction)
+ */
+gboolean rspamd_fuzzy_backend_sqlite_prepare_update(struct rspamd_fuzzy_backend_sqlite *backend,
+ const gchar *source);
+
+/**
+ * Add digest to the database
+ * @param backend
+ * @param cmd
+ * @return
+ */
+gboolean rspamd_fuzzy_backend_sqlite_add(struct rspamd_fuzzy_backend_sqlite *backend,
+ const struct rspamd_fuzzy_cmd *cmd);
+
+/**
+ * Delete digest from the database
+ * @param backend
+ * @param cmd
+ * @return
+ */
+gboolean rspamd_fuzzy_backend_sqlite_del(
+ struct rspamd_fuzzy_backend_sqlite *backend,
+ const struct rspamd_fuzzy_cmd *cmd);
+
+/**
+ * Commit updates to storage
+ */
+gboolean rspamd_fuzzy_backend_sqlite_finish_update(struct rspamd_fuzzy_backend_sqlite *backend,
+ const gchar *source, gboolean version_bump);
+
+/**
+ * Sync storage
+ * @param backend
+ * @return
+ */
+gboolean rspamd_fuzzy_backend_sqlite_sync(struct rspamd_fuzzy_backend_sqlite *backend,
+ gint64 expire,
+ gboolean clean_orphaned);
+
+/**
+ * Close storage
+ * @param backend
+ */
+void rspamd_fuzzy_backend_sqlite_close(struct rspamd_fuzzy_backend_sqlite *backend);
+
+gsize rspamd_fuzzy_backend_sqlite_count(struct rspamd_fuzzy_backend_sqlite *backend);
+
+gint rspamd_fuzzy_backend_sqlite_version(struct rspamd_fuzzy_backend_sqlite *backend, const gchar *source);
+
+gsize rspamd_fuzzy_backend_sqlite_expired(struct rspamd_fuzzy_backend_sqlite *backend);
+
+const gchar *rspamd_fuzzy_sqlite_backend_id(struct rspamd_fuzzy_backend_sqlite *backend);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* FUZZY_BACKEND_H_ */
diff --git a/src/libserver/fuzzy_wire.h b/src/libserver/fuzzy_wire.h
new file mode 100644
index 0000000..c2f93b8
--- /dev/null
+++ b/src/libserver/fuzzy_wire.h
@@ -0,0 +1,154 @@
+/*
+ * 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.
+ */
+
+#ifndef RSPAMD_FUZZY_STORAGE_H
+#define RSPAMD_FUZZY_STORAGE_H
+
+#include "config.h"
+#include "rspamd.h"
+#include "shingles.h"
+#include "cryptobox.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define RSPAMD_FUZZY_VERSION 4
+#define RSPAMD_FUZZY_KEYLEN 8
+
+#define RSPAMD_FUZZY_FLAG_WEAK (1u << 7u)
+/* Use lower 4 bits for the version */
+#define RSPAMD_FUZZY_VERSION_MASK 0x0fu
+/* Commands for fuzzy storage */
+#define FUZZY_CHECK 0
+#define FUZZY_WRITE 1
+#define FUZZY_DEL 2
+#define FUZZY_STAT 3
+#define FUZZY_PING 4
+#define FUZZY_CLIENT_MAX 4
+/* Internal commands */
+#define FUZZY_REFRESH 100 /* Update expire */
+#define FUZZY_DUP 101 /* Skip duplicate in update queue */
+
+/**
+ * The epoch of the fuzzy client
+ */
+enum rspamd_fuzzy_epoch {
+ RSPAMD_FUZZY_EPOCH10, /**< 1.0+ encryption */
+ RSPAMD_FUZZY_EPOCH11, /**< 1.7+ extended reply */
+ RSPAMD_FUZZY_EPOCH_MAX
+};
+
+RSPAMD_PACKED(rspamd_fuzzy_cmd)
+{
+ guint8 version;
+ guint8 cmd;
+ guint8 shingles_count;
+ guint8 flag;
+ gint32 value;
+ guint32 tag;
+ gchar digest[rspamd_cryptobox_HASHBYTES];
+};
+
+RSPAMD_PACKED(rspamd_fuzzy_shingle_cmd)
+{
+ struct rspamd_fuzzy_cmd basic;
+ struct rspamd_shingle sgl;
+};
+
+RSPAMD_PACKED(rspamd_fuzzy_reply_v1)
+{
+ gint32 value;
+ guint32 flag;
+ guint32 tag;
+ float prob;
+};
+
+RSPAMD_PACKED(rspamd_fuzzy_reply)
+{
+ struct rspamd_fuzzy_reply_v1 v1;
+ gchar digest[rspamd_cryptobox_HASHBYTES];
+ guint32 ts;
+ guchar reserved[12];
+};
+
+RSPAMD_PACKED(rspamd_fuzzy_encrypted_req_hdr)
+{
+ guchar magic[4];
+ guchar key_id[RSPAMD_FUZZY_KEYLEN];
+ guchar pubkey[32];
+ guchar nonce[rspamd_cryptobox_MAX_NONCEBYTES];
+ guchar mac[rspamd_cryptobox_MAX_MACBYTES];
+};
+
+RSPAMD_PACKED(rspamd_fuzzy_encrypted_cmd)
+{
+ struct rspamd_fuzzy_encrypted_req_hdr hdr;
+ struct rspamd_fuzzy_cmd cmd;
+};
+
+RSPAMD_PACKED(rspamd_fuzzy_encrypted_shingle_cmd)
+{
+ struct rspamd_fuzzy_encrypted_req_hdr hdr;
+ struct rspamd_fuzzy_shingle_cmd cmd;
+};
+
+RSPAMD_PACKED(rspamd_fuzzy_encrypted_rep_hdr)
+{
+ guchar nonce[rspamd_cryptobox_MAX_NONCEBYTES];
+ guchar mac[rspamd_cryptobox_MAX_MACBYTES];
+};
+
+RSPAMD_PACKED(rspamd_fuzzy_encrypted_reply)
+{
+ struct rspamd_fuzzy_encrypted_rep_hdr hdr;
+ struct rspamd_fuzzy_reply rep;
+};
+
+static const guchar fuzzy_encrypted_magic[4] = {'r', 's', 'f', 'e'};
+
+enum rspamd_fuzzy_extension_type {
+ RSPAMD_FUZZY_EXT_SOURCE_DOMAIN = 'd',
+ RSPAMD_FUZZY_EXT_SOURCE_IP4 = '4',
+ RSPAMD_FUZZY_EXT_SOURCE_IP6 = '6',
+};
+
+struct rspamd_fuzzy_cmd_extension {
+ enum rspamd_fuzzy_extension_type ext;
+ guint length;
+ struct rspamd_fuzzy_cmd_extension *next;
+ guchar *payload;
+};
+
+struct rspamd_fuzzy_stat_entry {
+ const gchar *name;
+ guint64 fuzzy_cnt;
+};
+
+RSPAMD_PACKED(fuzzy_peer_cmd)
+{
+ gint32 is_shingle;
+ union {
+ struct rspamd_fuzzy_cmd normal;
+ struct rspamd_fuzzy_shingle_cmd shingle;
+ } cmd;
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/html/html.cxx b/src/libserver/html/html.cxx
new file mode 100644
index 0000000..5861d45
--- /dev/null
+++ b/src/libserver/html/html.cxx
@@ -0,0 +1,2393 @@
+/*-
+ * Copyright 2021 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 "message.h"
+#include "html.h"
+#include "html_tags.h"
+#include "html_block.hxx"
+#include "html.hxx"
+#include "libserver/css/css_value.hxx"
+#include "libserver/css/css.hxx"
+#include "libserver/task.h"
+#include "libserver/cfg_file.h"
+
+#include "url.h"
+#include "contrib/libucl/khash.h"
+#include "libmime/images.h"
+#include "libutil/cxx/utf8_util.h"
+
+#include "html_tag_defs.hxx"
+#include "html_entities.hxx"
+#include "html_tag.hxx"
+#include "html_url.hxx"
+
+#include <frozen/unordered_map.h>
+#include <frozen/string.h>
+#include <fmt/core.h>
+
+#include <unicode/uversion.h>
+
+namespace rspamd::html {
+
+static const guint max_tags = 8192; /* Ignore tags if this maximum is reached */
+
+static const html_tags_storage html_tags_defs;
+
+auto html_components_map = frozen::make_unordered_map<frozen::string, html_component_type>(
+ {
+ {"name", html_component_type::RSPAMD_HTML_COMPONENT_NAME},
+ {"href", html_component_type::RSPAMD_HTML_COMPONENT_HREF},
+ {"src", html_component_type::RSPAMD_HTML_COMPONENT_HREF},
+ {"action", html_component_type::RSPAMD_HTML_COMPONENT_HREF},
+ {"color", html_component_type::RSPAMD_HTML_COMPONENT_COLOR},
+ {"bgcolor", html_component_type::RSPAMD_HTML_COMPONENT_BGCOLOR},
+ {"style", html_component_type::RSPAMD_HTML_COMPONENT_STYLE},
+ {"class", html_component_type::RSPAMD_HTML_COMPONENT_CLASS},
+ {"width", html_component_type::RSPAMD_HTML_COMPONENT_WIDTH},
+ {"height", html_component_type::RSPAMD_HTML_COMPONENT_HEIGHT},
+ {"size", html_component_type::RSPAMD_HTML_COMPONENT_SIZE},
+ {"rel", html_component_type::RSPAMD_HTML_COMPONENT_REL},
+ {"alt", html_component_type::RSPAMD_HTML_COMPONENT_ALT},
+ {"id", html_component_type::RSPAMD_HTML_COMPONENT_ID},
+ {"hidden", html_component_type::RSPAMD_HTML_COMPONENT_HIDDEN},
+ });
+
+#define msg_debug_html(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_html_log_id, "html", pool->tag.uid, \
+ __FUNCTION__, \
+ __VA_ARGS__)
+
+INIT_LOG_MODULE(html)
+
+/*
+ * This function is expected to be called on a closing tag to fill up all tags
+ * and return the current parent (meaning unclosed) tag
+ */
+static auto
+html_check_balance(struct html_content *hc,
+ struct html_tag *tag,
+ goffset tag_start_offset,
+ goffset tag_end_offset) -> html_tag *
+{
+ /* As agreed, the closing tag has the last opening at the parent ptr */
+ auto *opening_tag = tag->parent;
+
+ auto calculate_content_length = [tag_start_offset, tag_end_offset](html_tag *t) {
+ auto opening_content_offset = t->content_offset;
+
+ if (t->flags & (CM_EMPTY)) {
+ /* Attach closing tag just at the opening tag */
+ t->closing.start = t->tag_start;
+ t->closing.end = t->content_offset;
+ }
+ else {
+
+ if (opening_content_offset <= tag_start_offset) {
+ t->closing.start = tag_start_offset;
+ t->closing.end = tag_end_offset;
+ }
+ else {
+
+ t->closing.start = t->content_offset;
+ t->closing.end = tag_end_offset;
+ }
+ }
+ };
+
+ auto balance_tag = [&]() -> html_tag * {
+ auto it = tag->parent;
+ auto found_pair = false;
+
+ for (; it != nullptr; it = it->parent) {
+ if (it->id == tag->id && !(it->flags & FL_CLOSED)) {
+ found_pair = true;
+ break;
+ }
+ }
+
+ /*
+ * If we have found a closing pair, then we need to close all tags and
+ * return the top-most tag
+ */
+ if (found_pair) {
+ for (it = tag->parent; it != nullptr; it = it->parent) {
+ it->flags |= FL_CLOSED;
+ /* Insert a virtual closing tag for all tags that are not closed */
+ calculate_content_length(it);
+ if (it->id == tag->id && !(it->flags & FL_CLOSED)) {
+ break;
+ }
+ }
+
+ return it;
+ }
+ else {
+ /*
+ * We have not found a pair, so this closing tag is bogus and should
+ * be ignored completely.
+ * Unfortunately, it also means that we need to insert another tag,
+ * as the current closing tag is unusable for that purposes.
+ *
+ * We assume that callee will recognise that and reconstruct the
+ * tag at the tag_end_closing state, so we return nullptr...
+ */
+ }
+
+ /* Tag must be ignored and reconstructed */
+ return nullptr;
+ };
+
+ if (opening_tag) {
+
+ if (opening_tag->id == tag->id) {
+ opening_tag->flags |= FL_CLOSED;
+
+ calculate_content_length(opening_tag);
+ /* All good */
+ return opening_tag->parent;
+ }
+ else {
+ return balance_tag();
+ }
+ }
+ else {
+ /*
+ * We have no opening tag
+ * There are two possibilities:
+ *
+ * 1) We have some block tag in hc->all_tags;
+ * 2) We have no tags
+ */
+
+ if (hc->all_tags.empty()) {
+ hc->all_tags.push_back(std::make_unique<html_tag>());
+ auto *vtag = hc->all_tags.back().get();
+ vtag->id = Tag_HTML;
+ vtag->flags = FL_VIRTUAL;
+ vtag->tag_start = 0;
+ vtag->content_offset = 0;
+ calculate_content_length(vtag);
+
+ if (!hc->root_tag) {
+ hc->root_tag = vtag;
+ }
+ else {
+ vtag->parent = hc->root_tag;
+ }
+
+ tag->parent = vtag;
+
+ /* Recursively call with a virtual <html> tag inserted */
+ return html_check_balance(hc, tag, tag_start_offset, tag_end_offset);
+ }
+ }
+
+ return nullptr;
+}
+
+auto html_component_from_string(const std::string_view &st) -> std::optional<html_component_type>
+{
+ auto known_component_it = html_components_map.find(st);
+
+ if (known_component_it != html_components_map.end()) {
+ return known_component_it->second;
+ }
+ else {
+ return std::nullopt;
+ }
+}
+
+enum tag_parser_state {
+ parse_start = 0,
+ parse_name,
+ parse_attr_name,
+ parse_equal,
+ parse_start_dquote,
+ parse_dqvalue,
+ parse_end_dquote,
+ parse_start_squote,
+ parse_sqvalue,
+ parse_end_squote,
+ parse_value,
+ spaces_before_eq,
+ spaces_after_eq,
+ spaces_after_param,
+ ignore_bad_tag,
+ tag_end,
+ slash_after_value,
+ slash_in_unquoted_value,
+};
+struct tag_content_parser_state {
+ tag_parser_state cur_state = parse_start;
+ std::string buf;
+ std::optional<html_component_type> cur_component;
+
+ void reset()
+ {
+ cur_state = parse_start;
+ buf.clear();
+ cur_component = std::nullopt;
+ }
+};
+
+static inline void
+html_parse_tag_content(rspamd_mempool_t *pool,
+ struct html_content *hc,
+ struct html_tag *tag,
+ const char *in,
+ struct tag_content_parser_state &parser_env)
+{
+ auto state = parser_env.cur_state;
+
+ /*
+ * Stores tag component if it doesn't exist, performing copy of the
+ * value + decoding of the entities
+ * Parser env is set to clear the current html attribute fields (saved_p and
+ * cur_component)
+ */
+ auto store_component_value = [&]() -> void {
+ if (parser_env.cur_component) {
+
+ if (parser_env.buf.empty()) {
+ tag->components.emplace_back(parser_env.cur_component.value(),
+ std::string_view{});
+ }
+ else {
+ /* We need to copy buf to a persistent storage */
+ auto *s = rspamd_mempool_alloc_buffer(pool, parser_env.buf.size());
+
+ if (parser_env.cur_component.value() == html_component_type::RSPAMD_HTML_COMPONENT_ID ||
+ parser_env.cur_component.value() == html_component_type::RSPAMD_HTML_COMPONENT_CLASS) {
+ /* Lowercase */
+ rspamd_str_copy_lc(parser_env.buf.data(), s, parser_env.buf.size());
+ }
+ else {
+ memcpy(s, parser_env.buf.data(), parser_env.buf.size());
+ }
+
+ auto sz = rspamd_html_decode_entitles_inplace(s, parser_env.buf.size());
+ tag->components.emplace_back(parser_env.cur_component.value(),
+ std::string_view{s, sz});
+ }
+ }
+
+ parser_env.buf.clear();
+ parser_env.cur_component = std::nullopt;
+ };
+
+ auto store_component_name = [&]() -> bool {
+ decode_html_entitles_inplace(parser_env.buf);
+ auto known_component_it = html_components_map.find(std::string_view{parser_env.buf});
+ parser_env.buf.clear();
+
+ if (known_component_it != html_components_map.end()) {
+ parser_env.cur_component = known_component_it->second;
+
+ return true;
+ }
+ else {
+ parser_env.cur_component = std::nullopt;
+ }
+
+ return false;
+ };
+
+ auto store_value_character = [&](bool lc) -> void {
+ auto c = lc ? g_ascii_tolower(*in) : *in;
+
+ if (c == '\0') {
+ /* Replace with u0FFD */
+ parser_env.buf.append((const char *) u8"\uFFFD");
+ }
+ else {
+ parser_env.buf.push_back(c);
+ }
+ };
+
+ switch (state) {
+ case parse_start:
+ if (!g_ascii_isalpha(*in) && !g_ascii_isspace(*in)) {
+ hc->flags |= RSPAMD_HTML_FLAG_BAD_ELEMENTS;
+ state = ignore_bad_tag;
+ tag->id = N_TAGS;
+ tag->flags |= FL_BROKEN;
+ }
+ else if (g_ascii_isalpha(*in)) {
+ state = parse_name;
+ store_value_character(true);
+ }
+ break;
+
+ case parse_name:
+ if ((g_ascii_isspace(*in) || *in == '>' || *in == '/')) {
+ if (*in == '/') {
+ tag->flags |= FL_CLOSED;
+ }
+
+ if (parser_env.buf.empty()) {
+ hc->flags |= RSPAMD_HTML_FLAG_BAD_ELEMENTS;
+ tag->id = N_TAGS;
+ tag->flags |= FL_BROKEN;
+ state = ignore_bad_tag;
+ }
+ else {
+ decode_html_entitles_inplace(parser_env.buf);
+ const auto *tag_def = rspamd::html::html_tags_defs.by_name(parser_env.buf);
+
+ if (tag_def == nullptr) {
+ hc->flags |= RSPAMD_HTML_FLAG_UNKNOWN_ELEMENTS;
+ /* Assign -hash to match closing tag if needed */
+ auto nhash = static_cast<std::int32_t>(std::hash<std::string>{}(parser_env.buf));
+ /* Always negative */
+ tag->id = static_cast<tag_id_t>(nhash | G_MININT32);
+ }
+ else {
+ tag->id = tag_def->id;
+ tag->flags = tag_def->flags;
+ }
+
+ parser_env.buf.clear();
+
+ state = spaces_after_param;
+ }
+ }
+ else {
+ store_value_character(true);
+ }
+ break;
+
+ case parse_attr_name:
+ if (*in == '=') {
+ if (!parser_env.buf.empty()) {
+ store_component_name();
+ }
+ state = parse_equal;
+ }
+ else if (g_ascii_isspace(*in)) {
+ store_component_name();
+ state = spaces_before_eq;
+ }
+ else if (*in == '/') {
+ store_component_name();
+ store_component_value();
+ state = slash_after_value;
+ }
+ else if (*in == '>') {
+ store_component_name();
+ store_component_value();
+ state = tag_end;
+ }
+ else {
+ if (*in == '"' || *in == '\'' || *in == '<') {
+ /* Should never be in attribute names but ignored */
+ tag->flags |= FL_BROKEN;
+ }
+
+ store_value_character(true);
+ }
+
+ break;
+
+ case spaces_before_eq:
+ if (*in == '=') {
+ state = parse_equal;
+ }
+ else if (!g_ascii_isspace(*in)) {
+ /*
+ * HTML defines that crap could still be restored and
+ * calculated somehow... So we have to follow this stupid behaviour
+ */
+ /*
+ * TODO: estimate what insane things do email clients in each case
+ */
+ if (*in == '>') {
+ /*
+ * Attribute name followed by end of tag
+ * Should be okay (empty attribute). The rest is handled outside
+ * this automata.
+ */
+ store_component_value();
+ state = tag_end;
+ }
+ else if (*in == '"' || *in == '\'' || *in == '<') {
+ /* Attribute followed by quote... Missing '=' ? Dunno, need to test */
+ hc->flags |= RSPAMD_HTML_FLAG_BAD_ELEMENTS;
+ tag->flags |= FL_BROKEN;
+ store_component_value();
+ store_value_character(true);
+ state = spaces_after_param;
+ }
+ else {
+ /* Empty attribute */
+ store_component_value();
+ store_value_character(true);
+ state = spaces_after_param;
+ }
+ }
+ break;
+
+ case spaces_after_eq:
+ if (*in == '"') {
+ state = parse_start_dquote;
+ }
+ else if (*in == '\'') {
+ state = parse_start_squote;
+ }
+ else if (!g_ascii_isspace(*in)) {
+ store_value_character(true);
+ state = parse_value;
+ }
+ break;
+
+ case parse_equal:
+ if (g_ascii_isspace(*in)) {
+ state = spaces_after_eq;
+ }
+ else if (*in == '"') {
+ state = parse_start_dquote;
+ }
+ else if (*in == '\'') {
+ state = parse_start_squote;
+ }
+ else {
+ store_value_character(true);
+ state = parse_value;
+ }
+ break;
+
+ case parse_start_dquote:
+ if (*in == '"') {
+ state = spaces_after_param;
+ }
+ else {
+ store_value_character(false);
+ state = parse_dqvalue;
+ }
+ break;
+
+ case parse_start_squote:
+ if (*in == '\'') {
+ state = spaces_after_param;
+ }
+ else {
+ store_value_character(false);
+ state = parse_sqvalue;
+ }
+ break;
+
+ case parse_dqvalue:
+ if (*in == '"') {
+ store_component_value();
+ state = parse_end_dquote;
+ }
+ else {
+ store_value_character(false);
+ }
+ break;
+
+ case parse_sqvalue:
+ if (*in == '\'') {
+ store_component_value();
+ state = parse_end_squote;
+ }
+ else {
+ store_value_character(false);
+ }
+
+ break;
+
+ case parse_value:
+ if (*in == '/') {
+ state = slash_in_unquoted_value;
+ }
+ else if (g_ascii_isspace(*in) || *in == '>' || *in == '"') {
+ store_component_value();
+ state = spaces_after_param;
+ }
+ else {
+ store_value_character(false);
+ }
+ break;
+
+ case parse_end_dquote:
+ case parse_end_squote:
+ if (g_ascii_isspace(*in)) {
+ state = spaces_after_param;
+ }
+ else if (*in == '/') {
+ store_component_value();
+ store_value_character(true);
+ state = slash_after_value;
+ }
+ else {
+ /* No space, proceed immediately to the attribute name */
+ state = parse_attr_name;
+ store_component_value();
+ store_value_character(true);
+ }
+ break;
+
+ case spaces_after_param:
+ if (!g_ascii_isspace(*in)) {
+ if (*in == '/') {
+ state = slash_after_value;
+ }
+ else if (*in == '=') {
+ /* Attributes cannot start with '=' */
+ tag->flags |= FL_BROKEN;
+ store_value_character(true);
+ state = parse_attr_name;
+ }
+ else {
+ store_value_character(true);
+ state = parse_attr_name;
+ }
+ }
+ break;
+ case slash_after_value:
+ if (*in == '>') {
+ tag->flags |= FL_CLOSED;
+ state = tag_end;
+ }
+ else if (!g_ascii_isspace(*in)) {
+ tag->flags |= FL_BROKEN;
+ state = parse_attr_name;
+ }
+ break;
+ case slash_in_unquoted_value:
+ if (*in == '>') {
+ /* That slash was in fact closing tag slash, woohoo */
+ tag->flags |= FL_CLOSED;
+ state = tag_end;
+ store_component_value();
+ }
+ else {
+ /* Welcome to the world of html, revert state and save missing / */
+ parser_env.buf.push_back('/');
+ store_value_character(false);
+ state = parse_value;
+ }
+ break;
+ case ignore_bad_tag:
+ case tag_end:
+ break;
+ }
+
+ parser_env.cur_state = state;
+}
+
+static inline auto
+html_is_absolute_url(std::string_view st) -> bool
+{
+ auto alnum_pos = std::find_if(std::begin(st), std::end(st),
+ [](auto c) { return !g_ascii_isalnum(c); });
+
+ if (alnum_pos != std::end(st) && alnum_pos != std::begin(st)) {
+ if (*alnum_pos == ':') {
+ if (st.substr(0, std::distance(std::begin(st), alnum_pos)) == "mailto") {
+ return true;
+ }
+
+ std::advance(alnum_pos, 1);
+ if (alnum_pos != std::end(st)) {
+ /* Include even malformed urls */
+ if (*alnum_pos == '/' || *alnum_pos == '\\') {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+static auto
+html_process_url_tag(rspamd_mempool_t *pool,
+ struct html_tag *tag,
+ struct html_content *hc) -> std::optional<struct rspamd_url *>
+{
+ auto found_href_maybe = tag->find_component(html_component_type::RSPAMD_HTML_COMPONENT_HREF);
+
+ if (found_href_maybe) {
+ /* Check base url */
+ auto &href_value = found_href_maybe.value();
+
+ if (hc && hc->base_url) {
+ /*
+ * Relative url cannot start from the following:
+ * schema://
+ * data:
+ * slash
+ */
+
+ if (!html_is_absolute_url(href_value)) {
+
+ if (href_value.size() >= sizeof("data:") &&
+ g_ascii_strncasecmp(href_value.data(), "data:", sizeof("data:") - 1) == 0) {
+ /* Image data url, never insert as url */
+ return std::nullopt;
+ }
+
+ /* Assume relative url */
+ auto need_slash = false;
+
+ auto orig_len = href_value.size();
+ auto len = orig_len + hc->base_url->urllen;
+
+ if (hc->base_url->datalen == 0) {
+ need_slash = true;
+ len++;
+ }
+
+ auto *buf = rspamd_mempool_alloc_buffer(pool, len + 1);
+ auto nlen = (std::size_t) rspamd_snprintf(buf, len + 1,
+ "%*s%s%*s",
+ (int) hc->base_url->urllen, hc->base_url->string,
+ need_slash ? "/" : "",
+ (gint) orig_len, href_value.data());
+ href_value = {buf, nlen};
+ }
+ else if (href_value.size() > 2 && href_value[0] == '/' && href_value[1] != '/') {
+ /* Relative to the hostname */
+ auto orig_len = href_value.size();
+ auto len = orig_len + hc->base_url->hostlen + hc->base_url->protocollen +
+ 3 /* for :// */;
+ auto *buf = rspamd_mempool_alloc_buffer(pool, len + 1);
+ auto nlen = (std::size_t) rspamd_snprintf(buf, len + 1, "%*s://%*s/%*s",
+ (int) hc->base_url->protocollen, hc->base_url->string,
+ (int) hc->base_url->hostlen, rspamd_url_host_unsafe(hc->base_url),
+ (gint) orig_len, href_value.data());
+ href_value = {buf, nlen};
+ }
+ }
+
+ auto url = html_process_url(pool, href_value).value_or(nullptr);
+
+ if (url) {
+ if (tag->id != Tag_A) {
+ /* Mark special tags special */
+ url->flags |= RSPAMD_URL_FLAG_SPECIAL;
+ }
+
+ if (std::holds_alternative<std::monostate>(tag->extra)) {
+ tag->extra = url;
+ }
+
+ return url;
+ }
+
+ return std::nullopt;
+ }
+
+ return std::nullopt;
+}
+
+struct rspamd_html_url_query_cbd {
+ rspamd_mempool_t *pool;
+ khash_t(rspamd_url_hash) * url_set;
+ struct rspamd_url *url;
+ GPtrArray *part_urls;
+};
+
+static gboolean
+html_url_query_callback(struct rspamd_url *url, gsize start_offset,
+ gsize end_offset, gpointer ud)
+{
+ struct rspamd_html_url_query_cbd *cbd =
+ (struct rspamd_html_url_query_cbd *) ud;
+ rspamd_mempool_t *pool;
+
+ pool = cbd->pool;
+
+ if (url->protocol == PROTOCOL_MAILTO) {
+ if (url->userlen == 0) {
+ return FALSE;
+ }
+ }
+
+ msg_debug_html("found url %s in query of url"
+ " %*s",
+ url->string,
+ cbd->url->querylen, rspamd_url_query_unsafe(cbd->url));
+
+ url->flags |= RSPAMD_URL_FLAG_QUERY;
+
+ if (rspamd_url_set_add_or_increase(cbd->url_set, url, false) && cbd->part_urls) {
+ g_ptr_array_add(cbd->part_urls, url);
+ }
+
+ return TRUE;
+}
+
+static void
+html_process_query_url(rspamd_mempool_t *pool, struct rspamd_url *url,
+ khash_t(rspamd_url_hash) * url_set,
+ GPtrArray *part_urls)
+{
+ if (url->querylen > 0) {
+ struct rspamd_html_url_query_cbd qcbd;
+
+ qcbd.pool = pool;
+ qcbd.url_set = url_set;
+ qcbd.url = url;
+ qcbd.part_urls = part_urls;
+
+ rspamd_url_find_multiple(pool,
+ rspamd_url_query_unsafe(url), url->querylen,
+ RSPAMD_URL_FIND_ALL, NULL,
+ html_url_query_callback, &qcbd);
+ }
+
+ if (part_urls) {
+ g_ptr_array_add(part_urls, url);
+ }
+}
+
+static auto
+html_process_data_image(rspamd_mempool_t *pool,
+ struct html_image *img,
+ std::string_view input) -> void
+{
+ /*
+ * Here, we do very basic processing of the data:
+ * detect if we have something like: `data:image/xxx;base64,yyyzzz==`
+ * We only parse base64 encoded data.
+ * We ignore content type so far
+ */
+ struct rspamd_image *parsed_image;
+ const gchar *semicolon_pos = input.data(),
+ *end = input.data() + input.size();
+
+ if ((semicolon_pos = (const gchar *) memchr(semicolon_pos, ';', end - semicolon_pos)) != NULL) {
+ if (end - semicolon_pos > sizeof("base64,")) {
+ if (memcmp(semicolon_pos + 1, "base64,", sizeof("base64,") - 1) == 0) {
+ const gchar *data_pos = semicolon_pos + sizeof("base64,");
+ gchar *decoded;
+ gsize encoded_len = end - data_pos, decoded_len;
+ rspamd_ftok_t inp;
+
+ decoded_len = (encoded_len / 4 * 3) + 12;
+ decoded = rspamd_mempool_alloc_buffer(pool, decoded_len);
+ rspamd_cryptobox_base64_decode(data_pos, encoded_len,
+ reinterpret_cast<guchar *>(decoded), &decoded_len);
+ inp.begin = decoded;
+ inp.len = decoded_len;
+
+ parsed_image = rspamd_maybe_process_image(pool, &inp);
+
+ if (parsed_image) {
+ msg_debug_html("detected %s image of size %ud x %ud in data url",
+ rspamd_image_type_str(parsed_image->type),
+ parsed_image->width, parsed_image->height);
+ img->embedded_image = parsed_image;
+ }
+ }
+ }
+ else {
+ /* Nothing useful */
+ return;
+ }
+ }
+}
+
+static void
+html_process_img_tag(rspamd_mempool_t *pool,
+ struct html_tag *tag,
+ struct html_content *hc,
+ khash_t(rspamd_url_hash) * url_set,
+ GPtrArray *part_urls)
+{
+ struct html_image *img;
+
+ img = rspamd_mempool_alloc0_type(pool, struct html_image);
+ img->tag = tag;
+
+ for (const auto &param: tag->components) {
+
+ if (param.type == html_component_type::RSPAMD_HTML_COMPONENT_HREF) {
+ /* Check base url */
+ const auto &href_value = param.value;
+
+ if (href_value.size() > 0) {
+ rspamd_ftok_t fstr;
+ fstr.begin = href_value.data();
+ fstr.len = href_value.size();
+ img->src = rspamd_mempool_ftokdup(pool, &fstr);
+
+ if (href_value.size() > sizeof("cid:") - 1 && memcmp(href_value.data(),
+ "cid:", sizeof("cid:") - 1) == 0) {
+ /* We have an embedded image */
+ img->src += sizeof("cid:") - 1;
+ img->flags |= RSPAMD_HTML_FLAG_IMAGE_EMBEDDED;
+ }
+ else {
+ if (href_value.size() > sizeof("data:") - 1 && memcmp(href_value.data(),
+ "data:", sizeof("data:") - 1) == 0) {
+ /* We have an embedded image in HTML tag */
+ img->flags |=
+ (RSPAMD_HTML_FLAG_IMAGE_EMBEDDED | RSPAMD_HTML_FLAG_IMAGE_DATA);
+ html_process_data_image(pool, img, href_value);
+ hc->flags |= RSPAMD_HTML_FLAG_HAS_DATA_URLS;
+ }
+ else {
+ img->flags |= RSPAMD_HTML_FLAG_IMAGE_EXTERNAL;
+ if (img->src) {
+
+ std::string_view cpy{href_value};
+ auto maybe_url = html_process_url(pool, cpy);
+
+ if (maybe_url) {
+ img->url = maybe_url.value();
+ struct rspamd_url *existing;
+
+ img->url->flags |= RSPAMD_URL_FLAG_IMAGE;
+ existing = rspamd_url_set_add_or_return(url_set,
+ img->url);
+
+ if (existing && existing != img->url) {
+ /*
+ * We have some other URL that could be
+ * found, e.g. from another part. However,
+ * we still want to set an image flag on it
+ */
+ existing->flags |= img->url->flags;
+ existing->count++;
+ }
+ else if (part_urls) {
+ /* New url */
+ g_ptr_array_add(part_urls, img->url);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ if (param.type == html_component_type::RSPAMD_HTML_COMPONENT_HEIGHT) {
+ unsigned long val;
+
+ rspamd_strtoul(param.value.data(), param.value.size(), &val);
+ img->height = val;
+ }
+
+ if (param.type == html_component_type::RSPAMD_HTML_COMPONENT_WIDTH) {
+ unsigned long val;
+
+ rspamd_strtoul(param.value.data(), param.value.size(), &val);
+ img->width = val;
+ }
+
+ /* TODO: rework to css at some time */
+ if (param.type == html_component_type::RSPAMD_HTML_COMPONENT_STYLE) {
+ if (img->height == 0) {
+ auto style_st = param.value;
+ auto pos = rspamd_substring_search_caseless(style_st.data(),
+ style_st.size(),
+ "height", sizeof("height") - 1);
+ if (pos != -1) {
+ auto substr = style_st.substr(pos + sizeof("height") - 1);
+
+ for (auto i = 0; i < substr.size(); i++) {
+ auto t = substr[i];
+ if (g_ascii_isdigit(t)) {
+ unsigned long val;
+ rspamd_strtoul(substr.data(),
+ substr.size(), &val);
+ img->height = val;
+ break;
+ }
+ else if (!g_ascii_isspace(t) && t != '=' && t != ':') {
+ /* Fallback */
+ break;
+ }
+ }
+ }
+ }
+ if (img->width == 0) {
+ auto style_st = param.value;
+ auto pos = rspamd_substring_search_caseless(style_st.data(),
+ style_st.size(),
+ "width", sizeof("width") - 1);
+ if (pos != -1) {
+ auto substr = style_st.substr(pos + sizeof("width") - 1);
+
+ for (auto i = 0; i < substr.size(); i++) {
+ auto t = substr[i];
+ if (g_ascii_isdigit(t)) {
+ unsigned long val;
+ rspamd_strtoul(substr.data(),
+ substr.size(), &val);
+ img->width = val;
+ break;
+ }
+ else if (!g_ascii_isspace(t) && t != '=' && t != ':') {
+ /* Fallback */
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (img->embedded_image) {
+ if (img->height == 0) {
+ img->height = img->embedded_image->height;
+ }
+ if (img->width == 0) {
+ img->width = img->embedded_image->width;
+ }
+ }
+
+ hc->images.push_back(img);
+
+ if (std::holds_alternative<std::monostate>(tag->extra)) {
+ tag->extra = img;
+ }
+}
+
+static auto
+html_process_link_tag(rspamd_mempool_t *pool, struct html_tag *tag,
+ struct html_content *hc,
+ khash_t(rspamd_url_hash) * url_set,
+ GPtrArray *part_urls) -> void
+{
+ auto found_rel_maybe = tag->find_component(html_component_type::RSPAMD_HTML_COMPONENT_REL);
+
+ if (found_rel_maybe) {
+ if (found_rel_maybe.value() == "icon") {
+ html_process_img_tag(pool, tag, hc, url_set, part_urls);
+ }
+ }
+}
+
+static auto
+html_process_block_tag(rspamd_mempool_t *pool, struct html_tag *tag,
+ struct html_content *hc) -> void
+{
+ std::optional<css::css_value> maybe_fgcolor, maybe_bgcolor;
+ bool hidden = false;
+
+ for (const auto &param: tag->components) {
+ if (param.type == html_component_type::RSPAMD_HTML_COMPONENT_COLOR) {
+ maybe_fgcolor = css::css_value::maybe_color_from_string(param.value);
+ }
+
+ if (param.type == html_component_type::RSPAMD_HTML_COMPONENT_BGCOLOR) {
+ maybe_bgcolor = css::css_value::maybe_color_from_string(param.value);
+ }
+
+ if (param.type == html_component_type::RSPAMD_HTML_COMPONENT_STYLE) {
+ tag->block = rspamd::css::parse_css_declaration(pool, param.value);
+ }
+
+ if (param.type == html_component_type::RSPAMD_HTML_COMPONENT_HIDDEN) {
+ hidden = true;
+ }
+ }
+
+ if (!tag->block) {
+ tag->block = html_block::undefined_html_block_pool(pool);
+ }
+
+ if (hidden) {
+ tag->block->set_display(false);
+ }
+
+ if (maybe_fgcolor) {
+ tag->block->set_fgcolor(maybe_fgcolor->to_color().value());
+ }
+
+ if (maybe_bgcolor) {
+ tag->block->set_bgcolor(maybe_bgcolor->to_color().value());
+ }
+}
+
+static inline auto
+html_append_parsed(struct html_content *hc,
+ std::string_view data,
+ bool transparent,
+ std::size_t input_len,
+ std::string &dest) -> std::size_t
+{
+ auto cur_offset = dest.size();
+
+ if (dest.size() > input_len) {
+ /* Impossible case, refuse to append */
+ return 0;
+ }
+
+ if (data.size() > 0) {
+ /* Handle multiple spaces at the begin */
+
+ if (cur_offset > 0) {
+ auto last = dest.back();
+ if (!g_ascii_isspace(last) && g_ascii_isspace(data.front())) {
+ dest.append(" ");
+ data = {data.data() + 1, data.size() - 1};
+ cur_offset++;
+ }
+ }
+
+ if (data.find('\0') != std::string_view::npos) {
+ auto replace_zero_func = [](const auto &input, auto &output) {
+ const auto last = input.cend();
+ for (auto it = input.cbegin(); it != last; ++it) {
+ if (*it == '\0') {
+ output.append((const char *) u8"\uFFFD");
+ }
+ else {
+ output.push_back(*it);
+ }
+ }
+ };
+
+ dest.reserve(dest.size() + data.size() + sizeof(u8"\uFFFD"));
+ replace_zero_func(data, dest);
+ hc->flags |= RSPAMD_HTML_FLAG_HAS_ZEROS;
+ }
+ else {
+ dest.append(data);
+ }
+ }
+
+ auto nlen = decode_html_entitles_inplace(dest.data() + cur_offset,
+ dest.size() - cur_offset, true);
+
+ dest.resize(nlen + cur_offset);
+
+ if (transparent) {
+ /* Replace all visible characters with spaces */
+ auto start = std::next(dest.begin(), cur_offset);
+ std::replace_if(
+ start, std::end(dest), [](const auto c) {
+ return !g_ascii_isspace(c);
+ },
+ ' ');
+ }
+
+ return nlen;
+}
+
+static auto
+html_process_displayed_href_tag(rspamd_mempool_t *pool,
+ struct html_content *hc,
+ std::string_view data,
+ const struct html_tag *cur_tag,
+ GList **exceptions,
+ khash_t(rspamd_url_hash) * url_set,
+ goffset dest_offset) -> void
+{
+
+ if (std::holds_alternative<rspamd_url *>(cur_tag->extra)) {
+ auto *url = std::get<rspamd_url *>(cur_tag->extra);
+
+ html_check_displayed_url(pool,
+ exceptions, url_set,
+ data,
+ dest_offset,
+ url);
+ }
+}
+
+static auto
+html_append_tag_content(rspamd_mempool_t *pool,
+ const gchar *start, gsize len,
+ struct html_content *hc,
+ html_tag *tag,
+ GList **exceptions,
+ khash_t(rspamd_url_hash) * url_set) -> goffset
+{
+ auto is_visible = true, is_block = false, is_spaces = false, is_transparent = false;
+ goffset next_tag_offset = tag->closing.end,
+ initial_parsed_offset = hc->parsed.size(),
+ initial_invisible_offset = hc->invisible.size();
+
+ auto calculate_final_tag_offsets = [&]() -> void {
+ if (is_visible) {
+ tag->content_offset = initial_parsed_offset;
+ tag->closing.start = hc->parsed.size();
+ }
+ else {
+ tag->content_offset = initial_invisible_offset;
+ tag->closing.start = hc->invisible.size();
+ }
+ };
+
+ if (tag->closing.end == -1) {
+ if (tag->closing.start != -1) {
+ next_tag_offset = tag->closing.start;
+ tag->closing.end = tag->closing.start;
+ }
+ else {
+ next_tag_offset = tag->content_offset;
+ tag->closing.end = tag->content_offset;
+ }
+ }
+ if (tag->closing.start == -1) {
+ tag->closing.start = tag->closing.end;
+ }
+
+ auto append_margin = [&](char c) -> void {
+ /* We do care about visible margins only */
+ if (is_visible) {
+ if (!hc->parsed.empty() && hc->parsed.back() != c && hc->parsed.back() != '\n') {
+ if (hc->parsed.back() == ' ') {
+ /* We also strip extra spaces at the end, but limiting the start */
+ auto last = std::make_reverse_iterator(hc->parsed.begin() + initial_parsed_offset);
+ auto first = std::find_if(hc->parsed.rbegin(), last,
+ [](auto ch) -> auto {
+ return ch != ' ';
+ });
+ hc->parsed.erase(first.base(), hc->parsed.end());
+ g_assert(hc->parsed.size() >= initial_parsed_offset);
+ }
+ hc->parsed.push_back(c);
+ }
+ }
+ };
+
+ if (tag->id == Tag_BR || tag->id == Tag_HR) {
+
+ if (!(tag->flags & FL_IGNORE)) {
+ hc->parsed.append("\n");
+ }
+
+ auto ret = tag->content_offset;
+ calculate_final_tag_offsets();
+
+ return ret;
+ }
+ else if ((tag->id == Tag_HEAD && (tag->flags & FL_IGNORE)) || (tag->flags & CM_HEAD)) {
+ auto ret = tag->closing.end;
+ calculate_final_tag_offsets();
+
+ return ret;
+ }
+
+ if ((tag->flags & (FL_COMMENT | FL_XML | FL_IGNORE | CM_HEAD))) {
+ is_visible = false;
+ }
+ else {
+ if (!tag->block) {
+ is_visible = true;
+ }
+ else if (!tag->block->is_visible()) {
+ if (!tag->block->is_transparent()) {
+ is_visible = false;
+ }
+ else {
+ if (tag->block->has_display() &&
+ tag->block->display == css::css_display_value::DISPLAY_HIDDEN) {
+ is_visible = false;
+ }
+ else {
+ is_transparent = true;
+ }
+ }
+ }
+ else {
+ if (tag->block->display == css::css_display_value::DISPLAY_BLOCK) {
+ is_block = true;
+ }
+ else if (tag->block->display == css::css_display_value::DISPLAY_TABLE_ROW) {
+ is_spaces = true;
+ }
+ }
+ }
+
+ if (is_block) {
+ append_margin('\n');
+ }
+ else if (is_spaces) {
+ append_margin(' ');
+ }
+
+ goffset cur_offset = tag->content_offset;
+
+ for (auto *cld: tag->children) {
+ auto enclosed_start = cld->tag_start;
+ goffset initial_part_len = enclosed_start - cur_offset;
+
+ if (initial_part_len > 0) {
+ if (is_visible) {
+ html_append_parsed(hc,
+ {start + cur_offset, std::size_t(initial_part_len)},
+ is_transparent, len, hc->parsed);
+ }
+ else {
+ html_append_parsed(hc,
+ {start + cur_offset, std::size_t(initial_part_len)},
+ is_transparent, len, hc->invisible);
+ }
+ }
+
+ auto next_offset = html_append_tag_content(pool, start, len,
+ hc, cld, exceptions, url_set);
+
+ /* Do not allow shifting back */
+ if (next_offset > cur_offset) {
+ cur_offset = next_offset;
+ }
+ }
+
+ if (cur_offset < tag->closing.start) {
+ goffset final_part_len = tag->closing.start - cur_offset;
+
+ if (final_part_len > 0) {
+ if (is_visible) {
+ html_append_parsed(hc,
+ {start + cur_offset, std::size_t(final_part_len)},
+ is_transparent,
+ len,
+ hc->parsed);
+ }
+ else {
+ html_append_parsed(hc,
+ {start + cur_offset, std::size_t(final_part_len)},
+ is_transparent,
+ len,
+ hc->invisible);
+ }
+ }
+ }
+ if (is_block) {
+ append_margin('\n');
+ }
+ else if (is_spaces) {
+ append_margin(' ');
+ }
+
+ if (is_visible) {
+ if (tag->id == Tag_A) {
+ auto written_len = hc->parsed.size() - initial_parsed_offset;
+ html_process_displayed_href_tag(pool, hc,
+ {hc->parsed.data() + initial_parsed_offset, std::size_t(written_len)},
+ tag, exceptions,
+ url_set, initial_parsed_offset);
+ }
+ else if (tag->id == Tag_IMG) {
+ /* Process ALT if presented */
+ auto maybe_alt = tag->find_component(html_component_type::RSPAMD_HTML_COMPONENT_ALT);
+
+ if (maybe_alt) {
+ if (!hc->parsed.empty() && !g_ascii_isspace(hc->parsed.back())) {
+ /* Add a space */
+ hc->parsed += ' ';
+ }
+
+ hc->parsed.append(maybe_alt.value());
+
+ if (!hc->parsed.empty() && !g_ascii_isspace(hc->parsed.back())) {
+ /* Add a space */
+ hc->parsed += ' ';
+ }
+ }
+ }
+ }
+ else {
+ /* Invisible stuff */
+ if (std::holds_alternative<rspamd_url *>(tag->extra)) {
+ auto *url_enclosed = std::get<rspamd_url *>(tag->extra);
+
+ /*
+ * TODO: when hash is fixed to include flags we need to remove and add
+ * url to the hash set
+ */
+ if (url_enclosed) {
+ url_enclosed->flags |= RSPAMD_URL_FLAG_INVISIBLE;
+ }
+ }
+ }
+
+ calculate_final_tag_offsets();
+
+ return next_tag_offset;
+}
+
+auto html_process_input(struct rspamd_task *task,
+ GByteArray *in,
+ GList **exceptions,
+ khash_t(rspamd_url_hash) * url_set,
+ GPtrArray *part_urls,
+ bool allow_css,
+ std::uint16_t *cur_url_order) -> html_content *
+{
+ const gchar *p, *c, *end, *start;
+ guchar t;
+ auto closing = false;
+ guint obrace = 0, ebrace = 0;
+ struct rspamd_url *url = nullptr;
+ gint href_offset = -1;
+ auto overflow_input = false;
+ struct html_tag *cur_tag = nullptr, *parent_tag = nullptr, cur_closing_tag;
+ struct tag_content_parser_state content_parser_env;
+ auto process_size = in->len;
+
+
+ enum {
+ parse_start = 0,
+ content_before_start,
+ tag_begin,
+ sgml_tag,
+ xml_tag,
+ compound_tag,
+ comment_tag,
+ comment_content,
+ sgml_content,
+ tag_content,
+ tag_end_opening,
+ tag_end_closing,
+ html_text_content,
+ xml_tag_end,
+ tag_raw_text,
+ tag_raw_text_less_than,
+ tags_limit_overflow,
+ } state = parse_start;
+
+ enum class html_document_state {
+ doctype,
+ head,
+ body
+ } html_document_state = html_document_state::doctype;
+
+ g_assert(in != NULL);
+ g_assert(task != NULL);
+
+ auto *pool = task->task_pool;
+ auto cur_url_part_order = 0u;
+
+ auto *hc = new html_content;
+ rspamd_mempool_add_destructor(task->task_pool, html_content::html_content_dtor, hc);
+
+ if (task->cfg && in->len > task->cfg->max_html_len) {
+ msg_notice_task("html input is too big: %z, limit is %z",
+ in->len,
+ task->cfg->max_html_len);
+ process_size = task->cfg->max_html_len;
+ overflow_input = true;
+ }
+
+ auto new_tag = [&](int flags = 0) -> struct html_tag *
+ {
+
+ if (hc->all_tags.size() > rspamd::html::max_tags) {
+ hc->flags |= RSPAMD_HTML_FLAG_TOO_MANY_TAGS;
+
+ return nullptr;
+ }
+
+ hc->all_tags.emplace_back(std::make_unique<html_tag>());
+ auto *ntag = hc->all_tags.back().get();
+ ntag->tag_start = c - start;
+ ntag->flags = flags;
+
+ if (cur_tag && !(cur_tag->flags & (CM_EMPTY | FL_CLOSED)) && cur_tag != &cur_closing_tag) {
+ parent_tag = cur_tag;
+ }
+
+ if (flags & FL_XML) {
+ return ntag;
+ }
+
+ return ntag;
+ };
+
+ auto process_opening_tag = [&]() {
+ if (cur_tag->id > Tag_UNKNOWN) {
+ if (cur_tag->flags & CM_UNIQUE) {
+ if (!hc->tags_seen[cur_tag->id]) {
+ /* Duplicate tag has been found */
+ hc->flags |= RSPAMD_HTML_FLAG_DUPLICATE_ELEMENTS;
+ }
+ }
+ hc->tags_seen[cur_tag->id] = true;
+ }
+
+ /* Shift to the first unclosed tag */
+ auto *pt = parent_tag;
+ while (pt && (pt->flags & FL_CLOSED)) {
+ pt = pt->parent;
+ }
+
+ if (pt) {
+ g_assert(cur_tag != pt);
+ cur_tag->parent = pt;
+ g_assert(cur_tag->parent != &cur_closing_tag);
+ parent_tag = pt;
+ parent_tag->children.push_back(cur_tag);
+ }
+ else {
+ if (hc->root_tag) {
+ if (cur_tag != hc->root_tag) {
+ cur_tag->parent = hc->root_tag;
+ g_assert(cur_tag->parent != cur_tag);
+ hc->root_tag->children.push_back(cur_tag);
+ parent_tag = hc->root_tag;
+ }
+ }
+ else {
+ if (cur_tag->id == Tag_HTML) {
+ hc->root_tag = cur_tag;
+ }
+ else {
+ /* Insert a fake html tag */
+ hc->all_tags.emplace_back(std::make_unique<html_tag>());
+ auto *top_tag = hc->all_tags.back().get();
+ top_tag->tag_start = 0;
+ top_tag->flags = FL_VIRTUAL;
+ top_tag->id = Tag_HTML;
+ top_tag->content_offset = 0;
+ top_tag->children.push_back(cur_tag);
+ cur_tag->parent = top_tag;
+ g_assert(cur_tag->parent != cur_tag);
+ hc->root_tag = top_tag;
+ parent_tag = top_tag;
+ }
+ }
+ }
+
+ if (cur_tag->flags & FL_HREF && html_document_state == html_document_state::body) {
+ auto maybe_url = html_process_url_tag(pool, cur_tag, hc);
+
+ if (maybe_url.has_value()) {
+ url = maybe_url.value();
+
+ if (url_set != NULL) {
+ struct rspamd_url *maybe_existing =
+ rspamd_url_set_add_or_return(url_set, maybe_url.value());
+ if (maybe_existing == maybe_url.value()) {
+ if (cur_url_order) {
+ url->order = (*cur_url_order)++;
+ }
+ url->part_order = cur_url_part_order++;
+ html_process_query_url(pool, url, url_set,
+ part_urls);
+ }
+ else {
+ url = maybe_existing;
+ /* Replace extra as well */
+ cur_tag->extra = maybe_existing;
+ /* Increase count to avoid odd checks failure */
+ url->count++;
+ }
+ }
+ if (part_urls) {
+ g_ptr_array_add(part_urls, url);
+ }
+
+ href_offset = hc->parsed.size();
+ }
+ }
+ else if (cur_tag->id == Tag_BASE) {
+ /*
+ * Base is allowed only within head tag but HTML is retarded
+ */
+ auto maybe_url = html_process_url_tag(pool, cur_tag, hc);
+
+ if (maybe_url) {
+ msg_debug_html("got valid base tag");
+ cur_tag->extra = maybe_url.value();
+ cur_tag->flags |= FL_HREF;
+
+ if (hc->base_url == nullptr) {
+ hc->base_url = maybe_url.value();
+ }
+ else {
+ msg_debug_html("ignore redundant base tag");
+ }
+ }
+ else {
+ msg_debug_html("got invalid base tag!");
+ }
+ }
+
+ if (cur_tag->id == Tag_IMG) {
+ html_process_img_tag(pool, cur_tag, hc, url_set,
+ part_urls);
+ }
+ else if (cur_tag->id == Tag_LINK) {
+ html_process_link_tag(pool, cur_tag, hc, url_set,
+ part_urls);
+ }
+
+ if (!(cur_tag->flags & CM_EMPTY)) {
+ html_process_block_tag(pool, cur_tag, hc);
+ }
+ else {
+ /* Implicitly close */
+ cur_tag->flags |= FL_CLOSED;
+ }
+
+ if (cur_tag->flags & FL_CLOSED) {
+ cur_tag->closing.end = cur_tag->content_offset;
+ cur_tag->closing.start = cur_tag->tag_start;
+
+ cur_tag = parent_tag;
+ }
+ };
+
+ p = (const char *) in->data;
+ c = p;
+ end = p + process_size;
+ start = c;
+
+ while (p < end) {
+ t = *p;
+
+ switch (state) {
+ case parse_start:
+ if (t == '<') {
+ state = tag_begin;
+ }
+ else {
+ /* We have no starting tag, so assume that it's content */
+ hc->flags |= RSPAMD_HTML_FLAG_BAD_START;
+ cur_tag = new_tag();
+ html_document_state = html_document_state::body;
+
+ if (cur_tag) {
+ cur_tag->id = Tag_HTML;
+ hc->root_tag = cur_tag;
+ state = content_before_start;
+ }
+ else {
+ state = tags_limit_overflow;
+ }
+ }
+ break;
+ case content_before_start:
+ if (t == '<') {
+ state = tag_begin;
+ }
+ else {
+ p++;
+ }
+ break;
+ case tag_begin:
+ switch (t) {
+ case '<':
+ c = p;
+ p++;
+ closing = FALSE;
+ break;
+ case '!':
+ cur_tag = new_tag(FL_XML | FL_CLOSED);
+ if (cur_tag) {
+ state = sgml_tag;
+ }
+ else {
+ state = tags_limit_overflow;
+ }
+ p++;
+ break;
+ case '?':
+ cur_tag = new_tag(FL_XML | FL_CLOSED);
+ if (cur_tag) {
+ state = xml_tag;
+ }
+ else {
+ state = tags_limit_overflow;
+ }
+ hc->flags |= RSPAMD_HTML_FLAG_XML;
+ p++;
+ break;
+ case '/':
+ closing = TRUE;
+ /* We fill fake closing tag to fill it with the content parser */
+ cur_closing_tag.clear();
+ /*
+ * For closing tags, we need to find some corresponding opening tag.
+ * However, at this point we have not even parsed a name, so we
+ * can not assume anything about balancing, etc.
+ *
+ * So we need to ensure that:
+ * 1) We have some opening tag in the chain cur_tag->parent...
+ * 2) cur_tag is nullptr - okay, html is just brain damaged
+ * 3) cur_tag must NOT be equal to cur_closing tag. It means that
+ * we had some poor closing tag but we still need to find an opening
+ * tag... Somewhere...
+ */
+
+ if (cur_tag == &cur_closing_tag) {
+ if (parent_tag != &cur_closing_tag) {
+ cur_closing_tag.parent = parent_tag;
+ }
+ else {
+ cur_closing_tag.parent = nullptr;
+ }
+ }
+ else if (cur_tag && cur_tag->flags & FL_CLOSED) {
+ /* Cur tag is already closed, we should find something else */
+ auto *tmp = cur_tag;
+ while (tmp) {
+ tmp = tmp->parent;
+
+ if (tmp == nullptr || !(tmp->flags & FL_CLOSED)) {
+ break;
+ }
+ }
+
+ cur_closing_tag.parent = tmp;
+ }
+ else {
+ cur_closing_tag.parent = cur_tag;
+ }
+
+ cur_tag = &cur_closing_tag;
+ p++;
+ break;
+ case '>':
+ /* Empty tag */
+ hc->flags |= RSPAMD_HTML_FLAG_BAD_ELEMENTS;
+ state = html_text_content;
+ continue;
+ default:
+ if (g_ascii_isalpha(t)) {
+ state = tag_content;
+ content_parser_env.reset();
+
+ if (!closing) {
+ cur_tag = new_tag();
+ }
+
+ if (cur_tag) {
+ state = tag_content;
+ }
+ else {
+ state = tags_limit_overflow;
+ }
+ }
+ else {
+ /* Wrong bad tag */
+ state = html_text_content;
+ }
+ break;
+ }
+
+ break;
+
+ case sgml_tag:
+ switch (t) {
+ case '[':
+ state = compound_tag;
+ obrace = 1;
+ ebrace = 0;
+ p++;
+ break;
+ case '-':
+ cur_tag->flags |= FL_COMMENT;
+ state = comment_tag;
+ p++;
+ break;
+ default:
+ state = sgml_content;
+ break;
+ }
+
+ break;
+
+ case xml_tag:
+ if (t == '?') {
+ state = xml_tag_end;
+ }
+ else if (t == '>') {
+ /* Misformed xml tag */
+ hc->flags |= RSPAMD_HTML_FLAG_BAD_ELEMENTS;
+ state = tag_end_opening;
+ continue;
+ }
+ /* We efficiently ignore xml tags */
+ p++;
+ break;
+
+ case xml_tag_end:
+ if (t == '>') {
+ state = tag_end_opening;
+ cur_tag->content_offset = p - start + 1;
+ continue;
+ }
+ else {
+ hc->flags |= RSPAMD_HTML_FLAG_BAD_ELEMENTS;
+ }
+ p++;
+ break;
+
+ case compound_tag:
+ if (t == '[') {
+ obrace++;
+ }
+ else if (t == ']') {
+ ebrace++;
+ }
+ else if (t == '>' && obrace == ebrace) {
+ state = tag_end_opening;
+ cur_tag->content_offset = p - start + 1;
+ continue;
+ }
+ p++;
+ break;
+
+ case comment_tag:
+ if (t != '-') {
+ hc->flags |= RSPAMD_HTML_FLAG_BAD_ELEMENTS;
+ state = tag_end_opening;
+ }
+ else {
+ p++;
+ ebrace = 0;
+ /*
+ * https://www.w3.org/TR/2012/WD-html5-20120329/syntax.html#syntax-comments
+ * ... the text must not start with a single
+ * U+003E GREATER-THAN SIGN character (>),
+ * nor start with a "-" (U+002D) character followed by
+ * a U+003E GREATER-THAN SIGN (>) character,
+ * nor contain two consecutive U+002D HYPHEN-MINUS
+ * characters (--), nor end with a "-" (U+002D) character.
+ */
+ if (p[0] == '-' && p + 1 < end && p[1] == '>') {
+ hc->flags |= RSPAMD_HTML_FLAG_BAD_ELEMENTS;
+ p++;
+ state = tag_end_opening;
+ }
+ else if (*p == '>') {
+ hc->flags |= RSPAMD_HTML_FLAG_BAD_ELEMENTS;
+ state = tag_end_opening;
+ }
+ else {
+ state = comment_content;
+ }
+ }
+ break;
+
+ case comment_content:
+ if (t == '-') {
+ ebrace++;
+ }
+ else if (t == '>' && ebrace >= 2) {
+ cur_tag->content_offset = p - start + 1;
+ state = tag_end_opening;
+ continue;
+ }
+ else {
+ ebrace = 0;
+ }
+
+ p++;
+ break;
+
+ case html_text_content:
+ if (t != '<') {
+ p++;
+ }
+ else {
+ state = tag_begin;
+ }
+ break;
+
+ case tag_raw_text:
+ if (t == '<') {
+ c = p;
+ state = tag_raw_text_less_than;
+ }
+ p++;
+ break;
+ case tag_raw_text_less_than:
+ if (t == '/') {
+ /* Here are special things: we look for obrace and then ensure
+ * that if there is any closing brace nearby
+ * (we look maximum at 30 characters). We also need to ensure
+ * that we have no special characters, such as punctuation marks and
+ * so on.
+ * Basically, we validate the input to be sane.
+ * Since closing tags must not have attributes, these assumptions
+ * seems to be reasonable enough for our toy parser.
+ */
+ gint cur_lookahead = 1;
+ gint max_lookahead = MIN(end - p, 30);
+ bool valid_closing_tag = true;
+
+ if (p + 1 < end && !g_ascii_isalpha(p[1])) {
+ valid_closing_tag = false;
+ }
+ else {
+ while (cur_lookahead < max_lookahead) {
+ gchar tt = p[cur_lookahead];
+ if (tt == '>') {
+ break;
+ }
+ else if (tt < '\n' || tt == ',') {
+ valid_closing_tag = false;
+ break;
+ }
+ cur_lookahead++;
+ }
+
+ if (cur_lookahead == max_lookahead) {
+ valid_closing_tag = false;
+ }
+ }
+
+ if (valid_closing_tag) {
+ /* Shift back */
+ p = c;
+ state = tag_begin;
+ }
+ else {
+ p++;
+ state = tag_raw_text;
+ }
+ }
+ else {
+ p++;
+ state = tag_raw_text;
+ }
+ break;
+ case sgml_content:
+ /* TODO: parse DOCTYPE here */
+ if (t == '>') {
+ cur_tag->content_offset = p - start + 1;
+ state = tag_end_opening;
+ }
+ else {
+ p++;
+ }
+ break;
+
+ case tag_content:
+ html_parse_tag_content(pool, hc, cur_tag, p, content_parser_env);
+
+ if (t == '>') {
+ if (content_parser_env.cur_state != parse_dqvalue && content_parser_env.cur_state != parse_sqvalue) {
+ /* We have a closing element */
+ if (closing) {
+ cur_tag->closing.start = c - start;
+ cur_tag->closing.end = p - start + 1;
+
+ closing = FALSE;
+ state = tag_end_closing;
+ }
+ else {
+ cur_tag->content_offset = p - start + 1;
+ state = tag_end_opening;
+ }
+ }
+ else {
+ /*
+ * We are in the parse_quoted value state but got
+ * an unescaped `>` character.
+ * HTML is written for monkeys, so there are two possibilities:
+ * 1) We have missing ending quote
+ * 2) We have unescaped `>` character
+ * How to distinguish between those possibilities?
+ * Well, the idea is to do some lookahead and try to find a
+ * quote. If we can find a quote, we just pretend as we have
+ * not seen `>` character. Otherwise, we pretend that it is an
+ * unquoted stuff. This logic is quite fragile but I really
+ * don't know any better options...
+ */
+ auto end_quote = content_parser_env.cur_state == parse_sqvalue ? '\'' : '"';
+ if (memchr(p, end_quote, end - p) != nullptr) {
+ /* Unencoded `>` */
+ p++;
+ continue;
+ }
+ else {
+ if (closing) {
+ cur_tag->closing.start = c - start;
+ cur_tag->closing.end = p - start + 1;
+
+ closing = FALSE;
+ state = tag_end_closing;
+ }
+ else {
+ cur_tag->content_offset = p - start + 1;
+ state = tag_end_opening;
+ }
+ }
+ }
+ continue;
+ }
+ p++;
+ break;
+
+ case tag_end_opening:
+ content_parser_env.reset();
+ state = html_text_content;
+
+ if (cur_tag) {
+ if (cur_tag->id == Tag_STYLE || cur_tag->id == Tag_NOSCRIPT || cur_tag->id == Tag_SCRIPT) {
+ state = tag_raw_text;
+ }
+ if (html_document_state == html_document_state::doctype) {
+ if (cur_tag->id == Tag_HEAD || (cur_tag->flags & CM_HEAD)) {
+ html_document_state = html_document_state::head;
+ cur_tag->flags |= FL_IGNORE;
+ }
+ else if (cur_tag->id != Tag_HTML) {
+ html_document_state = html_document_state::body;
+ }
+ }
+ else if (html_document_state == html_document_state::head) {
+ if (!(cur_tag->flags & (CM_EMPTY | CM_HEAD))) {
+ if (parent_tag && (parent_tag->id == Tag_HEAD || !(parent_tag->flags & CM_HEAD))) {
+ /*
+ * As by standard, we have to close the HEAD tag
+ * and switch to the body state
+ */
+ parent_tag->flags |= FL_CLOSED;
+ parent_tag->closing.start = cur_tag->tag_start;
+ parent_tag->closing.end = cur_tag->content_offset;
+
+ html_document_state = html_document_state::body;
+ }
+ else if (cur_tag->id == Tag_BODY) {
+ html_document_state = html_document_state::body;
+ }
+ else {
+ /*
+ * For propagation in something like
+ * <title><p><a>ololo</a></p></title> - should be unprocessed
+ */
+ cur_tag->flags |= CM_HEAD;
+ }
+ }
+ }
+
+ process_opening_tag();
+ }
+
+ p++;
+ c = p;
+ break;
+ case tag_end_closing: {
+ if (cur_tag) {
+
+ if (cur_tag->flags & CM_EMPTY) {
+ /* Ignore closing empty tags */
+ cur_tag->flags |= FL_IGNORE;
+ }
+ if (html_document_state == html_document_state::doctype) {
+ }
+ else if (html_document_state == html_document_state::head) {
+ if (cur_tag->id == Tag_HEAD) {
+ html_document_state = html_document_state::body;
+ }
+ }
+
+ /* cur_tag here is a closing tag */
+ auto *next_cur_tag = html_check_balance(hc, cur_tag,
+ c - start, p - start + 1);
+
+ if (cur_tag->id == Tag_STYLE && allow_css) {
+ auto *opening_tag = cur_tag->parent;
+
+ if (opening_tag && opening_tag->id == Tag_STYLE &&
+ (int) opening_tag->content_offset < opening_tag->closing.start) {
+ auto ret_maybe = rspamd::css::parse_css(pool,
+ {start + opening_tag->content_offset,
+ opening_tag->closing.start - opening_tag->content_offset},
+ std::move(hc->css_style));
+
+ if (!ret_maybe.has_value()) {
+ if (ret_maybe.error().is_fatal()) {
+ auto err_str = fmt::format(
+ "cannot parse css (error code: {}): {}",
+ static_cast<int>(ret_maybe.error().type),
+ ret_maybe.error().description.value_or("unknown error"));
+ msg_info_pool("%*s", (int) err_str.size(), err_str.data());
+ }
+ }
+ else {
+ hc->css_style = ret_maybe.value();
+ }
+ }
+ }
+
+ if (next_cur_tag != nullptr) {
+ cur_tag = next_cur_tag;
+ }
+ else {
+ /*
+ * Here, we handle cases like <p>lala</b>...
+ * So the tag </b> is bogus and unpaired
+ * However, we need to exclude it from the output of <p> tag
+ * To do that, we create a fake opening tag and insert that to
+ * the current opening tag
+ */
+ auto *cur_opening_tag = cur_tag->parent;
+
+ while (cur_opening_tag && (cur_opening_tag->flags & FL_CLOSED)) {
+ cur_opening_tag = cur_opening_tag->parent;
+ }
+
+ if (!cur_opening_tag) {
+ cur_opening_tag = hc->root_tag;
+ }
+
+ auto &&vtag = std::make_unique<html_tag>();
+ vtag->id = cur_tag->id;
+ vtag->flags = FL_VIRTUAL | FL_CLOSED | cur_tag->flags;
+ vtag->tag_start = cur_tag->closing.start;
+ vtag->content_offset = p - start + 1;
+ vtag->closing = cur_tag->closing;
+ vtag->parent = cur_opening_tag;
+ g_assert(vtag->parent != &cur_closing_tag);
+ cur_opening_tag->children.push_back(vtag.get());
+ hc->all_tags.emplace_back(std::move(vtag));
+ cur_tag = cur_opening_tag;
+ parent_tag = cur_tag->parent;
+ g_assert(cur_tag->parent != &cur_closing_tag);
+ }
+ } /* if cur_tag != nullptr */
+ state = html_text_content;
+ p++;
+ c = p;
+ break;
+ }
+ case tags_limit_overflow:
+ msg_warn_pool("tags limit of %d tags is reached at the position %d;"
+ " ignoring the rest of the HTML content",
+ (int) hc->all_tags.size(), (int) (p - start));
+ c = p;
+ p = end;
+ break;
+ }
+ }
+
+ if (cur_tag && !(cur_tag->flags & FL_CLOSED) && cur_tag != &cur_closing_tag) {
+ cur_closing_tag.parent = cur_tag;
+ cur_closing_tag.id = cur_tag->id;
+ cur_tag = &cur_closing_tag;
+ html_check_balance(hc, cur_tag,
+ end - start, end - start);
+ }
+
+ /* Propagate styles */
+ hc->traverse_block_tags([&hc, &pool](const html_tag *tag) -> bool {
+ if (hc->css_style && tag->id > Tag_UNKNOWN && tag->id < Tag_MAX) {
+ auto *css_block = hc->css_style->check_tag_block(tag);
+
+ if (css_block) {
+ if (tag->block) {
+ tag->block->set_block(*css_block);
+ }
+ else {
+ tag->block = css_block;
+ }
+ }
+ }
+ if (tag->block) {
+ if (!tag->block->has_display()) {
+ /* If we have no display field, we can check it by tag */
+ if (tag->flags & CM_HEAD) {
+ tag->block->set_display(css::css_display_value::DISPLAY_HIDDEN,
+ html_block::set);
+ }
+ else if (tag->flags & (CM_BLOCK | CM_TABLE)) {
+ tag->block->set_display(css::css_display_value::DISPLAY_BLOCK,
+ html_block::implicit);
+ }
+ else if (tag->flags & CM_ROW) {
+ tag->block->set_display(css::css_display_value::DISPLAY_TABLE_ROW,
+ html_block::implicit);
+ }
+ else {
+ tag->block->set_display(css::css_display_value::DISPLAY_INLINE,
+ html_block::implicit);
+ }
+ }
+
+ tag->block->compute_visibility();
+
+ for (const auto *cld_tag: tag->children) {
+
+ if (cld_tag->block) {
+ cld_tag->block->propagate_block(*tag->block);
+ }
+ else {
+ cld_tag->block = rspamd_mempool_alloc0_type(pool, html_block);
+ *cld_tag->block = *tag->block;
+ }
+ }
+ }
+ return true;
+ },
+ html_content::traverse_type::PRE_ORDER);
+
+ /* Leftover before content */
+ switch (state) {
+ case tag_end_opening:
+ if (cur_tag != nullptr) {
+ process_opening_tag();
+ }
+ break;
+ default:
+ /* Do nothing */
+ break;
+ }
+
+ if (!hc->all_tags.empty() && hc->root_tag) {
+ html_append_tag_content(pool, start, end - start, hc, hc->root_tag,
+ exceptions, url_set);
+ }
+
+ /* Leftover after content */
+ switch (state) {
+ case tags_limit_overflow:
+ html_append_parsed(hc, {c, (std::size_t)(end - c)},
+ false, end - start, hc->parsed);
+ break;
+ default:
+ /* Do nothing */
+ break;
+ }
+
+ if (overflow_input) {
+ /*
+ * Append the rest of the input as raw html, this might work as
+ * further algorithms can skip words when auto *pool = task->task_pool;there are too many.
+ * It is still unclear about urls though...
+ */
+ html_append_parsed(hc, {end, in->len - process_size}, false,
+ end - start, hc->parsed);
+ }
+
+ if (!hc->parsed.empty()) {
+ /* Trim extra spaces at the end if needed */
+ if (g_ascii_isspace(hc->parsed.back())) {
+ auto last_it = std::end(hc->parsed);
+
+ /* Allow last newline */
+ if (hc->parsed.back() == '\n') {
+ --last_it;
+ }
+
+ hc->parsed.erase(std::find_if(hc->parsed.rbegin(), hc->parsed.rend(),
+ [](auto ch) -> auto {
+ return !g_ascii_isspace(ch);
+ })
+ .base(),
+ last_it);
+ }
+ }
+
+ return hc;
+}
+
+static auto
+html_find_image_by_cid(const html_content &hc, std::string_view cid)
+ -> std::optional<const html_image *>
+{
+ for (const auto *html_image: hc.images) {
+ /* Filter embedded images */
+ if (html_image->flags & RSPAMD_HTML_FLAG_IMAGE_EMBEDDED &&
+ html_image->src != nullptr) {
+ if (cid == html_image->src) {
+ return html_image;
+ }
+ }
+ }
+
+ return std::nullopt;
+}
+
+auto html_debug_structure(const html_content &hc) -> std::string
+{
+ std::string output;
+
+ if (hc.root_tag) {
+ auto rec_functor = [&](const html_tag *t, int level, auto rec_functor) -> void {
+ std::string pluses(level, '+');
+
+ if (!(t->flags & (FL_VIRTUAL | FL_IGNORE))) {
+ if (t->flags & FL_XML) {
+ output += fmt::format("{}xml;", pluses);
+ }
+ else {
+ output += fmt::format("{}{};", pluses,
+ html_tags_defs.name_by_id_safe(t->id));
+ }
+ level++;
+ }
+ for (const auto *cld: t->children) {
+ rec_functor(cld, level, rec_functor);
+ }
+ };
+
+ rec_functor(hc.root_tag, 1, rec_functor);
+ }
+
+ return output;
+}
+
+auto html_tag_by_name(const std::string_view &name)
+ -> std::optional<tag_id_t>
+{
+ const auto *td = rspamd::html::html_tags_defs.by_name(name);
+
+ if (td != nullptr) {
+ return td->id;
+ }
+
+ return std::nullopt;
+}
+
+auto html_tag::get_content(const struct html_content *hc) const -> std::string_view
+{
+ const std::string *dest = &hc->parsed;
+
+ if (block && !block->is_visible()) {
+ dest = &hc->invisible;
+ }
+ const auto clen = get_content_length();
+ if (content_offset < dest->size()) {
+ if (dest->size() - content_offset >= clen) {
+ return std::string_view{*dest}.substr(content_offset, clen);
+ }
+ else {
+ return std::string_view{*dest}.substr(content_offset, dest->size() - content_offset);
+ }
+ }
+
+ return std::string_view{};
+}
+
+}// namespace rspamd::html
+
+void *
+rspamd_html_process_part_full(struct rspamd_task *task,
+ GByteArray *in, GList **exceptions,
+ khash_t(rspamd_url_hash) * url_set,
+ GPtrArray *part_urls,
+ bool allow_css,
+ uint16_t *cur_url_order)
+{
+ return rspamd::html::html_process_input(task, in, exceptions, url_set,
+ part_urls, allow_css, cur_url_order);
+}
+
+void *
+rspamd_html_process_part(rspamd_mempool_t *pool,
+ GByteArray *in)
+{
+ struct rspamd_task fake_task;
+ memset(&fake_task, 0, sizeof(fake_task));
+ fake_task.task_pool = pool;
+ uint16_t order = 0;
+
+ return rspamd_html_process_part_full(&fake_task, in, NULL,
+ NULL, NULL, FALSE, &order);
+}
+
+guint rspamd_html_decode_entitles_inplace(gchar *s, gsize len)
+{
+ return rspamd::html::decode_html_entitles_inplace(s, len);
+}
+
+gint rspamd_html_tag_by_name(const gchar *name)
+{
+ const auto *td = rspamd::html::html_tags_defs.by_name(name);
+
+ if (td != nullptr) {
+ return td->id;
+ }
+
+ return -1;
+}
+
+gboolean
+rspamd_html_tag_seen(void *ptr, const gchar *tagname)
+{
+ gint id;
+ auto *hc = rspamd::html::html_content::from_ptr(ptr);
+
+ g_assert(hc != NULL);
+
+ id = rspamd_html_tag_by_name(tagname);
+
+ if (id != -1) {
+ return hc->tags_seen[id];
+ }
+
+ return FALSE;
+}
+
+const gchar *
+rspamd_html_tag_by_id(gint id)
+{
+ if (id > Tag_UNKNOWN && id < Tag_MAX) {
+ const auto *td = rspamd::html::html_tags_defs.by_id(id);
+
+ if (td != nullptr) {
+ return td->name.c_str();
+ }
+ }
+
+ return nullptr;
+}
+
+const gchar *
+rspamd_html_tag_name(void *p, gsize *len)
+{
+ auto *tag = reinterpret_cast<rspamd::html::html_tag *>(p);
+ auto tname = rspamd::html::html_tags_defs.name_by_id_safe(tag->id);
+
+ if (len) {
+ *len = tname.size();
+ }
+
+ return tname.data();
+}
+
+struct html_image *
+rspamd_html_find_embedded_image(void *html_content,
+ const char *cid, gsize cid_len)
+{
+ auto *hc = rspamd::html::html_content::from_ptr(html_content);
+
+ auto maybe_img = rspamd::html::html_find_image_by_cid(*hc, {cid, cid_len});
+
+ if (maybe_img) {
+ return (html_image *) maybe_img.value();
+ }
+
+ return nullptr;
+}
+
+bool rspamd_html_get_parsed_content(void *html_content, rspamd_ftok_t *dest)
+{
+ auto *hc = rspamd::html::html_content::from_ptr(html_content);
+
+ dest->begin = hc->parsed.data();
+ dest->len = hc->parsed.size();
+
+ return true;
+}
+
+gsize rspamd_html_get_tags_count(void *html_content)
+{
+ auto *hc = rspamd::html::html_content::from_ptr(html_content);
+
+ if (!hc) {
+ return 0;
+ }
+
+ return hc->all_tags.size();
+} \ No newline at end of file
diff --git a/src/libserver/html/html.h b/src/libserver/html/html.h
new file mode 100644
index 0000000..2d34f2a
--- /dev/null
+++ b/src/libserver/html/html.h
@@ -0,0 +1,137 @@
+/*-
+ * Copyright 2021 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_HTML_H
+#define RSPAMD_HTML_H
+
+#include "config.h"
+#include "libutil/mem_pool.h"
+#include "libserver/url.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * HTML content flags
+ */
+#define RSPAMD_HTML_FLAG_BAD_START (1 << 0)
+#define RSPAMD_HTML_FLAG_BAD_ELEMENTS (1 << 1)
+#define RSPAMD_HTML_FLAG_XML (1 << 2)
+#define RSPAMD_HTML_FLAG_UNBALANCED (1 << 3)
+#define RSPAMD_HTML_FLAG_UNKNOWN_ELEMENTS (1 << 4)
+#define RSPAMD_HTML_FLAG_DUPLICATE_ELEMENTS (1 << 5)
+#define RSPAMD_HTML_FLAG_TOO_MANY_TAGS (1 << 6)
+#define RSPAMD_HTML_FLAG_HAS_DATA_URLS (1 << 7)
+#define RSPAMD_HTML_FLAG_HAS_ZEROS (1 << 8)
+
+/*
+ * Image flags
+ */
+#define RSPAMD_HTML_FLAG_IMAGE_EMBEDDED (1 << 0)
+#define RSPAMD_HTML_FLAG_IMAGE_EXTERNAL (1 << 1)
+#define RSPAMD_HTML_FLAG_IMAGE_DATA (1 << 2)
+
+
+struct rspamd_image;
+
+struct html_image {
+ guint height;
+ guint width;
+ guint flags;
+ gchar *src;
+ struct rspamd_url *url;
+ struct rspamd_image *embedded_image;
+ void *tag;
+};
+
+
+/* Forwarded declaration */
+struct rspamd_task;
+
+/*
+ * Decode HTML entitles in text. Text is modified in place.
+ */
+guint rspamd_html_decode_entitles_inplace(gchar *s, gsize len);
+
+void *rspamd_html_process_part(rspamd_mempool_t *pool,
+ GByteArray *in);
+
+void *rspamd_html_process_part_full(struct rspamd_task *task,
+ GByteArray *in, GList **exceptions,
+ khash_t(rspamd_url_hash) * url_set,
+ GPtrArray *part_urls,
+ bool allow_css,
+ uint16_t *cur_url_order);
+
+/*
+ * Returns true if a specified tag has been seen in a part
+ */
+gboolean rspamd_html_tag_seen(void *ptr, const gchar *tagname);
+
+/**
+ * Returns name for the specified tag id
+ * @param id
+ * @return
+ */
+const gchar *rspamd_html_tag_by_id(gint id);
+
+/**
+ * Returns HTML tag id by name
+ * @param name
+ * @return
+ */
+gint rspamd_html_tag_by_name(const gchar *name);
+
+/**
+ * Gets a name for a tag
+ * @param tag
+ * @param len
+ * @return
+ */
+const gchar *rspamd_html_tag_name(void *tag, gsize *len);
+
+/**
+ * Find HTML image by content id
+ * @param html_content
+ * @param cid
+ * @param cid_len
+ * @return
+ */
+struct html_image *rspamd_html_find_embedded_image(void *html_content,
+ const char *cid, gsize cid_len);
+
+/**
+ * Stores parsed content in ftok_t structure
+ * @param html_content
+ * @param dest
+ * @return
+ */
+bool rspamd_html_get_parsed_content(void *html_content, rspamd_ftok_t *dest);
+
+/**
+ * Returns number of tags in the html content
+ * @param html_content
+ * @return
+ */
+gsize rspamd_html_get_tags_count(void *html_content);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/html/html.hxx b/src/libserver/html/html.hxx
new file mode 100644
index 0000000..3320fd6
--- /dev/null
+++ b/src/libserver/html/html.hxx
@@ -0,0 +1,146 @@
+/*-
+ * Copyright 2021 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_HTML_HXX
+#define RSPAMD_HTML_HXX
+#pragma once
+
+#include "config.h"
+#include "libserver/url.h"
+#include "libserver/html/html_tag.hxx"
+#include "libserver/html/html.h"
+#include "libserver/html/html_tags.h"
+
+
+#include <vector>
+#include <memory>
+#include <string>
+#include "function2/function2.hpp"
+
+namespace rspamd::css {
+/* Forward declaration */
+class css_style_sheet;
+}// namespace rspamd::css
+
+namespace rspamd::html {
+
+struct html_block;
+
+struct html_content {
+ struct rspamd_url *base_url = nullptr;
+ struct html_tag *root_tag = nullptr;
+ gint flags = 0;
+ std::vector<bool> tags_seen;
+ std::vector<html_image *> images;
+ std::vector<std::unique_ptr<struct html_tag>> all_tags;
+ std::string parsed;
+ std::string invisible;
+ std::shared_ptr<css::css_style_sheet> css_style;
+
+ /* Preallocate and reserve all internal structures */
+ html_content()
+ {
+ tags_seen.resize(Tag_MAX, false);
+ all_tags.reserve(128);
+ parsed.reserve(256);
+ }
+
+ static void html_content_dtor(void *ptr)
+ {
+ delete html_content::from_ptr(ptr);
+ }
+
+ static auto from_ptr(void *ptr) -> html_content *
+ {
+ return static_cast<html_content *>(ptr);
+ }
+
+ enum class traverse_type {
+ PRE_ORDER,
+ POST_ORDER
+ };
+ auto traverse_block_tags(fu2::function<bool(const html_tag *)> &&func,
+ traverse_type how = traverse_type::PRE_ORDER) const -> bool
+ {
+
+ if (root_tag == nullptr) {
+ return false;
+ }
+
+ auto rec_functor_pre_order = [&](const html_tag *root, auto &&rec) -> bool {
+ if (func(root)) {
+
+ for (const auto *c: root->children) {
+ if (!rec(c, rec)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ return false;
+ };
+ auto rec_functor_post_order = [&](const html_tag *root, auto &&rec) -> bool {
+ for (const auto *c: root->children) {
+ if (!rec(c, rec)) {
+ return false;
+ }
+ }
+
+ return func(root);
+ };
+
+ switch (how) {
+ case traverse_type::PRE_ORDER:
+ return rec_functor_pre_order(root_tag, rec_functor_pre_order);
+ case traverse_type::POST_ORDER:
+ return rec_functor_post_order(root_tag, rec_functor_post_order);
+ default:
+ RSPAMD_UNREACHABLE;
+ }
+ }
+
+ auto traverse_all_tags(fu2::function<bool(const html_tag *)> &&func) const -> bool
+ {
+ for (const auto &tag: all_tags) {
+ if (!(tag->flags & (FL_XML | FL_VIRTUAL))) {
+ if (!func(tag.get())) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+private:
+ ~html_content() = default;
+};
+
+
+auto html_tag_by_name(const std::string_view &name) -> std::optional<tag_id_t>;
+auto html_process_input(struct rspamd_task *task,
+ GByteArray *in,
+ GList **exceptions,
+ khash_t(rspamd_url_hash) * url_set,
+ GPtrArray *part_urls,
+ bool allow_css,
+ std::uint16_t *cur_url_order) -> html_content *;
+auto html_debug_structure(const html_content &hc) -> std::string;
+
+}// namespace rspamd::html
+
+#endif//RSPAMD_HTML_HXX
diff --git a/src/libserver/html/html_block.hxx b/src/libserver/html/html_block.hxx
new file mode 100644
index 0000000..f9b5184
--- /dev/null
+++ b/src/libserver/html/html_block.hxx
@@ -0,0 +1,358 @@
+/*-
+ * Copyright 2021 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_HTML_BLOCK_HXX
+#define RSPAMD_HTML_BLOCK_HXX
+#pragma once
+
+#include "libserver/css/css_value.hxx"
+#include <cmath>
+
+namespace rspamd::html {
+
+/*
+ * Block tag definition
+ */
+struct html_block {
+ rspamd::css::css_color fg_color;
+ rspamd::css::css_color bg_color;
+ std::int16_t height;
+ std::int16_t width;
+ rspamd::css::css_display_value display;
+ std::int8_t font_size;
+
+ unsigned fg_color_mask : 2;
+ unsigned bg_color_mask : 2;
+ unsigned height_mask : 2;
+ unsigned width_mask : 2;
+ unsigned font_mask : 2;
+ unsigned display_mask : 2;
+ unsigned visibility_mask : 2;
+
+ constexpr static const auto unset = 0;
+ constexpr static const auto inherited = 1;
+ constexpr static const auto implicit = 1;
+ constexpr static const auto set = 3;
+ constexpr static const auto invisible_flag = 1;
+ constexpr static const auto transparent_flag = 2;
+
+ /* Helpers to set mask when setting the elements */
+ auto set_fgcolor(const rspamd::css::css_color &c, int how = html_block::set) -> void
+ {
+ fg_color = c;
+ fg_color_mask = how;
+ }
+ auto set_bgcolor(const rspamd::css::css_color &c, int how = html_block::set) -> void
+ {
+ bg_color = c;
+ bg_color_mask = how;
+ }
+ auto set_height(float h, bool is_percent = false, int how = html_block::set) -> void
+ {
+ h = is_percent ? (-h) : h;
+ if (h < INT16_MIN) {
+ /* Negative numbers encode percents... */
+ height = -100;
+ }
+ else if (h > INT16_MAX) {
+ height = INT16_MAX;
+ }
+ else {
+ height = h;
+ }
+ height_mask = how;
+ }
+
+ auto set_width(float w, bool is_percent = false, int how = html_block::set) -> void
+ {
+ w = is_percent ? (-w) : w;
+ if (w < INT16_MIN) {
+ width = INT16_MIN;
+ }
+ else if (w > INT16_MAX) {
+ width = INT16_MAX;
+ }
+ else {
+ width = w;
+ }
+ width_mask = how;
+ }
+
+ auto set_display(bool v, int how = html_block::set) -> void
+ {
+ if (v) {
+ display = rspamd::css::css_display_value::DISPLAY_INLINE;
+ }
+ else {
+ display = rspamd::css::css_display_value::DISPLAY_HIDDEN;
+ }
+ display_mask = how;
+ }
+
+ auto set_display(rspamd::css::css_display_value v, int how = html_block::set) -> void
+ {
+ display = v;
+ display_mask = how;
+ }
+
+ auto set_font_size(float fs, bool is_percent = false, int how = html_block::set) -> void
+ {
+ fs = is_percent ? (-fs) : fs;
+ if (fs < INT8_MIN) {
+ font_size = -100;
+ }
+ else if (fs > INT8_MAX) {
+ font_size = INT8_MAX;
+ }
+ else {
+ font_size = fs;
+ }
+ font_mask = how;
+ }
+
+private:
+ template<typename T, typename MT>
+ static constexpr auto simple_prop(MT mask_val, MT other_mask, T &our_val,
+ T other_val) -> MT
+ {
+ if (other_mask && other_mask > mask_val) {
+ our_val = other_val;
+ mask_val = html_block::inherited;
+ }
+
+ return mask_val;
+ }
+
+ /* Sizes propagation logic
+ * We can have multiple cases:
+ * 1) Our size is > 0 and we can use it as is
+ * 2) Parent size is > 0 and our size is undefined, so propagate parent
+ * 3) Parent size is < 0 and our size is undefined - propagate parent
+ * 4) Parent size is > 0 and our size is < 0 - multiply parent by abs(ours)
+ * 5) Parent size is undefined and our size is < 0 - tricky stuff, assume some defaults
+ */
+ template<typename T, typename MT>
+ static constexpr auto size_prop(MT mask_val, MT other_mask, T &our_val,
+ T other_val, T default_val) -> MT
+ {
+ if (mask_val) {
+ /* We have our value */
+ if (our_val < 0) {
+ if (other_mask > 0) {
+ if (other_val >= 0) {
+ our_val = other_val * (-our_val / 100.0);
+ }
+ else {
+ our_val *= (-other_val / 100.0);
+ }
+ }
+ else {
+ /* Parent value is not defined and our value is relative */
+ our_val = default_val * (-our_val / 100.0);
+ }
+ }
+ else if (other_mask && other_mask > mask_val) {
+ our_val = other_val;
+ mask_val = html_block::inherited;
+ }
+ }
+ else {
+ /* We propagate parent if defined */
+ if (other_mask && other_mask > mask_val) {
+ our_val = other_val;
+ mask_val = html_block::inherited;
+ }
+ /* Otherwise do nothing */
+ }
+
+ return mask_val;
+ }
+
+public:
+ /**
+ * Propagate values from the block if they are not defined by the current block
+ * @param other
+ * @return
+ */
+ auto propagate_block(const html_block &other) -> void
+ {
+ fg_color_mask = html_block::simple_prop(fg_color_mask, other.fg_color_mask,
+ fg_color, other.fg_color);
+ bg_color_mask = html_block::simple_prop(bg_color_mask, other.bg_color_mask,
+ bg_color, other.bg_color);
+ display_mask = html_block::simple_prop(display_mask, other.display_mask,
+ display, other.display);
+
+ height_mask = html_block::size_prop(height_mask, other.height_mask,
+ height, other.height, static_cast<std::int16_t>(800));
+ width_mask = html_block::size_prop(width_mask, other.width_mask,
+ width, other.width, static_cast<std::int16_t>(1024));
+ font_mask = html_block::size_prop(font_mask, other.font_mask,
+ font_size, other.font_size, static_cast<std::int8_t>(10));
+ }
+
+ /*
+ * Set block overriding all inherited values
+ */
+ auto set_block(const html_block &other) -> void
+ {
+ constexpr auto set_value = [](auto mask_val, auto other_mask, auto &our_val,
+ auto other_val) constexpr -> int {
+ if (other_mask && mask_val != html_block::set) {
+ our_val = other_val;
+ mask_val = other_mask;
+ }
+
+ return mask_val;
+ };
+
+ fg_color_mask = set_value(fg_color_mask, other.fg_color_mask, fg_color, other.fg_color);
+ bg_color_mask = set_value(bg_color_mask, other.bg_color_mask, bg_color, other.bg_color);
+ display_mask = set_value(display_mask, other.display_mask, display, other.display);
+ height_mask = set_value(height_mask, other.height_mask, height, other.height);
+ width_mask = set_value(width_mask, other.width_mask, width, other.width);
+ font_mask = set_value(font_mask, other.font_mask, font_size, other.font_size);
+ }
+
+ auto compute_visibility(void) -> void
+ {
+ if (display_mask) {
+ if (display == css::css_display_value::DISPLAY_HIDDEN) {
+ visibility_mask = html_block::invisible_flag;
+
+ return;
+ }
+ }
+
+ if (font_mask) {
+ if (font_size == 0) {
+ visibility_mask = html_block::invisible_flag;
+
+ return;
+ }
+ }
+
+ auto is_similar_colors = [](const rspamd::css::css_color &fg, const rspamd::css::css_color &bg) -> bool {
+ constexpr const auto min_visible_diff = 0.1f;
+ auto diff_r = ((float) fg.r - bg.r);
+ auto diff_g = ((float) fg.g - bg.g);
+ auto diff_b = ((float) fg.b - bg.b);
+ auto ravg = ((float) fg.r + bg.r) / 2.0f;
+
+ /* Square diffs */
+ diff_r *= diff_r;
+ diff_g *= diff_g;
+ diff_b *= diff_b;
+
+ auto diff = std::sqrt(2.0f * diff_r + 4.0f * diff_g + 3.0f * diff_b +
+ (ravg * (diff_r - diff_b) / 256.0f)) /
+ 256.0f;
+
+ return diff < min_visible_diff;
+ };
+ /* Check if we have both bg/fg colors */
+ if (fg_color_mask && bg_color_mask) {
+ if (fg_color.alpha < 10) {
+ /* Too transparent */
+ visibility_mask = html_block::transparent_flag;
+
+ return;
+ }
+
+ if (bg_color.alpha > 10) {
+ if (is_similar_colors(fg_color, bg_color)) {
+ visibility_mask = html_block::transparent_flag;
+ return;
+ }
+ }
+ }
+ else if (fg_color_mask) {
+ /* Merely fg color */
+ if (fg_color.alpha < 10) {
+ /* Too transparent */
+ visibility_mask = html_block::transparent_flag;
+
+ return;
+ }
+
+ /* Implicit fg color */
+ if (is_similar_colors(fg_color, rspamd::css::css_color::white())) {
+ visibility_mask = html_block::transparent_flag;
+ return;
+ }
+ }
+ else if (bg_color_mask) {
+ if (bg_color.alpha > 10) {
+ if (is_similar_colors(rspamd::css::css_color::black(), bg_color)) {
+ visibility_mask = html_block::transparent_flag;
+ return;
+ }
+ }
+ }
+
+ visibility_mask = html_block::unset;
+ }
+
+ constexpr auto is_visible(void) const -> bool
+ {
+ return visibility_mask == html_block::unset;
+ }
+
+ constexpr auto is_transparent(void) const -> bool
+ {
+ return visibility_mask == html_block::transparent_flag;
+ }
+
+ constexpr auto has_display(int how = html_block::set) const -> bool
+ {
+ return display_mask >= how;
+ }
+
+ /**
+ * Returns a default html block for root HTML element
+ * @return
+ */
+ static auto default_html_block(void) -> html_block
+ {
+ return html_block{.fg_color = rspamd::css::css_color::black(),
+ .bg_color = rspamd::css::css_color::white(),
+ .height = 0,
+ .width = 0,
+ .display = rspamd::css::css_display_value::DISPLAY_INLINE,
+ .font_size = 12,
+ .fg_color_mask = html_block::inherited,
+ .bg_color_mask = html_block::inherited,
+ .height_mask = html_block::unset,
+ .width_mask = html_block::unset,
+ .font_mask = html_block::unset,
+ .display_mask = html_block::inherited,
+ .visibility_mask = html_block::unset};
+ }
+ /**
+ * Produces html block with no defined values allocated from the pool
+ * @param pool
+ * @return
+ */
+ static auto undefined_html_block_pool(rspamd_mempool_t *pool) -> html_block *
+ {
+ auto *bl = rspamd_mempool_alloc0_type(pool, html_block);
+
+ return bl;
+ }
+};
+
+}// namespace rspamd::html
+
+#endif//RSPAMD_HTML_BLOCK_HXX
diff --git a/src/libserver/html/html_entities.cxx b/src/libserver/html/html_entities.cxx
new file mode 100644
index 0000000..c642536
--- /dev/null
+++ b/src/libserver/html/html_entities.cxx
@@ -0,0 +1,2644 @@
+/*-
+ * Copyright 2021 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 "html_entities.hxx"
+
+#include <string>
+#include <utility>
+#include <vector>
+#include "contrib/ankerl/unordered_dense.h"
+#include <unicode/utf8.h>
+#include <unicode/uchar.h>
+#include "libutil/cxx/util.hxx"
+
+#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
+#include "doctest/doctest.h"
+
+namespace rspamd::html {
+
+struct html_entity_def {
+ const char *name;
+ const char *replacement;
+ unsigned code;
+ bool allow_heuristic;
+};
+
+#define ENTITY_DEF(name, code, replacement) \
+ html_entity_def \
+ { \
+ (name), (replacement), (code), false \
+ }
+#define ENTITY_DEF_HEUR(name, code, replacement) \
+ html_entity_def \
+ { \
+ (name), (replacement), (code), true \
+ }
+
+static const html_entity_def html_entities_array[] = {
+ ENTITY_DEF_HEUR("szlig", 223, "\xc3\x9f"),
+ ENTITY_DEF("prime", 8242, "\xe2\x80\xb2"),
+ ENTITY_DEF("lnsim", 8934, "\xe2\x8b\xa6"),
+ ENTITY_DEF("nvDash", 8877, "\xe2\x8a\xad"),
+ ENTITY_DEF("isinsv", 8947, "\xe2\x8b\xb3"),
+ ENTITY_DEF("notin", 8713, "\xe2\x88\x89"),
+ ENTITY_DEF("becaus", 8757, "\xe2\x88\xb5"),
+ ENTITY_DEF("Leftrightarrow", 8660, "\xe2\x87\x94"),
+ ENTITY_DEF("EmptySmallSquare", 9723, "\xe2\x97\xbb"),
+ ENTITY_DEF("SquareUnion", 8852, "\xe2\x8a\x94"),
+ ENTITY_DEF("subdot", 10941, "\xe2\xaa\xbd"),
+ ENTITY_DEF("Dstrok", 272, "\xc4\x90"),
+ ENTITY_DEF("rrarr", 8649, "\xe2\x87\x89"),
+ ENTITY_DEF("rArr", 8658, "\xe2\x87\x92"),
+ ENTITY_DEF_HEUR("Aacute", 193, "\xc3\x81"),
+ ENTITY_DEF("kappa", 954, "\xce\xba"),
+ ENTITY_DEF("Iopf", 120128, "\xf0\x9d\x95\x80"),
+ ENTITY_DEF("hyphen", 8208, "\xe2\x80\x90"),
+ ENTITY_DEF("rarrbfs", 10528, "\xe2\xa4\xa0"),
+ ENTITY_DEF("supsetneqq", 10956, "\xe2\xab\x8c"),
+ ENTITY_DEF("gacute", 501, "\xc7\xb5"),
+ ENTITY_DEF("VeryThinSpace", 8202, "\xe2\x80\x8a"),
+ ENTITY_DEF("tint", 8749, "\xe2\x88\xad"),
+ ENTITY_DEF("ffr", 120099, "\xf0\x9d\x94\xa3"),
+ ENTITY_DEF("kgreen", 312, "\xc4\xb8"),
+ ENTITY_DEF("nis", 8956, "\xe2\x8b\xbc"),
+ ENTITY_DEF("NotRightTriangleBar", 10704, "\xe2\xa7\x90\xcc\xb8"),
+ ENTITY_DEF("Eogon", 280, "\xc4\x98"),
+ ENTITY_DEF("lbrke", 10635, "\xe2\xa6\x8b"),
+ ENTITY_DEF("phi", 966, "\xcf\x86"),
+ ENTITY_DEF("notnivc", 8957, "\xe2\x8b\xbd"),
+ ENTITY_DEF("utilde", 361, "\xc5\xa9"),
+ ENTITY_DEF("Fopf", 120125, "\xf0\x9d\x94\xbd"),
+ ENTITY_DEF("Vcy", 1042, "\xd0\x92"),
+ ENTITY_DEF("erDot", 8787, "\xe2\x89\x93"),
+ ENTITY_DEF("nsubE", 10949, "\xe2\xab\x85\xcc\xb8"),
+ ENTITY_DEF_HEUR("egrave", 232, "\xc3\xa8"),
+ ENTITY_DEF("Lcedil", 315, "\xc4\xbb"),
+ ENTITY_DEF("lharul", 10602, "\xe2\xa5\xaa"),
+ ENTITY_DEF_HEUR("middot", 183, "\xc2\xb7"),
+ ENTITY_DEF("ggg", 8921, "\xe2\x8b\x99"),
+ ENTITY_DEF("NestedLessLess", 8810, "\xe2\x89\xaa"),
+ ENTITY_DEF("tau", 964, "\xcf\x84"),
+ ENTITY_DEF("setmn", 8726, "\xe2\x88\x96"),
+ ENTITY_DEF("frac78", 8542, "\xe2\x85\x9e"),
+ ENTITY_DEF_HEUR("para", 182, "\xc2\xb6"),
+ ENTITY_DEF("Rcedil", 342, "\xc5\x96"),
+ ENTITY_DEF("propto", 8733, "\xe2\x88\x9d"),
+ ENTITY_DEF("sqsubset", 8847, "\xe2\x8a\x8f"),
+ ENTITY_DEF("ensp", 8194, "\xe2\x80\x82"),
+ ENTITY_DEF("boxvH", 9578, "\xe2\x95\xaa"),
+ ENTITY_DEF("NotGreaterTilde", 8821, "\xe2\x89\xb5"),
+ ENTITY_DEF("ffllig", 64260, "\xef\xac\x84"),
+ ENTITY_DEF("kcedil", 311, "\xc4\xb7"),
+ ENTITY_DEF("omega", 969, "\xcf\x89"),
+ ENTITY_DEF("sime", 8771, "\xe2\x89\x83"),
+ ENTITY_DEF("LeftTriangleEqual", 8884, "\xe2\x8a\xb4"),
+ ENTITY_DEF("bsemi", 8271, "\xe2\x81\x8f"),
+ ENTITY_DEF("rdquor", 8221, "\xe2\x80\x9d"),
+ ENTITY_DEF("Utilde", 360, "\xc5\xa8"),
+ ENTITY_DEF("bsol", 92, "\x5c"),
+ ENTITY_DEF("risingdotseq", 8787, "\xe2\x89\x93"),
+ ENTITY_DEF("ultri", 9720, "\xe2\x97\xb8"),
+ ENTITY_DEF("rhov", 1009, "\xcf\xb1"),
+ ENTITY_DEF("TildeEqual", 8771, "\xe2\x89\x83"),
+ ENTITY_DEF("jukcy", 1108, "\xd1\x94"),
+ ENTITY_DEF("perp", 8869, "\xe2\x8a\xa5"),
+ ENTITY_DEF("capbrcup", 10825, "\xe2\xa9\x89"),
+ ENTITY_DEF("ltrie", 8884, "\xe2\x8a\xb4"),
+ ENTITY_DEF("LessTilde", 8818, "\xe2\x89\xb2"),
+ ENTITY_DEF("popf", 120161, "\xf0\x9d\x95\xa1"),
+ ENTITY_DEF("dbkarow", 10511, "\xe2\xa4\x8f"),
+ ENTITY_DEF("roang", 10221, "\xe2\x9f\xad"),
+ ENTITY_DEF_HEUR("brvbar", 166, "\xc2\xa6"),
+ ENTITY_DEF("CenterDot", 183, "\xc2\xb7"),
+ ENTITY_DEF("notindot", 8949, "\xe2\x8b\xb5\xcc\xb8"),
+ ENTITY_DEF("supmult", 10946, "\xe2\xab\x82"),
+ ENTITY_DEF("multimap", 8888, "\xe2\x8a\xb8"),
+ ENTITY_DEF_HEUR("frac34", 190, "\xc2\xbe"),
+ ENTITY_DEF("mapsto", 8614, "\xe2\x86\xa6"),
+ ENTITY_DEF("flat", 9837, "\xe2\x99\xad"),
+ ENTITY_DEF("updownarrow", 8597, "\xe2\x86\x95"),
+ ENTITY_DEF("gne", 10888, "\xe2\xaa\x88"),
+ ENTITY_DEF("nrarrc", 10547, "\xe2\xa4\xb3\xcc\xb8"),
+ ENTITY_DEF("suphsol", 10185, "\xe2\x9f\x89"),
+ ENTITY_DEF("nGtv", 8811, "\xe2\x89\xab\xcc\xb8"),
+ ENTITY_DEF("hopf", 120153, "\xf0\x9d\x95\x99"),
+ ENTITY_DEF("pointint", 10773, "\xe2\xa8\x95"),
+ ENTITY_DEF("glj", 10916, "\xe2\xaa\xa4"),
+ ENTITY_DEF("LeftDoubleBracket", 10214, "\xe2\x9f\xa6"),
+ ENTITY_DEF("NotSupersetEqual", 8841, "\xe2\x8a\x89"),
+ ENTITY_DEF("dot", 729, "\xcb\x99"),
+ ENTITY_DEF("tbrk", 9140, "\xe2\x8e\xb4"),
+ ENTITY_DEF("LeftUpDownVector", 10577, "\xe2\xa5\x91"),
+ ENTITY_DEF_HEUR("uml", 168, "\xc2\xa8"),
+ ENTITY_DEF("bbrk", 9141, "\xe2\x8e\xb5"),
+ ENTITY_DEF("nearrow", 8599, "\xe2\x86\x97"),
+ ENTITY_DEF("backsimeq", 8909, "\xe2\x8b\x8d"),
+ ENTITY_DEF("dblac", 733, "\xcb\x9d"),
+ ENTITY_DEF("circleddash", 8861, "\xe2\x8a\x9d"),
+ ENTITY_DEF("ldsh", 8626, "\xe2\x86\xb2"),
+ ENTITY_DEF("sce", 10928, "\xe2\xaa\xb0"),
+ ENTITY_DEF("angst", 197, "\xc3\x85"),
+ ENTITY_DEF_HEUR("yen", 165, "\xc2\xa5"),
+ ENTITY_DEF("nsupE", 10950, "\xe2\xab\x86\xcc\xb8"),
+ ENTITY_DEF("Uscr", 119984, "\xf0\x9d\x92\xb0"),
+ ENTITY_DEF("subplus", 10943, "\xe2\xaa\xbf"),
+ ENTITY_DEF("nleqq", 8806, "\xe2\x89\xa6\xcc\xb8"),
+ ENTITY_DEF("nprcue", 8928, "\xe2\x8b\xa0"),
+ ENTITY_DEF("Ocirc", 212, "\xc3\x94"),
+ ENTITY_DEF("disin", 8946, "\xe2\x8b\xb2"),
+ ENTITY_DEF("EqualTilde", 8770, "\xe2\x89\x82"),
+ ENTITY_DEF("YUcy", 1070, "\xd0\xae"),
+ ENTITY_DEF("Kscr", 119974, "\xf0\x9d\x92\xa6"),
+ ENTITY_DEF("lg", 8822, "\xe2\x89\xb6"),
+ ENTITY_DEF("nLeftrightarrow", 8654, "\xe2\x87\x8e"),
+ ENTITY_DEF("eplus", 10865, "\xe2\xa9\xb1"),
+ ENTITY_DEF("les", 10877, "\xe2\xa9\xbd"),
+ ENTITY_DEF("sfr", 120112, "\xf0\x9d\x94\xb0"),
+ ENTITY_DEF("HumpDownHump", 8782, "\xe2\x89\x8e"),
+ ENTITY_DEF("Fouriertrf", 8497, "\xe2\x84\xb1"),
+ ENTITY_DEF("Updownarrow", 8661, "\xe2\x87\x95"),
+ ENTITY_DEF("nrarr", 8603, "\xe2\x86\x9b"),
+ ENTITY_DEF("radic", 8730, "\xe2\x88\x9a"),
+ ENTITY_DEF("gnap", 10890, "\xe2\xaa\x8a"),
+ ENTITY_DEF("zeta", 950, "\xce\xb6"),
+ ENTITY_DEF("Qscr", 119980, "\xf0\x9d\x92\xac"),
+ ENTITY_DEF("NotRightTriangleEqual", 8941, "\xe2\x8b\xad"),
+ ENTITY_DEF("nshortmid", 8740, "\xe2\x88\xa4"),
+ ENTITY_DEF("SHCHcy", 1065, "\xd0\xa9"),
+ ENTITY_DEF("piv", 982, "\xcf\x96"),
+ ENTITY_DEF("angmsdaa", 10664, "\xe2\xa6\xa8"),
+ ENTITY_DEF("curlywedge", 8911, "\xe2\x8b\x8f"),
+ ENTITY_DEF("sqcaps", 8851, "\xe2\x8a\x93\xef\xb8\x80"),
+ ENTITY_DEF("sum", 8721, "\xe2\x88\x91"),
+ ENTITY_DEF("rarrtl", 8611, "\xe2\x86\xa3"),
+ ENTITY_DEF("gescc", 10921, "\xe2\xaa\xa9"),
+ ENTITY_DEF("sup", 8835, "\xe2\x8a\x83"),
+ ENTITY_DEF("smid", 8739, "\xe2\x88\xa3"),
+ ENTITY_DEF("cularr", 8630, "\xe2\x86\xb6"),
+ ENTITY_DEF("olcross", 10683, "\xe2\xa6\xbb"),
+ ENTITY_DEF_HEUR("GT", 62, "\x3e"),
+ ENTITY_DEF("scap", 10936, "\xe2\xaa\xb8"),
+ ENTITY_DEF("capcup", 10823, "\xe2\xa9\x87"),
+ ENTITY_DEF("NotSquareSubsetEqual", 8930, "\xe2\x8b\xa2"),
+ ENTITY_DEF("uhblk", 9600, "\xe2\x96\x80"),
+ ENTITY_DEF("latail", 10521, "\xe2\xa4\x99"),
+ ENTITY_DEF("smtes", 10924, "\xe2\xaa\xac\xef\xb8\x80"),
+ ENTITY_DEF("RoundImplies", 10608, "\xe2\xa5\xb0"),
+ ENTITY_DEF("wreath", 8768, "\xe2\x89\x80"),
+ ENTITY_DEF("curlyvee", 8910, "\xe2\x8b\x8e"),
+ ENTITY_DEF("uscr", 120010, "\xf0\x9d\x93\x8a"),
+ ENTITY_DEF("nleftrightarrow", 8622, "\xe2\x86\xae"),
+ ENTITY_DEF("ucy", 1091, "\xd1\x83"),
+ ENTITY_DEF("nvge", 8805, "\xe2\x89\xa5\xe2\x83\x92"),
+ ENTITY_DEF("bnot", 8976, "\xe2\x8c\x90"),
+ ENTITY_DEF("alefsym", 8501, "\xe2\x84\xb5"),
+ ENTITY_DEF("star", 9734, "\xe2\x98\x86"),
+ ENTITY_DEF("boxHd", 9572, "\xe2\x95\xa4"),
+ ENTITY_DEF("vsubnE", 10955, "\xe2\xab\x8b\xef\xb8\x80"),
+ ENTITY_DEF("Popf", 8473, "\xe2\x84\x99"),
+ ENTITY_DEF("simgE", 10912, "\xe2\xaa\xa0"),
+ ENTITY_DEF("upsilon", 965, "\xcf\x85"),
+ ENTITY_DEF("NoBreak", 8288, "\xe2\x81\xa0"),
+ ENTITY_DEF("realine", 8475, "\xe2\x84\x9b"),
+ ENTITY_DEF("frac38", 8540, "\xe2\x85\x9c"),
+ ENTITY_DEF("YAcy", 1071, "\xd0\xaf"),
+ ENTITY_DEF("bnequiv", 8801, "\xe2\x89\xa1\xe2\x83\xa5"),
+ ENTITY_DEF("cudarrr", 10549, "\xe2\xa4\xb5"),
+ ENTITY_DEF("lsime", 10893, "\xe2\xaa\x8d"),
+ ENTITY_DEF("lowbar", 95, "\x5f"),
+ ENTITY_DEF("utdot", 8944, "\xe2\x8b\xb0"),
+ ENTITY_DEF("ReverseElement", 8715, "\xe2\x88\x8b"),
+ ENTITY_DEF("nshortparallel", 8742, "\xe2\x88\xa6"),
+ ENTITY_DEF("DJcy", 1026, "\xd0\x82"),
+ ENTITY_DEF("nsube", 8840, "\xe2\x8a\x88"),
+ ENTITY_DEF("VDash", 8875, "\xe2\x8a\xab"),
+ ENTITY_DEF("Ncaron", 327, "\xc5\x87"),
+ ENTITY_DEF("LeftUpVector", 8639, "\xe2\x86\xbf"),
+ ENTITY_DEF("Kcy", 1050, "\xd0\x9a"),
+ ENTITY_DEF("NotLeftTriangleEqual", 8940, "\xe2\x8b\xac"),
+ ENTITY_DEF("nvHarr", 10500, "\xe2\xa4\x84"),
+ ENTITY_DEF("lotimes", 10804, "\xe2\xa8\xb4"),
+ ENTITY_DEF("RightFloor", 8971, "\xe2\x8c\x8b"),
+ ENTITY_DEF("succ", 8827, "\xe2\x89\xbb"),
+ ENTITY_DEF("Ucy", 1059, "\xd0\xa3"),
+ ENTITY_DEF("darr", 8595, "\xe2\x86\x93"),
+ ENTITY_DEF("lbarr", 10508, "\xe2\xa4\x8c"),
+ ENTITY_DEF("xfr", 120117, "\xf0\x9d\x94\xb5"),
+ ENTITY_DEF("zopf", 120171, "\xf0\x9d\x95\xab"),
+ ENTITY_DEF("Phi", 934, "\xce\xa6"),
+ ENTITY_DEF("ord", 10845, "\xe2\xa9\x9d"),
+ ENTITY_DEF("iinfin", 10716, "\xe2\xa7\x9c"),
+ ENTITY_DEF("Xfr", 120091, "\xf0\x9d\x94\x9b"),
+ ENTITY_DEF("qint", 10764, "\xe2\xa8\x8c"),
+ ENTITY_DEF("Upsilon", 933, "\xce\xa5"),
+ ENTITY_DEF("NotSubset", 8834, "\xe2\x8a\x82\xe2\x83\x92"),
+ ENTITY_DEF("gfr", 120100, "\xf0\x9d\x94\xa4"),
+ ENTITY_DEF("notnivb", 8958, "\xe2\x8b\xbe"),
+ ENTITY_DEF("Afr", 120068, "\xf0\x9d\x94\x84"),
+ ENTITY_DEF_HEUR("ge", 8805, "\xe2\x89\xa5"),
+ ENTITY_DEF_HEUR("iexcl", 161, "\xc2\xa1"),
+ ENTITY_DEF("dfr", 120097, "\xf0\x9d\x94\xa1"),
+ ENTITY_DEF("rsaquo", 8250, "\xe2\x80\xba"),
+ ENTITY_DEF("xcap", 8898, "\xe2\x8b\x82"),
+ ENTITY_DEF("Jopf", 120129, "\xf0\x9d\x95\x81"),
+ ENTITY_DEF("Hstrok", 294, "\xc4\xa6"),
+ ENTITY_DEF("ldca", 10550, "\xe2\xa4\xb6"),
+ ENTITY_DEF("lmoust", 9136, "\xe2\x8e\xb0"),
+ ENTITY_DEF("wcirc", 373, "\xc5\xb5"),
+ ENTITY_DEF("DownRightVector", 8641, "\xe2\x87\x81"),
+ ENTITY_DEF("LessFullEqual", 8806, "\xe2\x89\xa6"),
+ ENTITY_DEF("dotsquare", 8865, "\xe2\x8a\xa1"),
+ ENTITY_DEF("zhcy", 1078, "\xd0\xb6"),
+ ENTITY_DEF("mDDot", 8762, "\xe2\x88\xba"),
+ ENTITY_DEF("Prime", 8243, "\xe2\x80\xb3"),
+ ENTITY_DEF("prec", 8826, "\xe2\x89\xba"),
+ ENTITY_DEF("swnwar", 10538, "\xe2\xa4\xaa"),
+ ENTITY_DEF_HEUR("COPY", 169, "\xc2\xa9"),
+ ENTITY_DEF("cong", 8773, "\xe2\x89\x85"),
+ ENTITY_DEF("sacute", 347, "\xc5\x9b"),
+ ENTITY_DEF("Nopf", 8469, "\xe2\x84\x95"),
+ ENTITY_DEF("it", 8290, "\xe2\x81\xa2"),
+ ENTITY_DEF("SOFTcy", 1068, "\xd0\xac"),
+ ENTITY_DEF("uuarr", 8648, "\xe2\x87\x88"),
+ ENTITY_DEF("iota", 953, "\xce\xb9"),
+ ENTITY_DEF("notinE", 8953, "\xe2\x8b\xb9\xcc\xb8"),
+ ENTITY_DEF("jfr", 120103, "\xf0\x9d\x94\xa7"),
+ ENTITY_DEF_HEUR("QUOT", 34, "\x22"),
+ ENTITY_DEF("vsupnE", 10956, "\xe2\xab\x8c\xef\xb8\x80"),
+ ENTITY_DEF_HEUR("igrave", 236, "\xc3\xac"),
+ ENTITY_DEF("bsim", 8765, "\xe2\x88\xbd"),
+ ENTITY_DEF("npreceq", 10927, "\xe2\xaa\xaf\xcc\xb8"),
+ ENTITY_DEF("zcaron", 382, "\xc5\xbe"),
+ ENTITY_DEF("DD", 8517, "\xe2\x85\x85"),
+ ENTITY_DEF("gamma", 947, "\xce\xb3"),
+ ENTITY_DEF("homtht", 8763, "\xe2\x88\xbb"),
+ ENTITY_DEF("NonBreakingSpace", 160, "\xc2\xa0"),
+ ENTITY_DEF("Proportion", 8759, "\xe2\x88\xb7"),
+ ENTITY_DEF("nedot", 8784, "\xe2\x89\x90\xcc\xb8"),
+ ENTITY_DEF("nabla", 8711, "\xe2\x88\x87"),
+ ENTITY_DEF("ac", 8766, "\xe2\x88\xbe"),
+ ENTITY_DEF("nsupe", 8841, "\xe2\x8a\x89"),
+ ENTITY_DEF("ell", 8467, "\xe2\x84\x93"),
+ ENTITY_DEF("boxvR", 9566, "\xe2\x95\x9e"),
+ ENTITY_DEF("LowerRightArrow", 8600, "\xe2\x86\x98"),
+ ENTITY_DEF("boxHu", 9575, "\xe2\x95\xa7"),
+ ENTITY_DEF("lE", 8806, "\xe2\x89\xa6"),
+ ENTITY_DEF("dzigrarr", 10239, "\xe2\x9f\xbf"),
+ ENTITY_DEF("rfloor", 8971, "\xe2\x8c\x8b"),
+ ENTITY_DEF("gneq", 10888, "\xe2\xaa\x88"),
+ ENTITY_DEF("rightleftharpoons", 8652, "\xe2\x87\x8c"),
+ ENTITY_DEF("gtquest", 10876, "\xe2\xa9\xbc"),
+ ENTITY_DEF("searhk", 10533, "\xe2\xa4\xa5"),
+ ENTITY_DEF("gesdoto", 10882, "\xe2\xaa\x82"),
+ ENTITY_DEF("cross", 10007, "\xe2\x9c\x97"),
+ ENTITY_DEF("rdquo", 8221, "\xe2\x80\x9d"),
+ ENTITY_DEF("sqsupset", 8848, "\xe2\x8a\x90"),
+ ENTITY_DEF("divonx", 8903, "\xe2\x8b\x87"),
+ ENTITY_DEF("lat", 10923, "\xe2\xaa\xab"),
+ ENTITY_DEF("rmoustache", 9137, "\xe2\x8e\xb1"),
+ ENTITY_DEF("succapprox", 10936, "\xe2\xaa\xb8"),
+ ENTITY_DEF("nhpar", 10994, "\xe2\xab\xb2"),
+ ENTITY_DEF("sharp", 9839, "\xe2\x99\xaf"),
+ ENTITY_DEF("lrcorner", 8991, "\xe2\x8c\x9f"),
+ ENTITY_DEF("Vscr", 119985, "\xf0\x9d\x92\xb1"),
+ ENTITY_DEF("varsigma", 962, "\xcf\x82"),
+ ENTITY_DEF("bsolb", 10693, "\xe2\xa7\x85"),
+ ENTITY_DEF("cupcap", 10822, "\xe2\xa9\x86"),
+ ENTITY_DEF("leftrightarrow", 8596, "\xe2\x86\x94"),
+ ENTITY_DEF("LeftTee", 8867, "\xe2\x8a\xa3"),
+ ENTITY_DEF("Sqrt", 8730, "\xe2\x88\x9a"),
+ ENTITY_DEF("Odblac", 336, "\xc5\x90"),
+ ENTITY_DEF("ocir", 8858, "\xe2\x8a\x9a"),
+ ENTITY_DEF("eqslantless", 10901, "\xe2\xaa\x95"),
+ ENTITY_DEF("supedot", 10948, "\xe2\xab\x84"),
+ ENTITY_DEF("intercal", 8890, "\xe2\x8a\xba"),
+ ENTITY_DEF("Gbreve", 286, "\xc4\x9e"),
+ ENTITY_DEF("xrArr", 10233, "\xe2\x9f\xb9"),
+ ENTITY_DEF("NotTildeEqual", 8772, "\xe2\x89\x84"),
+ ENTITY_DEF("Bfr", 120069, "\xf0\x9d\x94\x85"),
+ ENTITY_DEF_HEUR("Iuml", 207, "\xc3\x8f"),
+ ENTITY_DEF("leg", 8922, "\xe2\x8b\x9a"),
+ ENTITY_DEF("boxhU", 9576, "\xe2\x95\xa8"),
+ ENTITY_DEF("Gopf", 120126, "\xf0\x9d\x94\xbe"),
+ ENTITY_DEF("af", 8289, "\xe2\x81\xa1"),
+ ENTITY_DEF("xwedge", 8896, "\xe2\x8b\x80"),
+ ENTITY_DEF("precapprox", 10935, "\xe2\xaa\xb7"),
+ ENTITY_DEF("lcedil", 316, "\xc4\xbc"),
+ ENTITY_DEF("between", 8812, "\xe2\x89\xac"),
+ ENTITY_DEF_HEUR("Oslash", 216, "\xc3\x98"),
+ ENTITY_DEF("breve", 728, "\xcb\x98"),
+ ENTITY_DEF("caps", 8745, "\xe2\x88\xa9\xef\xb8\x80"),
+ ENTITY_DEF("vangrt", 10652, "\xe2\xa6\x9c"),
+ ENTITY_DEF("lagran", 8466, "\xe2\x84\x92"),
+ ENTITY_DEF("kopf", 120156, "\xf0\x9d\x95\x9c"),
+ ENTITY_DEF("ReverseUpEquilibrium", 10607, "\xe2\xa5\xaf"),
+ ENTITY_DEF("nlsim", 8820, "\xe2\x89\xb4"),
+ ENTITY_DEF("Cap", 8914, "\xe2\x8b\x92"),
+ ENTITY_DEF("angmsdac", 10666, "\xe2\xa6\xaa"),
+ ENTITY_DEF("iocy", 1105, "\xd1\x91"),
+ ENTITY_DEF("seswar", 10537, "\xe2\xa4\xa9"),
+ ENTITY_DEF("dzcy", 1119, "\xd1\x9f"),
+ ENTITY_DEF("nsubset", 8834, "\xe2\x8a\x82\xe2\x83\x92"),
+ ENTITY_DEF("cup", 8746, "\xe2\x88\xaa"),
+ ENTITY_DEF("npar", 8742, "\xe2\x88\xa6"),
+ ENTITY_DEF("late", 10925, "\xe2\xaa\xad"),
+ ENTITY_DEF("plussim", 10790, "\xe2\xa8\xa6"),
+ ENTITY_DEF("Darr", 8609, "\xe2\x86\xa1"),
+ ENTITY_DEF("nexist", 8708, "\xe2\x88\x84"),
+ ENTITY_DEF_HEUR("cent", 162, "\xc2\xa2"),
+ ENTITY_DEF("khcy", 1093, "\xd1\x85"),
+ ENTITY_DEF("smallsetminus", 8726, "\xe2\x88\x96"),
+ ENTITY_DEF("ycirc", 375, "\xc5\xb7"),
+ ENTITY_DEF("lharu", 8636, "\xe2\x86\xbc"),
+ ENTITY_DEF("upuparrows", 8648, "\xe2\x87\x88"),
+ ENTITY_DEF("sigmaf", 962, "\xcf\x82"),
+ ENTITY_DEF("nltri", 8938, "\xe2\x8b\xaa"),
+ ENTITY_DEF("mstpos", 8766, "\xe2\x88\xbe"),
+ ENTITY_DEF("Zopf", 8484, "\xe2\x84\xa4"),
+ ENTITY_DEF("dwangle", 10662, "\xe2\xa6\xa6"),
+ ENTITY_DEF("bowtie", 8904, "\xe2\x8b\x88"),
+ ENTITY_DEF("Dfr", 120071, "\xf0\x9d\x94\x87"),
+ ENTITY_DEF_HEUR("iacute", 237, "\xc3\xad"),
+ ENTITY_DEF("njcy", 1114, "\xd1\x9a"),
+ ENTITY_DEF("cfr", 120096, "\xf0\x9d\x94\xa0"),
+ ENTITY_DEF("TripleDot", 8411, "\xe2\x83\x9b"),
+ ENTITY_DEF("Or", 10836, "\xe2\xa9\x94"),
+ ENTITY_DEF("blk34", 9619, "\xe2\x96\x93"),
+ ENTITY_DEF("equiv", 8801, "\xe2\x89\xa1"),
+ ENTITY_DEF("fflig", 64256, "\xef\xac\x80"),
+ ENTITY_DEF("Rang", 10219, "\xe2\x9f\xab"),
+ ENTITY_DEF("Wopf", 120142, "\xf0\x9d\x95\x8e"),
+ ENTITY_DEF("boxUl", 9564, "\xe2\x95\x9c"),
+ ENTITY_DEF_HEUR("frac12", 189, "\xc2\xbd"),
+ ENTITY_DEF("clubs", 9827, "\xe2\x99\xa3"),
+ ENTITY_DEF("amalg", 10815, "\xe2\xa8\xbf"),
+ ENTITY_DEF("Lang", 10218, "\xe2\x9f\xaa"),
+ ENTITY_DEF("asymp", 8776, "\xe2\x89\x88"),
+ ENTITY_DEF("models", 8871, "\xe2\x8a\xa7"),
+ ENTITY_DEF("emptyset", 8709, "\xe2\x88\x85"),
+ ENTITY_DEF("Tscr", 119983, "\xf0\x9d\x92\xaf"),
+ ENTITY_DEF("nleftarrow", 8602, "\xe2\x86\x9a"),
+ ENTITY_DEF("Omacr", 332, "\xc5\x8c"),
+ ENTITY_DEF("gtrarr", 10616, "\xe2\xa5\xb8"),
+ ENTITY_DEF("excl", 33, "\x21"),
+ ENTITY_DEF("rarrw", 8605, "\xe2\x86\x9d"),
+ ENTITY_DEF("abreve", 259, "\xc4\x83"),
+ ENTITY_DEF("CircleTimes", 8855, "\xe2\x8a\x97"),
+ ENTITY_DEF("aopf", 120146, "\xf0\x9d\x95\x92"),
+ ENTITY_DEF("eqvparsl", 10725, "\xe2\xa7\xa5"),
+ ENTITY_DEF("boxv", 9474, "\xe2\x94\x82"),
+ ENTITY_DEF("SuchThat", 8715, "\xe2\x88\x8b"),
+ ENTITY_DEF("varphi", 981, "\xcf\x95"),
+ ENTITY_DEF("Ropf", 8477, "\xe2\x84\x9d"),
+ ENTITY_DEF("rscr", 120007, "\xf0\x9d\x93\x87"),
+ ENTITY_DEF("Rrightarrow", 8667, "\xe2\x87\x9b"),
+ ENTITY_DEF("equest", 8799, "\xe2\x89\x9f"),
+ ENTITY_DEF_HEUR("ntilde", 241, "\xc3\xb1"),
+ ENTITY_DEF("Escr", 8496, "\xe2\x84\xb0"),
+ ENTITY_DEF("Lopf", 120131, "\xf0\x9d\x95\x83"),
+ ENTITY_DEF("GreaterGreater", 10914, "\xe2\xaa\xa2"),
+ ENTITY_DEF("pluscir", 10786, "\xe2\xa8\xa2"),
+ ENTITY_DEF("nsupset", 8835, "\xe2\x8a\x83\xe2\x83\x92"),
+ ENTITY_DEF("uArr", 8657, "\xe2\x87\x91"),
+ ENTITY_DEF("nwarhk", 10531, "\xe2\xa4\xa3"),
+ ENTITY_DEF("Ycirc", 374, "\xc5\xb6"),
+ ENTITY_DEF("tdot", 8411, "\xe2\x83\x9b"),
+ ENTITY_DEF("circledS", 9416, "\xe2\x93\x88"),
+ ENTITY_DEF("lhard", 8637, "\xe2\x86\xbd"),
+ ENTITY_DEF("iukcy", 1110, "\xd1\x96"),
+ ENTITY_DEF("PrecedesSlantEqual", 8828, "\xe2\x89\xbc"),
+ ENTITY_DEF("Sfr", 120086, "\xf0\x9d\x94\x96"),
+ ENTITY_DEF("egs", 10902, "\xe2\xaa\x96"),
+ ENTITY_DEF("oelig", 339, "\xc5\x93"),
+ ENTITY_DEF("bigtriangledown", 9661, "\xe2\x96\xbd"),
+ ENTITY_DEF("EmptyVerySmallSquare", 9643, "\xe2\x96\xab"),
+ ENTITY_DEF("Backslash", 8726, "\xe2\x88\x96"),
+ ENTITY_DEF("nscr", 120003, "\xf0\x9d\x93\x83"),
+ ENTITY_DEF("uogon", 371, "\xc5\xb3"),
+ ENTITY_DEF("circeq", 8791, "\xe2\x89\x97"),
+ ENTITY_DEF("check", 10003, "\xe2\x9c\x93"),
+ ENTITY_DEF("Sup", 8913, "\xe2\x8b\x91"),
+ ENTITY_DEF("Rcaron", 344, "\xc5\x98"),
+ ENTITY_DEF("lneqq", 8808, "\xe2\x89\xa8"),
+ ENTITY_DEF("lrhar", 8651, "\xe2\x87\x8b"),
+ ENTITY_DEF("ulcorn", 8988, "\xe2\x8c\x9c"),
+ ENTITY_DEF("timesd", 10800, "\xe2\xa8\xb0"),
+ ENTITY_DEF("Sum", 8721, "\xe2\x88\x91"),
+ ENTITY_DEF("varpropto", 8733, "\xe2\x88\x9d"),
+ ENTITY_DEF("Lcaron", 317, "\xc4\xbd"),
+ ENTITY_DEF("lbrkslu", 10637, "\xe2\xa6\x8d"),
+ ENTITY_DEF_HEUR("AElig", 198, "\xc3\x86"),
+ ENTITY_DEF("varr", 8597, "\xe2\x86\x95"),
+ ENTITY_DEF("nvinfin", 10718, "\xe2\xa7\x9e"),
+ ENTITY_DEF("leq", 8804, "\xe2\x89\xa4"),
+ ENTITY_DEF("biguplus", 10756, "\xe2\xa8\x84"),
+ ENTITY_DEF("rpar", 41, "\x29"),
+ ENTITY_DEF("eng", 331, "\xc5\x8b"),
+ ENTITY_DEF("NegativeThinSpace", 8203, "\xe2\x80\x8b"),
+ ENTITY_DEF("lesssim", 8818, "\xe2\x89\xb2"),
+ ENTITY_DEF("lBarr", 10510, "\xe2\xa4\x8e"),
+ ENTITY_DEF("LeftUpTeeVector", 10592, "\xe2\xa5\xa0"),
+ ENTITY_DEF("gnE", 8809, "\xe2\x89\xa9"),
+ ENTITY_DEF("efr", 120098, "\xf0\x9d\x94\xa2"),
+ ENTITY_DEF("barvee", 8893, "\xe2\x8a\xbd"),
+ ENTITY_DEF("ee", 8519, "\xe2\x85\x87"),
+ ENTITY_DEF("Uogon", 370, "\xc5\xb2"),
+ ENTITY_DEF("gnapprox", 10890, "\xe2\xaa\x8a"),
+ ENTITY_DEF("olcir", 10686, "\xe2\xa6\xbe"),
+ ENTITY_DEF("boxUL", 9565, "\xe2\x95\x9d"),
+ ENTITY_DEF("Gg", 8921, "\xe2\x8b\x99"),
+ ENTITY_DEF("CloseCurlyQuote", 8217, "\xe2\x80\x99"),
+ ENTITY_DEF("leftharpoondown", 8637, "\xe2\x86\xbd"),
+ ENTITY_DEF("vfr", 120115, "\xf0\x9d\x94\xb3"),
+ ENTITY_DEF("gvertneqq", 8809, "\xe2\x89\xa9\xef\xb8\x80"),
+ ENTITY_DEF_HEUR("ouml", 246, "\xc3\xb6"),
+ ENTITY_DEF("raemptyv", 10675, "\xe2\xa6\xb3"),
+ ENTITY_DEF("Zcaron", 381, "\xc5\xbd"),
+ ENTITY_DEF("scE", 10932, "\xe2\xaa\xb4"),
+ ENTITY_DEF("boxvh", 9532, "\xe2\x94\xbc"),
+ ENTITY_DEF("ominus", 8854, "\xe2\x8a\x96"),
+ ENTITY_DEF("oopf", 120160, "\xf0\x9d\x95\xa0"),
+ ENTITY_DEF("nsucceq", 10928, "\xe2\xaa\xb0\xcc\xb8"),
+ ENTITY_DEF("RBarr", 10512, "\xe2\xa4\x90"),
+ ENTITY_DEF("iprod", 10812, "\xe2\xa8\xbc"),
+ ENTITY_DEF("lvnE", 8808, "\xe2\x89\xa8\xef\xb8\x80"),
+ ENTITY_DEF("andand", 10837, "\xe2\xa9\x95"),
+ ENTITY_DEF("upharpoonright", 8638, "\xe2\x86\xbe"),
+ ENTITY_DEF("ncongdot", 10861, "\xe2\xa9\xad\xcc\xb8"),
+ ENTITY_DEF("drcrop", 8972, "\xe2\x8c\x8c"),
+ ENTITY_DEF("nsimeq", 8772, "\xe2\x89\x84"),
+ ENTITY_DEF("subsub", 10965, "\xe2\xab\x95"),
+ ENTITY_DEF("hardcy", 1098, "\xd1\x8a"),
+ ENTITY_DEF("leqslant", 10877, "\xe2\xa9\xbd"),
+ ENTITY_DEF("uharl", 8639, "\xe2\x86\xbf"),
+ ENTITY_DEF("expectation", 8496, "\xe2\x84\xb0"),
+ ENTITY_DEF("mdash", 8212, "\xe2\x80\x94"),
+ ENTITY_DEF("VerticalTilde", 8768, "\xe2\x89\x80"),
+ ENTITY_DEF("rdldhar", 10601, "\xe2\xa5\xa9"),
+ ENTITY_DEF("leftharpoonup", 8636, "\xe2\x86\xbc"),
+ ENTITY_DEF("mu", 956, "\xce\xbc"),
+ ENTITY_DEF("curarrm", 10556, "\xe2\xa4\xbc"),
+ ENTITY_DEF("Cdot", 266, "\xc4\x8a"),
+ ENTITY_DEF("NotTildeTilde", 8777, "\xe2\x89\x89"),
+ ENTITY_DEF("boxul", 9496, "\xe2\x94\x98"),
+ ENTITY_DEF("planckh", 8462, "\xe2\x84\x8e"),
+ ENTITY_DEF("CapitalDifferentialD", 8517, "\xe2\x85\x85"),
+ ENTITY_DEF("boxDL", 9559, "\xe2\x95\x97"),
+ ENTITY_DEF("cupbrcap", 10824, "\xe2\xa9\x88"),
+ ENTITY_DEF("boxdL", 9557, "\xe2\x95\x95"),
+ ENTITY_DEF("supe", 8839, "\xe2\x8a\x87"),
+ ENTITY_DEF("nvlt", 60, "\x3c\xe2\x83\x92"),
+ ENTITY_DEF("par", 8741, "\xe2\x88\xa5"),
+ ENTITY_DEF("InvisibleComma", 8291, "\xe2\x81\xa3"),
+ ENTITY_DEF("ring", 730, "\xcb\x9a"),
+ ENTITY_DEF("nvap", 8781, "\xe2\x89\x8d\xe2\x83\x92"),
+ ENTITY_DEF("veeeq", 8794, "\xe2\x89\x9a"),
+ ENTITY_DEF("Hfr", 8460, "\xe2\x84\x8c"),
+ ENTITY_DEF("dstrok", 273, "\xc4\x91"),
+ ENTITY_DEF("gesles", 10900, "\xe2\xaa\x94"),
+ ENTITY_DEF("dash", 8208, "\xe2\x80\x90"),
+ ENTITY_DEF("SHcy", 1064, "\xd0\xa8"),
+ ENTITY_DEF("congdot", 10861, "\xe2\xa9\xad"),
+ ENTITY_DEF("imagline", 8464, "\xe2\x84\x90"),
+ ENTITY_DEF("ncy", 1085, "\xd0\xbd"),
+ ENTITY_DEF("bigstar", 9733, "\xe2\x98\x85"),
+ ENTITY_DEF_HEUR("REG", 174, "\xc2\xae"),
+ ENTITY_DEF("triangleq", 8796, "\xe2\x89\x9c"),
+ ENTITY_DEF("rsqb", 93, "\x5d"),
+ ENTITY_DEF("ddarr", 8650, "\xe2\x87\x8a"),
+ ENTITY_DEF("csub", 10959, "\xe2\xab\x8f"),
+ ENTITY_DEF("quest", 63, "\x3f"),
+ ENTITY_DEF("Star", 8902, "\xe2\x8b\x86"),
+ ENTITY_DEF_HEUR("LT", 60, "\x3c"),
+ ENTITY_DEF("ncong", 8775, "\xe2\x89\x87"),
+ ENTITY_DEF("prnE", 10933, "\xe2\xaa\xb5"),
+ ENTITY_DEF("bigtriangleup", 9651, "\xe2\x96\xb3"),
+ ENTITY_DEF("Tilde", 8764, "\xe2\x88\xbc"),
+ ENTITY_DEF("ltrif", 9666, "\xe2\x97\x82"),
+ ENTITY_DEF("ldrdhar", 10599, "\xe2\xa5\xa7"),
+ ENTITY_DEF("lcaron", 318, "\xc4\xbe"),
+ ENTITY_DEF("equivDD", 10872, "\xe2\xa9\xb8"),
+ ENTITY_DEF("lHar", 10594, "\xe2\xa5\xa2"),
+ ENTITY_DEF("vBar", 10984, "\xe2\xab\xa8"),
+ ENTITY_DEF("Mopf", 120132, "\xf0\x9d\x95\x84"),
+ ENTITY_DEF("LeftArrow", 8592, "\xe2\x86\x90"),
+ ENTITY_DEF("Rho", 929, "\xce\xa1"),
+ ENTITY_DEF("Ccirc", 264, "\xc4\x88"),
+ ENTITY_DEF("ifr", 120102, "\xf0\x9d\x94\xa6"),
+ ENTITY_DEF("cacute", 263, "\xc4\x87"),
+ ENTITY_DEF("centerdot", 183, "\xc2\xb7"),
+ ENTITY_DEF("dollar", 36, "\x24"),
+ ENTITY_DEF("lang", 10216, "\xe2\x9f\xa8"),
+ ENTITY_DEF("curvearrowright", 8631, "\xe2\x86\xb7"),
+ ENTITY_DEF("half", 189, "\xc2\xbd"),
+ ENTITY_DEF("Ecy", 1069, "\xd0\xad"),
+ ENTITY_DEF("rcub", 125, "\x7d"),
+ ENTITY_DEF("rcy", 1088, "\xd1\x80"),
+ ENTITY_DEF("isins", 8948, "\xe2\x8b\xb4"),
+ ENTITY_DEF("bsolhsub", 10184, "\xe2\x9f\x88"),
+ ENTITY_DEF("boxuL", 9563, "\xe2\x95\x9b"),
+ ENTITY_DEF("shchcy", 1097, "\xd1\x89"),
+ ENTITY_DEF("cwconint", 8754, "\xe2\x88\xb2"),
+ ENTITY_DEF("euro", 8364, "\xe2\x82\xac"),
+ ENTITY_DEF("lesseqqgtr", 10891, "\xe2\xaa\x8b"),
+ ENTITY_DEF("sim", 8764, "\xe2\x88\xbc"),
+ ENTITY_DEF("rarrc", 10547, "\xe2\xa4\xb3"),
+ ENTITY_DEF("boxdl", 9488, "\xe2\x94\x90"),
+ ENTITY_DEF("Epsilon", 917, "\xce\x95"),
+ ENTITY_DEF("iiiint", 10764, "\xe2\xa8\x8c"),
+ ENTITY_DEF("Rightarrow", 8658, "\xe2\x87\x92"),
+ ENTITY_DEF("conint", 8750, "\xe2\x88\xae"),
+ ENTITY_DEF("boxDl", 9558, "\xe2\x95\x96"),
+ ENTITY_DEF("kappav", 1008, "\xcf\xb0"),
+ ENTITY_DEF("profsurf", 8979, "\xe2\x8c\x93"),
+ ENTITY_DEF_HEUR("auml", 228, "\xc3\xa4"),
+ ENTITY_DEF("heartsuit", 9829, "\xe2\x99\xa5"),
+ ENTITY_DEF_HEUR("eacute", 233, "\xc3\xa9"),
+ ENTITY_DEF_HEUR("gt", 62, "\x3e"),
+ ENTITY_DEF("Gcedil", 290, "\xc4\xa2"),
+ ENTITY_DEF("easter", 10862, "\xe2\xa9\xae"),
+ ENTITY_DEF("Tcy", 1058, "\xd0\xa2"),
+ ENTITY_DEF("swarrow", 8601, "\xe2\x86\x99"),
+ ENTITY_DEF("lopf", 120157, "\xf0\x9d\x95\x9d"),
+ ENTITY_DEF("Agrave", 192, "\xc3\x80"),
+ ENTITY_DEF("Aring", 197, "\xc3\x85"),
+ ENTITY_DEF("fpartint", 10765, "\xe2\xa8\x8d"),
+ ENTITY_DEF("xoplus", 10753, "\xe2\xa8\x81"),
+ ENTITY_DEF("LeftDownTeeVector", 10593, "\xe2\xa5\xa1"),
+ ENTITY_DEF("int", 8747, "\xe2\x88\xab"),
+ ENTITY_DEF("Zeta", 918, "\xce\x96"),
+ ENTITY_DEF("loz", 9674, "\xe2\x97\x8a"),
+ ENTITY_DEF("ncup", 10818, "\xe2\xa9\x82"),
+ ENTITY_DEF("napE", 10864, "\xe2\xa9\xb0\xcc\xb8"),
+ ENTITY_DEF("csup", 10960, "\xe2\xab\x90"),
+ ENTITY_DEF("Ncedil", 325, "\xc5\x85"),
+ ENTITY_DEF("cuwed", 8911, "\xe2\x8b\x8f"),
+ ENTITY_DEF("Dot", 168, "\xc2\xa8"),
+ ENTITY_DEF("SquareIntersection", 8851, "\xe2\x8a\x93"),
+ ENTITY_DEF("map", 8614, "\xe2\x86\xa6"),
+ ENTITY_DEF_HEUR("aelig", 230, "\xc3\xa6"),
+ ENTITY_DEF("RightArrow", 8594, "\xe2\x86\x92"),
+ ENTITY_DEF("rightharpoondown", 8641, "\xe2\x87\x81"),
+ ENTITY_DEF("bNot", 10989, "\xe2\xab\xad"),
+ ENTITY_DEF("nsccue", 8929, "\xe2\x8b\xa1"),
+ ENTITY_DEF("zigrarr", 8669, "\xe2\x87\x9d"),
+ ENTITY_DEF("Sacute", 346, "\xc5\x9a"),
+ ENTITY_DEF("orv", 10843, "\xe2\xa9\x9b"),
+ ENTITY_DEF("RightVectorBar", 10579, "\xe2\xa5\x93"),
+ ENTITY_DEF("nrarrw", 8605, "\xe2\x86\x9d\xcc\xb8"),
+ ENTITY_DEF("nbump", 8782, "\xe2\x89\x8e\xcc\xb8"),
+ ENTITY_DEF_HEUR("iquest", 191, "\xc2\xbf"),
+ ENTITY_DEF("wr", 8768, "\xe2\x89\x80"),
+ ENTITY_DEF("UpArrow", 8593, "\xe2\x86\x91"),
+ ENTITY_DEF("notinva", 8713, "\xe2\x88\x89"),
+ ENTITY_DEF("ddagger", 8225, "\xe2\x80\xa1"),
+ ENTITY_DEF("nLeftarrow", 8653, "\xe2\x87\x8d"),
+ ENTITY_DEF("rbbrk", 10099, "\xe2\x9d\xb3"),
+ ENTITY_DEF("RightTriangle", 8883, "\xe2\x8a\xb3"),
+ ENTITY_DEF("leqq", 8806, "\xe2\x89\xa6"),
+ ENTITY_DEF("Vert", 8214, "\xe2\x80\x96"),
+ ENTITY_DEF("gesl", 8923, "\xe2\x8b\x9b\xef\xb8\x80"),
+ ENTITY_DEF("LeftTeeVector", 10586, "\xe2\xa5\x9a"),
+ ENTITY_DEF("Union", 8899, "\xe2\x8b\x83"),
+ ENTITY_DEF("sc", 8827, "\xe2\x89\xbb"),
+ ENTITY_DEF("ofr", 120108, "\xf0\x9d\x94\xac"),
+ ENTITY_DEF("quatint", 10774, "\xe2\xa8\x96"),
+ ENTITY_DEF("apacir", 10863, "\xe2\xa9\xaf"),
+ ENTITY_DEF("profalar", 9006, "\xe2\x8c\xae"),
+ ENTITY_DEF("subsetneq", 8842, "\xe2\x8a\x8a"),
+ ENTITY_DEF("Vvdash", 8874, "\xe2\x8a\xaa"),
+ ENTITY_DEF("ohbar", 10677, "\xe2\xa6\xb5"),
+ ENTITY_DEF("Gt", 8811, "\xe2\x89\xab"),
+ ENTITY_DEF("exist", 8707, "\xe2\x88\x83"),
+ ENTITY_DEF("gtrapprox", 10886, "\xe2\xaa\x86"),
+ ENTITY_DEF_HEUR("euml", 235, "\xc3\xab"),
+ ENTITY_DEF("Equilibrium", 8652, "\xe2\x87\x8c"),
+ ENTITY_DEF_HEUR("aacute", 225, "\xc3\xa1"),
+ ENTITY_DEF("omid", 10678, "\xe2\xa6\xb6"),
+ ENTITY_DEF("loarr", 8701, "\xe2\x87\xbd"),
+ ENTITY_DEF("SucceedsSlantEqual", 8829, "\xe2\x89\xbd"),
+ ENTITY_DEF("angsph", 8738, "\xe2\x88\xa2"),
+ ENTITY_DEF("nsmid", 8740, "\xe2\x88\xa4"),
+ ENTITY_DEF("lsquor", 8218, "\xe2\x80\x9a"),
+ ENTITY_DEF("cemptyv", 10674, "\xe2\xa6\xb2"),
+ ENTITY_DEF("rAarr", 8667, "\xe2\x87\x9b"),
+ ENTITY_DEF("searr", 8600, "\xe2\x86\x98"),
+ ENTITY_DEF("complexes", 8450, "\xe2\x84\x82"),
+ ENTITY_DEF("UnderParenthesis", 9181, "\xe2\x8f\x9d"),
+ ENTITY_DEF("nparsl", 11005, "\xe2\xab\xbd\xe2\x83\xa5"),
+ ENTITY_DEF("Lacute", 313, "\xc4\xb9"),
+ ENTITY_DEF_HEUR("deg", 176, "\xc2\xb0"),
+ ENTITY_DEF("Racute", 340, "\xc5\x94"),
+ ENTITY_DEF("Verbar", 8214, "\xe2\x80\x96"),
+ ENTITY_DEF("sqcups", 8852, "\xe2\x8a\x94\xef\xb8\x80"),
+ ENTITY_DEF("Hopf", 8461, "\xe2\x84\x8d"),
+ ENTITY_DEF("naturals", 8469, "\xe2\x84\x95"),
+ ENTITY_DEF("Cedilla", 184, "\xc2\xb8"),
+ ENTITY_DEF("exponentiale", 8519, "\xe2\x85\x87"),
+ ENTITY_DEF("vnsup", 8835, "\xe2\x8a\x83\xe2\x83\x92"),
+ ENTITY_DEF("leftrightarrows", 8646, "\xe2\x87\x86"),
+ ENTITY_DEF("Laplacetrf", 8466, "\xe2\x84\x92"),
+ ENTITY_DEF("vartriangleright", 8883, "\xe2\x8a\xb3"),
+ ENTITY_DEF("rtri", 9657, "\xe2\x96\xb9"),
+ ENTITY_DEF("gE", 8807, "\xe2\x89\xa7"),
+ ENTITY_DEF("SmallCircle", 8728, "\xe2\x88\x98"),
+ ENTITY_DEF("diamondsuit", 9830, "\xe2\x99\xa6"),
+ ENTITY_DEF_HEUR("Otilde", 213, "\xc3\x95"),
+ ENTITY_DEF("lneq", 10887, "\xe2\xaa\x87"),
+ ENTITY_DEF("lesdoto", 10881, "\xe2\xaa\x81"),
+ ENTITY_DEF("ltquest", 10875, "\xe2\xa9\xbb"),
+ ENTITY_DEF("thinsp", 8201, "\xe2\x80\x89"),
+ ENTITY_DEF("barwed", 8965, "\xe2\x8c\x85"),
+ ENTITY_DEF("elsdot", 10903, "\xe2\xaa\x97"),
+ ENTITY_DEF("circ", 710, "\xcb\x86"),
+ ENTITY_DEF("ni", 8715, "\xe2\x88\x8b"),
+ ENTITY_DEF("mlcp", 10971, "\xe2\xab\x9b"),
+ ENTITY_DEF("Vdash", 8873, "\xe2\x8a\xa9"),
+ ENTITY_DEF("ShortRightArrow", 8594, "\xe2\x86\x92"),
+ ENTITY_DEF("upharpoonleft", 8639, "\xe2\x86\xbf"),
+ ENTITY_DEF("UnderBracket", 9141, "\xe2\x8e\xb5"),
+ ENTITY_DEF("rAtail", 10524, "\xe2\xa4\x9c"),
+ ENTITY_DEF("iopf", 120154, "\xf0\x9d\x95\x9a"),
+ ENTITY_DEF("longleftarrow", 10229, "\xe2\x9f\xb5"),
+ ENTITY_DEF("Zacute", 377, "\xc5\xb9"),
+ ENTITY_DEF("duhar", 10607, "\xe2\xa5\xaf"),
+ ENTITY_DEF("Mfr", 120080, "\xf0\x9d\x94\x90"),
+ ENTITY_DEF("prnap", 10937, "\xe2\xaa\xb9"),
+ ENTITY_DEF("eqcirc", 8790, "\xe2\x89\x96"),
+ ENTITY_DEF("rarrlp", 8620, "\xe2\x86\xac"),
+ ENTITY_DEF("le", 8804, "\xe2\x89\xa4"),
+ ENTITY_DEF("Oscr", 119978, "\xf0\x9d\x92\xaa"),
+ ENTITY_DEF("langd", 10641, "\xe2\xa6\x91"),
+ ENTITY_DEF("Ucirc", 219, "\xc3\x9b"),
+ ENTITY_DEF("precnapprox", 10937, "\xe2\xaa\xb9"),
+ ENTITY_DEF("succcurlyeq", 8829, "\xe2\x89\xbd"),
+ ENTITY_DEF("Tau", 932, "\xce\xa4"),
+ ENTITY_DEF("larr", 8592, "\xe2\x86\x90"),
+ ENTITY_DEF("neArr", 8663, "\xe2\x87\x97"),
+ ENTITY_DEF("subsim", 10951, "\xe2\xab\x87"),
+ ENTITY_DEF("DScy", 1029, "\xd0\x85"),
+ ENTITY_DEF("preccurlyeq", 8828, "\xe2\x89\xbc"),
+ ENTITY_DEF("NotLessLess", 8810, "\xe2\x89\xaa\xcc\xb8"),
+ ENTITY_DEF("succnapprox", 10938, "\xe2\xaa\xba"),
+ ENTITY_DEF("prcue", 8828, "\xe2\x89\xbc"),
+ ENTITY_DEF("Downarrow", 8659, "\xe2\x87\x93"),
+ ENTITY_DEF("angmsdah", 10671, "\xe2\xa6\xaf"),
+ ENTITY_DEF("Emacr", 274, "\xc4\x92"),
+ ENTITY_DEF("lsh", 8624, "\xe2\x86\xb0"),
+ ENTITY_DEF("simne", 8774, "\xe2\x89\x86"),
+ ENTITY_DEF("Bumpeq", 8782, "\xe2\x89\x8e"),
+ ENTITY_DEF("RightUpTeeVector", 10588, "\xe2\xa5\x9c"),
+ ENTITY_DEF("Sigma", 931, "\xce\xa3"),
+ ENTITY_DEF("nvltrie", 8884, "\xe2\x8a\xb4\xe2\x83\x92"),
+ ENTITY_DEF("lfr", 120105, "\xf0\x9d\x94\xa9"),
+ ENTITY_DEF("emsp13", 8196, "\xe2\x80\x84"),
+ ENTITY_DEF("parsl", 11005, "\xe2\xab\xbd"),
+ ENTITY_DEF_HEUR("ucirc", 251, "\xc3\xbb"),
+ ENTITY_DEF("gsiml", 10896, "\xe2\xaa\x90"),
+ ENTITY_DEF("xsqcup", 10758, "\xe2\xa8\x86"),
+ ENTITY_DEF("Omicron", 927, "\xce\x9f"),
+ ENTITY_DEF("gsime", 10894, "\xe2\xaa\x8e"),
+ ENTITY_DEF("circlearrowleft", 8634, "\xe2\x86\xba"),
+ ENTITY_DEF("sqsupe", 8850, "\xe2\x8a\x92"),
+ ENTITY_DEF("supE", 10950, "\xe2\xab\x86"),
+ ENTITY_DEF("dlcrop", 8973, "\xe2\x8c\x8d"),
+ ENTITY_DEF("RightDownTeeVector", 10589, "\xe2\xa5\x9d"),
+ ENTITY_DEF("Colone", 10868, "\xe2\xa9\xb4"),
+ ENTITY_DEF("awconint", 8755, "\xe2\x88\xb3"),
+ ENTITY_DEF("smte", 10924, "\xe2\xaa\xac"),
+ ENTITY_DEF("lEg", 10891, "\xe2\xaa\x8b"),
+ ENTITY_DEF("circledast", 8859, "\xe2\x8a\x9b"),
+ ENTITY_DEF("ecolon", 8789, "\xe2\x89\x95"),
+ ENTITY_DEF("rect", 9645, "\xe2\x96\xad"),
+ ENTITY_DEF("Equal", 10869, "\xe2\xa9\xb5"),
+ ENTITY_DEF("nwnear", 10535, "\xe2\xa4\xa7"),
+ ENTITY_DEF("capdot", 10816, "\xe2\xa9\x80"),
+ ENTITY_DEF("straightphi", 981, "\xcf\x95"),
+ ENTITY_DEF("forkv", 10969, "\xe2\xab\x99"),
+ ENTITY_DEF("ZHcy", 1046, "\xd0\x96"),
+ ENTITY_DEF("Element", 8712, "\xe2\x88\x88"),
+ ENTITY_DEF("rthree", 8908, "\xe2\x8b\x8c"),
+ ENTITY_DEF("vzigzag", 10650, "\xe2\xa6\x9a"),
+ ENTITY_DEF("hybull", 8259, "\xe2\x81\x83"),
+ ENTITY_DEF("intprod", 10812, "\xe2\xa8\xbc"),
+ ENTITY_DEF("HumpEqual", 8783, "\xe2\x89\x8f"),
+ ENTITY_DEF("bigsqcup", 10758, "\xe2\xa8\x86"),
+ ENTITY_DEF("mp", 8723, "\xe2\x88\x93"),
+ ENTITY_DEF("lescc", 10920, "\xe2\xaa\xa8"),
+ ENTITY_DEF("NotPrecedes", 8832, "\xe2\x8a\x80"),
+ ENTITY_DEF("wedge", 8743, "\xe2\x88\xa7"),
+ ENTITY_DEF("Supset", 8913, "\xe2\x8b\x91"),
+ ENTITY_DEF("pm", 177, "\xc2\xb1"),
+ ENTITY_DEF("kfr", 120104, "\xf0\x9d\x94\xa8"),
+ ENTITY_DEF("ufisht", 10622, "\xe2\xa5\xbe"),
+ ENTITY_DEF("ecaron", 283, "\xc4\x9b"),
+ ENTITY_DEF("chcy", 1095, "\xd1\x87"),
+ ENTITY_DEF("Esim", 10867, "\xe2\xa9\xb3"),
+ ENTITY_DEF("fltns", 9649, "\xe2\x96\xb1"),
+ ENTITY_DEF("nsce", 10928, "\xe2\xaa\xb0\xcc\xb8"),
+ ENTITY_DEF("hookrightarrow", 8618, "\xe2\x86\xaa"),
+ ENTITY_DEF("semi", 59, "\x3b"),
+ ENTITY_DEF("ges", 10878, "\xe2\xa9\xbe"),
+ ENTITY_DEF("approxeq", 8778, "\xe2\x89\x8a"),
+ ENTITY_DEF("rarrsim", 10612, "\xe2\xa5\xb4"),
+ ENTITY_DEF("boxhD", 9573, "\xe2\x95\xa5"),
+ ENTITY_DEF("varpi", 982, "\xcf\x96"),
+ ENTITY_DEF("larrb", 8676, "\xe2\x87\xa4"),
+ ENTITY_DEF("copf", 120148, "\xf0\x9d\x95\x94"),
+ ENTITY_DEF("Dopf", 120123, "\xf0\x9d\x94\xbb"),
+ ENTITY_DEF("LeftVector", 8636, "\xe2\x86\xbc"),
+ ENTITY_DEF("iff", 8660, "\xe2\x87\x94"),
+ ENTITY_DEF("lnap", 10889, "\xe2\xaa\x89"),
+ ENTITY_DEF("NotGreaterFullEqual", 8807, "\xe2\x89\xa7\xcc\xb8"),
+ ENTITY_DEF("varrho", 1009, "\xcf\xb1"),
+ ENTITY_DEF("NotSucceeds", 8833, "\xe2\x8a\x81"),
+ ENTITY_DEF("ltrPar", 10646, "\xe2\xa6\x96"),
+ ENTITY_DEF("nlE", 8806, "\xe2\x89\xa6\xcc\xb8"),
+ ENTITY_DEF("Zfr", 8488, "\xe2\x84\xa8"),
+ ENTITY_DEF("LeftArrowBar", 8676, "\xe2\x87\xa4"),
+ ENTITY_DEF("boxplus", 8862, "\xe2\x8a\x9e"),
+ ENTITY_DEF("sqsube", 8849, "\xe2\x8a\x91"),
+ ENTITY_DEF("Re", 8476, "\xe2\x84\x9c"),
+ ENTITY_DEF("Wfr", 120090, "\xf0\x9d\x94\x9a"),
+ ENTITY_DEF("epsi", 949, "\xce\xb5"),
+ ENTITY_DEF("oacute", 243, "\xc3\xb3"),
+ ENTITY_DEF("bdquo", 8222, "\xe2\x80\x9e"),
+ ENTITY_DEF("wscr", 120012, "\xf0\x9d\x93\x8c"),
+ ENTITY_DEF("bullet", 8226, "\xe2\x80\xa2"),
+ ENTITY_DEF("frown", 8994, "\xe2\x8c\xa2"),
+ ENTITY_DEF("siml", 10909, "\xe2\xaa\x9d"),
+ ENTITY_DEF("Rarr", 8608, "\xe2\x86\xa0"),
+ ENTITY_DEF("Scaron", 352, "\xc5\xa0"),
+ ENTITY_DEF("gtreqqless", 10892, "\xe2\xaa\x8c"),
+ ENTITY_DEF("Larr", 8606, "\xe2\x86\x9e"),
+ ENTITY_DEF("notniva", 8716, "\xe2\x88\x8c"),
+ ENTITY_DEF("gg", 8811, "\xe2\x89\xab"),
+ ENTITY_DEF("phmmat", 8499, "\xe2\x84\xb3"),
+ ENTITY_DEF("boxVL", 9571, "\xe2\x95\xa3"),
+ ENTITY_DEF("sigmav", 962, "\xcf\x82"),
+ ENTITY_DEF("order", 8500, "\xe2\x84\xb4"),
+ ENTITY_DEF("subsup", 10963, "\xe2\xab\x93"),
+ ENTITY_DEF("afr", 120094, "\xf0\x9d\x94\x9e"),
+ ENTITY_DEF("lbrace", 123, "\x7b"),
+ ENTITY_DEF("urcorn", 8989, "\xe2\x8c\x9d"),
+ ENTITY_DEF("Im", 8465, "\xe2\x84\x91"),
+ ENTITY_DEF("CounterClockwiseContourIntegral", 8755, "\xe2\x88\xb3"),
+ ENTITY_DEF("lne", 10887, "\xe2\xaa\x87"),
+ ENTITY_DEF("chi", 967, "\xcf\x87"),
+ ENTITY_DEF("cudarrl", 10552, "\xe2\xa4\xb8"),
+ ENTITY_DEF("ang", 8736, "\xe2\x88\xa0"),
+ ENTITY_DEF("isindot", 8949, "\xe2\x8b\xb5"),
+ ENTITY_DEF("Lfr", 120079, "\xf0\x9d\x94\x8f"),
+ ENTITY_DEF("Rsh", 8625, "\xe2\x86\xb1"),
+ ENTITY_DEF("Ocy", 1054, "\xd0\x9e"),
+ ENTITY_DEF("nvrArr", 10499, "\xe2\xa4\x83"),
+ ENTITY_DEF("otimes", 8855, "\xe2\x8a\x97"),
+ ENTITY_DEF("eqslantgtr", 10902, "\xe2\xaa\x96"),
+ ENTITY_DEF("Rfr", 8476, "\xe2\x84\x9c"),
+ ENTITY_DEF("blacktriangleleft", 9666, "\xe2\x97\x82"),
+ ENTITY_DEF("Lsh", 8624, "\xe2\x86\xb0"),
+ ENTITY_DEF("boxvr", 9500, "\xe2\x94\x9c"),
+ ENTITY_DEF("scedil", 351, "\xc5\x9f"),
+ ENTITY_DEF_HEUR("iuml", 239, "\xc3\xaf"),
+ ENTITY_DEF("NJcy", 1034, "\xd0\x8a"),
+ ENTITY_DEF("Dagger", 8225, "\xe2\x80\xa1"),
+ ENTITY_DEF("rarrap", 10613, "\xe2\xa5\xb5"),
+ ENTITY_DEF("udblac", 369, "\xc5\xb1"),
+ ENTITY_DEF("Sopf", 120138, "\xf0\x9d\x95\x8a"),
+ ENTITY_DEF("scnsim", 8937, "\xe2\x8b\xa9"),
+ ENTITY_DEF("hbar", 8463, "\xe2\x84\x8f"),
+ ENTITY_DEF("frac15", 8533, "\xe2\x85\x95"),
+ ENTITY_DEF_HEUR("sup3", 179, "\xc2\xb3"),
+ ENTITY_DEF("NegativeThickSpace", 8203, "\xe2\x80\x8b"),
+ ENTITY_DEF("npr", 8832, "\xe2\x8a\x80"),
+ ENTITY_DEF("doteq", 8784, "\xe2\x89\x90"),
+ ENTITY_DEF("subrarr", 10617, "\xe2\xa5\xb9"),
+ ENTITY_DEF("SquareSubset", 8847, "\xe2\x8a\x8f"),
+ ENTITY_DEF("vprop", 8733, "\xe2\x88\x9d"),
+ ENTITY_DEF("OpenCurlyQuote", 8216, "\xe2\x80\x98"),
+ ENTITY_DEF("supseteq", 8839, "\xe2\x8a\x87"),
+ ENTITY_DEF("nRightarrow", 8655, "\xe2\x87\x8f"),
+ ENTITY_DEF("Longleftarrow", 10232, "\xe2\x9f\xb8"),
+ ENTITY_DEF("lsquo", 8216, "\xe2\x80\x98"),
+ ENTITY_DEF("hstrok", 295, "\xc4\xa7"),
+ ENTITY_DEF("NotTilde", 8769, "\xe2\x89\x81"),
+ ENTITY_DEF("ogt", 10689, "\xe2\xa7\x81"),
+ ENTITY_DEF("block", 9608, "\xe2\x96\x88"),
+ ENTITY_DEF("minusd", 8760, "\xe2\x88\xb8"),
+ ENTITY_DEF("esdot", 8784, "\xe2\x89\x90"),
+ ENTITY_DEF("nsim", 8769, "\xe2\x89\x81"),
+ ENTITY_DEF("scsim", 8831, "\xe2\x89\xbf"),
+ ENTITY_DEF("boxVl", 9570, "\xe2\x95\xa2"),
+ ENTITY_DEF("ltimes", 8905, "\xe2\x8b\x89"),
+ ENTITY_DEF("thkap", 8776, "\xe2\x89\x88"),
+ ENTITY_DEF("vnsub", 8834, "\xe2\x8a\x82\xe2\x83\x92"),
+ ENTITY_DEF("thetasym", 977, "\xcf\x91"),
+ ENTITY_DEF("eopf", 120150, "\xf0\x9d\x95\x96"),
+ ENTITY_DEF("image", 8465, "\xe2\x84\x91"),
+ ENTITY_DEF("doteqdot", 8785, "\xe2\x89\x91"),
+ ENTITY_DEF("Udblac", 368, "\xc5\xb0"),
+ ENTITY_DEF("gnsim", 8935, "\xe2\x8b\xa7"),
+ ENTITY_DEF("yicy", 1111, "\xd1\x97"),
+ ENTITY_DEF("vopf", 120167, "\xf0\x9d\x95\xa7"),
+ ENTITY_DEF("DDotrahd", 10513, "\xe2\xa4\x91"),
+ ENTITY_DEF("Iota", 921, "\xce\x99"),
+ ENTITY_DEF("GJcy", 1027, "\xd0\x83"),
+ ENTITY_DEF("rightthreetimes", 8908, "\xe2\x8b\x8c"),
+ ENTITY_DEF("nrtri", 8939, "\xe2\x8b\xab"),
+ ENTITY_DEF("TildeFullEqual", 8773, "\xe2\x89\x85"),
+ ENTITY_DEF("Dcaron", 270, "\xc4\x8e"),
+ ENTITY_DEF("ccaron", 269, "\xc4\x8d"),
+ ENTITY_DEF("lacute", 314, "\xc4\xba"),
+ ENTITY_DEF("VerticalBar", 8739, "\xe2\x88\xa3"),
+ ENTITY_DEF("Igrave", 204, "\xc3\x8c"),
+ ENTITY_DEF("boxH", 9552, "\xe2\x95\x90"),
+ ENTITY_DEF("Pfr", 120083, "\xf0\x9d\x94\x93"),
+ ENTITY_DEF("equals", 61, "\x3d"),
+ ENTITY_DEF("rbrack", 93, "\x5d"),
+ ENTITY_DEF("OverParenthesis", 9180, "\xe2\x8f\x9c"),
+ ENTITY_DEF("in", 8712, "\xe2\x88\x88"),
+ ENTITY_DEF("llcorner", 8990, "\xe2\x8c\x9e"),
+ ENTITY_DEF("mcomma", 10793, "\xe2\xa8\xa9"),
+ ENTITY_DEF("NotGreater", 8815, "\xe2\x89\xaf"),
+ ENTITY_DEF("midcir", 10992, "\xe2\xab\xb0"),
+ ENTITY_DEF("Edot", 278, "\xc4\x96"),
+ ENTITY_DEF("oplus", 8853, "\xe2\x8a\x95"),
+ ENTITY_DEF("geqq", 8807, "\xe2\x89\xa7"),
+ ENTITY_DEF("curvearrowleft", 8630, "\xe2\x86\xb6"),
+ ENTITY_DEF("Poincareplane", 8460, "\xe2\x84\x8c"),
+ ENTITY_DEF("yscr", 120014, "\xf0\x9d\x93\x8e"),
+ ENTITY_DEF("ccaps", 10829, "\xe2\xa9\x8d"),
+ ENTITY_DEF("rpargt", 10644, "\xe2\xa6\x94"),
+ ENTITY_DEF("topfork", 10970, "\xe2\xab\x9a"),
+ ENTITY_DEF("Gamma", 915, "\xce\x93"),
+ ENTITY_DEF("umacr", 363, "\xc5\xab"),
+ ENTITY_DEF("frac13", 8531, "\xe2\x85\x93"),
+ ENTITY_DEF("cirfnint", 10768, "\xe2\xa8\x90"),
+ ENTITY_DEF("xlArr", 10232, "\xe2\x9f\xb8"),
+ ENTITY_DEF("digamma", 989, "\xcf\x9d"),
+ ENTITY_DEF("Hat", 94, "\x5e"),
+ ENTITY_DEF("lates", 10925, "\xe2\xaa\xad\xef\xb8\x80"),
+ ENTITY_DEF("lgE", 10897, "\xe2\xaa\x91"),
+ ENTITY_DEF("commat", 64, "\x40"),
+ ENTITY_DEF("NotPrecedesSlantEqual", 8928, "\xe2\x8b\xa0"),
+ ENTITY_DEF("phone", 9742, "\xe2\x98\x8e"),
+ ENTITY_DEF("Ecirc", 202, "\xc3\x8a"),
+ ENTITY_DEF_HEUR("lt", 60, "\x3c"),
+ ENTITY_DEF("intcal", 8890, "\xe2\x8a\xba"),
+ ENTITY_DEF("xdtri", 9661, "\xe2\x96\xbd"),
+ ENTITY_DEF("Abreve", 258, "\xc4\x82"),
+ ENTITY_DEF("gopf", 120152, "\xf0\x9d\x95\x98"),
+ ENTITY_DEF("Xopf", 120143, "\xf0\x9d\x95\x8f"),
+ ENTITY_DEF("Iacute", 205, "\xc3\x8d"),
+ ENTITY_DEF("Aopf", 120120, "\xf0\x9d\x94\xb8"),
+ ENTITY_DEF("gbreve", 287, "\xc4\x9f"),
+ ENTITY_DEF("nleq", 8816, "\xe2\x89\xb0"),
+ ENTITY_DEF("xopf", 120169, "\xf0\x9d\x95\xa9"),
+ ENTITY_DEF("SquareSupersetEqual", 8850, "\xe2\x8a\x92"),
+ ENTITY_DEF("NotLessTilde", 8820, "\xe2\x89\xb4"),
+ ENTITY_DEF("SubsetEqual", 8838, "\xe2\x8a\x86"),
+ ENTITY_DEF("Sc", 10940, "\xe2\xaa\xbc"),
+ ENTITY_DEF("sdote", 10854, "\xe2\xa9\xa6"),
+ ENTITY_DEF("loplus", 10797, "\xe2\xa8\xad"),
+ ENTITY_DEF("zfr", 120119, "\xf0\x9d\x94\xb7"),
+ ENTITY_DEF("subseteqq", 10949, "\xe2\xab\x85"),
+ ENTITY_DEF("Vdashl", 10982, "\xe2\xab\xa6"),
+ ENTITY_DEF("integers", 8484, "\xe2\x84\xa4"),
+ ENTITY_DEF("Umacr", 362, "\xc5\xaa"),
+ ENTITY_DEF("dopf", 120149, "\xf0\x9d\x95\x95"),
+ ENTITY_DEF("RightDownVectorBar", 10581, "\xe2\xa5\x95"),
+ ENTITY_DEF("angmsdaf", 10669, "\xe2\xa6\xad"),
+ ENTITY_DEF("Jfr", 120077, "\xf0\x9d\x94\x8d"),
+ ENTITY_DEF("bernou", 8492, "\xe2\x84\xac"),
+ ENTITY_DEF("lceil", 8968, "\xe2\x8c\x88"),
+ ENTITY_DEF("nvsim", 8764, "\xe2\x88\xbc\xe2\x83\x92"),
+ ENTITY_DEF("NotSucceedsSlantEqual", 8929, "\xe2\x8b\xa1"),
+ ENTITY_DEF("hearts", 9829, "\xe2\x99\xa5"),
+ ENTITY_DEF("vee", 8744, "\xe2\x88\xa8"),
+ ENTITY_DEF("LJcy", 1033, "\xd0\x89"),
+ ENTITY_DEF("nlt", 8814, "\xe2\x89\xae"),
+ ENTITY_DEF("because", 8757, "\xe2\x88\xb5"),
+ ENTITY_DEF("hairsp", 8202, "\xe2\x80\x8a"),
+ ENTITY_DEF("comma", 44, "\x2c"),
+ ENTITY_DEF("iecy", 1077, "\xd0\xb5"),
+ ENTITY_DEF("npre", 10927, "\xe2\xaa\xaf\xcc\xb8"),
+ ENTITY_DEF("NotSquareSubset", 8847, "\xe2\x8a\x8f\xcc\xb8"),
+ ENTITY_DEF("mscr", 120002, "\xf0\x9d\x93\x82"),
+ ENTITY_DEF("jopf", 120155, "\xf0\x9d\x95\x9b"),
+ ENTITY_DEF("bumpE", 10926, "\xe2\xaa\xae"),
+ ENTITY_DEF("thicksim", 8764, "\xe2\x88\xbc"),
+ ENTITY_DEF("Nfr", 120081, "\xf0\x9d\x94\x91"),
+ ENTITY_DEF("yucy", 1102, "\xd1\x8e"),
+ ENTITY_DEF("notinvc", 8950, "\xe2\x8b\xb6"),
+ ENTITY_DEF("lstrok", 322, "\xc5\x82"),
+ ENTITY_DEF("robrk", 10215, "\xe2\x9f\xa7"),
+ ENTITY_DEF("LeftTriangleBar", 10703, "\xe2\xa7\x8f"),
+ ENTITY_DEF("hksearow", 10533, "\xe2\xa4\xa5"),
+ ENTITY_DEF("bigcap", 8898, "\xe2\x8b\x82"),
+ ENTITY_DEF("udhar", 10606, "\xe2\xa5\xae"),
+ ENTITY_DEF("Yscr", 119988, "\xf0\x9d\x92\xb4"),
+ ENTITY_DEF("smeparsl", 10724, "\xe2\xa7\xa4"),
+ ENTITY_DEF("NotLess", 8814, "\xe2\x89\xae"),
+ ENTITY_DEF("dcaron", 271, "\xc4\x8f"),
+ ENTITY_DEF("ange", 10660, "\xe2\xa6\xa4"),
+ ENTITY_DEF("dHar", 10597, "\xe2\xa5\xa5"),
+ ENTITY_DEF("UpperRightArrow", 8599, "\xe2\x86\x97"),
+ ENTITY_DEF("trpezium", 9186, "\xe2\x8f\xa2"),
+ ENTITY_DEF("boxminus", 8863, "\xe2\x8a\x9f"),
+ ENTITY_DEF("notni", 8716, "\xe2\x88\x8c"),
+ ENTITY_DEF("dtrif", 9662, "\xe2\x96\xbe"),
+ ENTITY_DEF("nhArr", 8654, "\xe2\x87\x8e"),
+ ENTITY_DEF("larrpl", 10553, "\xe2\xa4\xb9"),
+ ENTITY_DEF("simeq", 8771, "\xe2\x89\x83"),
+ ENTITY_DEF("geqslant", 10878, "\xe2\xa9\xbe"),
+ ENTITY_DEF("RightUpVectorBar", 10580, "\xe2\xa5\x94"),
+ ENTITY_DEF("nsc", 8833, "\xe2\x8a\x81"),
+ ENTITY_DEF("div", 247, "\xc3\xb7"),
+ ENTITY_DEF("orslope", 10839, "\xe2\xa9\x97"),
+ ENTITY_DEF("lparlt", 10643, "\xe2\xa6\x93"),
+ ENTITY_DEF("trie", 8796, "\xe2\x89\x9c"),
+ ENTITY_DEF("cirmid", 10991, "\xe2\xab\xaf"),
+ ENTITY_DEF("wp", 8472, "\xe2\x84\x98"),
+ ENTITY_DEF("dagger", 8224, "\xe2\x80\xa0"),
+ ENTITY_DEF("utri", 9653, "\xe2\x96\xb5"),
+ ENTITY_DEF("supnE", 10956, "\xe2\xab\x8c"),
+ ENTITY_DEF("eg", 10906, "\xe2\xaa\x9a"),
+ ENTITY_DEF("LeftDownVector", 8643, "\xe2\x87\x83"),
+ ENTITY_DEF("NotLessEqual", 8816, "\xe2\x89\xb0"),
+ ENTITY_DEF("Bopf", 120121, "\xf0\x9d\x94\xb9"),
+ ENTITY_DEF("LongLeftRightArrow", 10231, "\xe2\x9f\xb7"),
+ ENTITY_DEF("Gfr", 120074, "\xf0\x9d\x94\x8a"),
+ ENTITY_DEF("sqsubseteq", 8849, "\xe2\x8a\x91"),
+ ENTITY_DEF_HEUR("ograve", 242, "\xc3\xb2"),
+ ENTITY_DEF("larrhk", 8617, "\xe2\x86\xa9"),
+ ENTITY_DEF("sigma", 963, "\xcf\x83"),
+ ENTITY_DEF("NotSquareSupersetEqual", 8931, "\xe2\x8b\xa3"),
+ ENTITY_DEF("gvnE", 8809, "\xe2\x89\xa9\xef\xb8\x80"),
+ ENTITY_DEF("timesbar", 10801, "\xe2\xa8\xb1"),
+ ENTITY_DEF("Iukcy", 1030, "\xd0\x86"),
+ ENTITY_DEF("bscr", 119991, "\xf0\x9d\x92\xb7"),
+ ENTITY_DEF("Exists", 8707, "\xe2\x88\x83"),
+ ENTITY_DEF("tscr", 120009, "\xf0\x9d\x93\x89"),
+ ENTITY_DEF("tcy", 1090, "\xd1\x82"),
+ ENTITY_DEF("nwarr", 8598, "\xe2\x86\x96"),
+ ENTITY_DEF("hoarr", 8703, "\xe2\x87\xbf"),
+ ENTITY_DEF("lnapprox", 10889, "\xe2\xaa\x89"),
+ ENTITY_DEF("nu", 957, "\xce\xbd"),
+ ENTITY_DEF("bcy", 1073, "\xd0\xb1"),
+ ENTITY_DEF("ndash", 8211, "\xe2\x80\x93"),
+ ENTITY_DEF("smt", 10922, "\xe2\xaa\xaa"),
+ ENTITY_DEF("scaron", 353, "\xc5\xa1"),
+ ENTITY_DEF("IOcy", 1025, "\xd0\x81"),
+ ENTITY_DEF("Ifr", 8465, "\xe2\x84\x91"),
+ ENTITY_DEF("cularrp", 10557, "\xe2\xa4\xbd"),
+ ENTITY_DEF("lvertneqq", 8808, "\xe2\x89\xa8\xef\xb8\x80"),
+ ENTITY_DEF("nlarr", 8602, "\xe2\x86\x9a"),
+ ENTITY_DEF("colon", 58, "\x3a"),
+ ENTITY_DEF("ddotseq", 10871, "\xe2\xa9\xb7"),
+ ENTITY_DEF("zacute", 378, "\xc5\xba"),
+ ENTITY_DEF("DoubleVerticalBar", 8741, "\xe2\x88\xa5"),
+ ENTITY_DEF("larrfs", 10525, "\xe2\xa4\x9d"),
+ ENTITY_DEF("NotExists", 8708, "\xe2\x88\x84"),
+ ENTITY_DEF("geq", 8805, "\xe2\x89\xa5"),
+ ENTITY_DEF("Ffr", 120073, "\xf0\x9d\x94\x89"),
+ ENTITY_DEF_HEUR("divide", 247, "\xc3\xb7"),
+ ENTITY_DEF("blank", 9251, "\xe2\x90\xa3"),
+ ENTITY_DEF("IEcy", 1045, "\xd0\x95"),
+ ENTITY_DEF_HEUR("ordm", 186, "\xc2\xba"),
+ ENTITY_DEF("fopf", 120151, "\xf0\x9d\x95\x97"),
+ ENTITY_DEF("ecir", 8790, "\xe2\x89\x96"),
+ ENTITY_DEF("complement", 8705, "\xe2\x88\x81"),
+ ENTITY_DEF("top", 8868, "\xe2\x8a\xa4"),
+ ENTITY_DEF("DoubleContourIntegral", 8751, "\xe2\x88\xaf"),
+ ENTITY_DEF("nisd", 8954, "\xe2\x8b\xba"),
+ ENTITY_DEF("bcong", 8780, "\xe2\x89\x8c"),
+ ENTITY_DEF("plusdu", 10789, "\xe2\xa8\xa5"),
+ ENTITY_DEF("TildeTilde", 8776, "\xe2\x89\x88"),
+ ENTITY_DEF("lnE", 8808, "\xe2\x89\xa8"),
+ ENTITY_DEF("DoubleLongRightArrow", 10233, "\xe2\x9f\xb9"),
+ ENTITY_DEF("nsubseteqq", 10949, "\xe2\xab\x85\xcc\xb8"),
+ ENTITY_DEF("DownTeeArrow", 8615, "\xe2\x86\xa7"),
+ ENTITY_DEF("Cscr", 119966, "\xf0\x9d\x92\x9e"),
+ ENTITY_DEF("NegativeVeryThinSpace", 8203, "\xe2\x80\x8b"),
+ ENTITY_DEF("emsp", 8195, "\xe2\x80\x83"),
+ ENTITY_DEF("vartriangleleft", 8882, "\xe2\x8a\xb2"),
+ ENTITY_DEF("ropar", 10630, "\xe2\xa6\x86"),
+ ENTITY_DEF("checkmark", 10003, "\xe2\x9c\x93"),
+ ENTITY_DEF("Ycy", 1067, "\xd0\xab"),
+ ENTITY_DEF("supset", 8835, "\xe2\x8a\x83"),
+ ENTITY_DEF("gneqq", 8809, "\xe2\x89\xa9"),
+ ENTITY_DEF("Lstrok", 321, "\xc5\x81"),
+ ENTITY_DEF_HEUR("AMP", 38, "\x26"),
+ ENTITY_DEF("acE", 8766, "\xe2\x88\xbe\xcc\xb3"),
+ ENTITY_DEF("sqsupseteq", 8850, "\xe2\x8a\x92"),
+ ENTITY_DEF("nle", 8816, "\xe2\x89\xb0"),
+ ENTITY_DEF("nesear", 10536, "\xe2\xa4\xa8"),
+ ENTITY_DEF("LeftDownVectorBar", 10585, "\xe2\xa5\x99"),
+ ENTITY_DEF("Integral", 8747, "\xe2\x88\xab"),
+ ENTITY_DEF("Beta", 914, "\xce\x92"),
+ ENTITY_DEF("nvdash", 8876, "\xe2\x8a\xac"),
+ ENTITY_DEF("nges", 10878, "\xe2\xa9\xbe\xcc\xb8"),
+ ENTITY_DEF("demptyv", 10673, "\xe2\xa6\xb1"),
+ ENTITY_DEF("eta", 951, "\xce\xb7"),
+ ENTITY_DEF("GreaterSlantEqual", 10878, "\xe2\xa9\xbe"),
+ ENTITY_DEF_HEUR("ccedil", 231, "\xc3\xa7"),
+ ENTITY_DEF("pfr", 120109, "\xf0\x9d\x94\xad"),
+ ENTITY_DEF("bbrktbrk", 9142, "\xe2\x8e\xb6"),
+ ENTITY_DEF("mcy", 1084, "\xd0\xbc"),
+ ENTITY_DEF("Not", 10988, "\xe2\xab\xac"),
+ ENTITY_DEF("qscr", 120006, "\xf0\x9d\x93\x86"),
+ ENTITY_DEF("zwj", 8205, "\xe2\x80\x8d"),
+ ENTITY_DEF("ntrianglerighteq", 8941, "\xe2\x8b\xad"),
+ ENTITY_DEF("permil", 8240, "\xe2\x80\xb0"),
+ ENTITY_DEF("squarf", 9642, "\xe2\x96\xaa"),
+ ENTITY_DEF("apos", 39, "\x27"),
+ ENTITY_DEF("lrm", 8206, "\xe2\x80\x8e"),
+ ENTITY_DEF("male", 9794, "\xe2\x99\x82"),
+ ENTITY_DEF_HEUR("agrave", 224, "\xc3\xa0"),
+ ENTITY_DEF("Lt", 8810, "\xe2\x89\xaa"),
+ ENTITY_DEF("capand", 10820, "\xe2\xa9\x84"),
+ ENTITY_DEF_HEUR("aring", 229, "\xc3\xa5"),
+ ENTITY_DEF("Jukcy", 1028, "\xd0\x84"),
+ ENTITY_DEF("bumpe", 8783, "\xe2\x89\x8f"),
+ ENTITY_DEF("dd", 8518, "\xe2\x85\x86"),
+ ENTITY_DEF("tscy", 1094, "\xd1\x86"),
+ ENTITY_DEF("oS", 9416, "\xe2\x93\x88"),
+ ENTITY_DEF("succeq", 10928, "\xe2\xaa\xb0"),
+ ENTITY_DEF("xharr", 10231, "\xe2\x9f\xb7"),
+ ENTITY_DEF("pluse", 10866, "\xe2\xa9\xb2"),
+ ENTITY_DEF("rfisht", 10621, "\xe2\xa5\xbd"),
+ ENTITY_DEF("HorizontalLine", 9472, "\xe2\x94\x80"),
+ ENTITY_DEF("DiacriticalAcute", 180, "\xc2\xb4"),
+ ENTITY_DEF("hfr", 120101, "\xf0\x9d\x94\xa5"),
+ ENTITY_DEF("preceq", 10927, "\xe2\xaa\xaf"),
+ ENTITY_DEF("rationals", 8474, "\xe2\x84\x9a"),
+ ENTITY_DEF_HEUR("Auml", 196, "\xc3\x84"),
+ ENTITY_DEF("LeftRightArrow", 8596, "\xe2\x86\x94"),
+ ENTITY_DEF("blacktriangleright", 9656, "\xe2\x96\xb8"),
+ ENTITY_DEF("dharr", 8642, "\xe2\x87\x82"),
+ ENTITY_DEF("isin", 8712, "\xe2\x88\x88"),
+ ENTITY_DEF("ldrushar", 10571, "\xe2\xa5\x8b"),
+ ENTITY_DEF("squ", 9633, "\xe2\x96\xa1"),
+ ENTITY_DEF("rbrksld", 10638, "\xe2\xa6\x8e"),
+ ENTITY_DEF("bigwedge", 8896, "\xe2\x8b\x80"),
+ ENTITY_DEF("swArr", 8665, "\xe2\x87\x99"),
+ ENTITY_DEF("IJlig", 306, "\xc4\xb2"),
+ ENTITY_DEF("harr", 8596, "\xe2\x86\x94"),
+ ENTITY_DEF("range", 10661, "\xe2\xa6\xa5"),
+ ENTITY_DEF("urtri", 9721, "\xe2\x97\xb9"),
+ ENTITY_DEF("NotVerticalBar", 8740, "\xe2\x88\xa4"),
+ ENTITY_DEF("ic", 8291, "\xe2\x81\xa3"),
+ ENTITY_DEF("solbar", 9023, "\xe2\x8c\xbf"),
+ ENTITY_DEF("approx", 8776, "\xe2\x89\x88"),
+ ENTITY_DEF("SquareSuperset", 8848, "\xe2\x8a\x90"),
+ ENTITY_DEF("numsp", 8199, "\xe2\x80\x87"),
+ ENTITY_DEF("nLt", 8810, "\xe2\x89\xaa\xe2\x83\x92"),
+ ENTITY_DEF("tilde", 732, "\xcb\x9c"),
+ ENTITY_DEF("rlarr", 8644, "\xe2\x87\x84"),
+ ENTITY_DEF("langle", 10216, "\xe2\x9f\xa8"),
+ ENTITY_DEF("nleqslant", 10877, "\xe2\xa9\xbd\xcc\xb8"),
+ ENTITY_DEF("Nacute", 323, "\xc5\x83"),
+ ENTITY_DEF("NotLeftTriangle", 8938, "\xe2\x8b\xaa"),
+ ENTITY_DEF("sopf", 120164, "\xf0\x9d\x95\xa4"),
+ ENTITY_DEF("xmap", 10236, "\xe2\x9f\xbc"),
+ ENTITY_DEF("supne", 8843, "\xe2\x8a\x8b"),
+ ENTITY_DEF("Int", 8748, "\xe2\x88\xac"),
+ ENTITY_DEF("nsupseteqq", 10950, "\xe2\xab\x86\xcc\xb8"),
+ ENTITY_DEF("circlearrowright", 8635, "\xe2\x86\xbb"),
+ ENTITY_DEF("NotCongruent", 8802, "\xe2\x89\xa2"),
+ ENTITY_DEF("Scedil", 350, "\xc5\x9e"),
+ ENTITY_DEF_HEUR("raquo", 187, "\xc2\xbb"),
+ ENTITY_DEF("ycy", 1099, "\xd1\x8b"),
+ ENTITY_DEF("notinvb", 8951, "\xe2\x8b\xb7"),
+ ENTITY_DEF("andv", 10842, "\xe2\xa9\x9a"),
+ ENTITY_DEF("nap", 8777, "\xe2\x89\x89"),
+ ENTITY_DEF("shcy", 1096, "\xd1\x88"),
+ ENTITY_DEF("ssetmn", 8726, "\xe2\x88\x96"),
+ ENTITY_DEF("downarrow", 8595, "\xe2\x86\x93"),
+ ENTITY_DEF("gesdotol", 10884, "\xe2\xaa\x84"),
+ ENTITY_DEF("Congruent", 8801, "\xe2\x89\xa1"),
+ ENTITY_DEF_HEUR("pound", 163, "\xc2\xa3"),
+ ENTITY_DEF("ZeroWidthSpace", 8203, "\xe2\x80\x8b"),
+ ENTITY_DEF("rdca", 10551, "\xe2\xa4\xb7"),
+ ENTITY_DEF("rmoust", 9137, "\xe2\x8e\xb1"),
+ ENTITY_DEF("zcy", 1079, "\xd0\xb7"),
+ ENTITY_DEF("Square", 9633, "\xe2\x96\xa1"),
+ ENTITY_DEF("subE", 10949, "\xe2\xab\x85"),
+ ENTITY_DEF("infintie", 10717, "\xe2\xa7\x9d"),
+ ENTITY_DEF("Cayleys", 8493, "\xe2\x84\xad"),
+ ENTITY_DEF("lsaquo", 8249, "\xe2\x80\xb9"),
+ ENTITY_DEF("realpart", 8476, "\xe2\x84\x9c"),
+ ENTITY_DEF("nprec", 8832, "\xe2\x8a\x80"),
+ ENTITY_DEF("RightTriangleBar", 10704, "\xe2\xa7\x90"),
+ ENTITY_DEF("Kopf", 120130, "\xf0\x9d\x95\x82"),
+ ENTITY_DEF("Ubreve", 364, "\xc5\xac"),
+ ENTITY_DEF("Uopf", 120140, "\xf0\x9d\x95\x8c"),
+ ENTITY_DEF("trianglelefteq", 8884, "\xe2\x8a\xb4"),
+ ENTITY_DEF("rotimes", 10805, "\xe2\xa8\xb5"),
+ ENTITY_DEF("qfr", 120110, "\xf0\x9d\x94\xae"),
+ ENTITY_DEF("gtcc", 10919, "\xe2\xaa\xa7"),
+ ENTITY_DEF("fnof", 402, "\xc6\x92"),
+ ENTITY_DEF("tritime", 10811, "\xe2\xa8\xbb"),
+ ENTITY_DEF("andslope", 10840, "\xe2\xa9\x98"),
+ ENTITY_DEF("harrw", 8621, "\xe2\x86\xad"),
+ ENTITY_DEF("NotSquareSuperset", 8848, "\xe2\x8a\x90\xcc\xb8"),
+ ENTITY_DEF("Amacr", 256, "\xc4\x80"),
+ ENTITY_DEF("OpenCurlyDoubleQuote", 8220, "\xe2\x80\x9c"),
+ ENTITY_DEF_HEUR("thorn", 254, "\xc3\xbe"),
+ ENTITY_DEF_HEUR("ordf", 170, "\xc2\xaa"),
+ ENTITY_DEF("natur", 9838, "\xe2\x99\xae"),
+ ENTITY_DEF("xi", 958, "\xce\xbe"),
+ ENTITY_DEF("infin", 8734, "\xe2\x88\x9e"),
+ ENTITY_DEF("nspar", 8742, "\xe2\x88\xa6"),
+ ENTITY_DEF("Jcy", 1049, "\xd0\x99"),
+ ENTITY_DEF("DownLeftTeeVector", 10590, "\xe2\xa5\x9e"),
+ ENTITY_DEF("rbarr", 10509, "\xe2\xa4\x8d"),
+ ENTITY_DEF("Xi", 926, "\xce\x9e"),
+ ENTITY_DEF("bull", 8226, "\xe2\x80\xa2"),
+ ENTITY_DEF("cuesc", 8927, "\xe2\x8b\x9f"),
+ ENTITY_DEF("backcong", 8780, "\xe2\x89\x8c"),
+ ENTITY_DEF("frac35", 8535, "\xe2\x85\x97"),
+ ENTITY_DEF("hscr", 119997, "\xf0\x9d\x92\xbd"),
+ ENTITY_DEF("LessEqualGreater", 8922, "\xe2\x8b\x9a"),
+ ENTITY_DEF("Implies", 8658, "\xe2\x87\x92"),
+ ENTITY_DEF("ETH", 208, "\xc3\x90"),
+ ENTITY_DEF_HEUR("Yacute", 221, "\xc3\x9d"),
+ ENTITY_DEF_HEUR("shy", 173, "\xc2\xad"),
+ ENTITY_DEF("Rarrtl", 10518, "\xe2\xa4\x96"),
+ ENTITY_DEF_HEUR("sup1", 185, "\xc2\xb9"),
+ ENTITY_DEF("reals", 8477, "\xe2\x84\x9d"),
+ ENTITY_DEF("blacklozenge", 10731, "\xe2\xa7\xab"),
+ ENTITY_DEF("ncedil", 326, "\xc5\x86"),
+ ENTITY_DEF("Lambda", 923, "\xce\x9b"),
+ ENTITY_DEF("uopf", 120166, "\xf0\x9d\x95\xa6"),
+ ENTITY_DEF("bigodot", 10752, "\xe2\xa8\x80"),
+ ENTITY_DEF("ubreve", 365, "\xc5\xad"),
+ ENTITY_DEF("drbkarow", 10512, "\xe2\xa4\x90"),
+ ENTITY_DEF("els", 10901, "\xe2\xaa\x95"),
+ ENTITY_DEF("shortparallel", 8741, "\xe2\x88\xa5"),
+ ENTITY_DEF("Pcy", 1055, "\xd0\x9f"),
+ ENTITY_DEF("dsol", 10742, "\xe2\xa7\xb6"),
+ ENTITY_DEF("supsim", 10952, "\xe2\xab\x88"),
+ ENTITY_DEF("Longrightarrow", 10233, "\xe2\x9f\xb9"),
+ ENTITY_DEF("ThickSpace", 8287, "\xe2\x81\x9f\xe2\x80\x8a"),
+ ENTITY_DEF("Itilde", 296, "\xc4\xa8"),
+ ENTITY_DEF("nparallel", 8742, "\xe2\x88\xa6"),
+ ENTITY_DEF("And", 10835, "\xe2\xa9\x93"),
+ ENTITY_DEF("boxhd", 9516, "\xe2\x94\xac"),
+ ENTITY_DEF("Dashv", 10980, "\xe2\xab\xa4"),
+ ENTITY_DEF("NotSuperset", 8835, "\xe2\x8a\x83\xe2\x83\x92"),
+ ENTITY_DEF("Eta", 919, "\xce\x97"),
+ ENTITY_DEF("Qopf", 8474, "\xe2\x84\x9a"),
+ ENTITY_DEF("period", 46, "\x2e"),
+ ENTITY_DEF("angmsd", 8737, "\xe2\x88\xa1"),
+ ENTITY_DEF("fllig", 64258, "\xef\xac\x82"),
+ ENTITY_DEF("cuvee", 8910, "\xe2\x8b\x8e"),
+ ENTITY_DEF("wedbar", 10847, "\xe2\xa9\x9f"),
+ ENTITY_DEF("Fscr", 8497, "\xe2\x84\xb1"),
+ ENTITY_DEF("veebar", 8891, "\xe2\x8a\xbb"),
+ ENTITY_DEF("Longleftrightarrow", 10234, "\xe2\x9f\xba"),
+ ENTITY_DEF_HEUR("reg", 174, "\xc2\xae"),
+ ENTITY_DEF("NegativeMediumSpace", 8203, "\xe2\x80\x8b"),
+ ENTITY_DEF("Upsi", 978, "\xcf\x92"),
+ ENTITY_DEF("Mellintrf", 8499, "\xe2\x84\xb3"),
+ ENTITY_DEF("boxHU", 9577, "\xe2\x95\xa9"),
+ ENTITY_DEF("frac56", 8538, "\xe2\x85\x9a"),
+ ENTITY_DEF("utrif", 9652, "\xe2\x96\xb4"),
+ ENTITY_DEF("LeftTriangle", 8882, "\xe2\x8a\xb2"),
+ ENTITY_DEF("nsime", 8772, "\xe2\x89\x84"),
+ ENTITY_DEF("rcedil", 343, "\xc5\x97"),
+ ENTITY_DEF("aogon", 261, "\xc4\x85"),
+ ENTITY_DEF("uHar", 10595, "\xe2\xa5\xa3"),
+ ENTITY_DEF("ForAll", 8704, "\xe2\x88\x80"),
+ ENTITY_DEF("prE", 10931, "\xe2\xaa\xb3"),
+ ENTITY_DEF("boxV", 9553, "\xe2\x95\x91"),
+ ENTITY_DEF("softcy", 1100, "\xd1\x8c"),
+ ENTITY_DEF("hercon", 8889, "\xe2\x8a\xb9"),
+ ENTITY_DEF("lmoustache", 9136, "\xe2\x8e\xb0"),
+ ENTITY_DEF("Product", 8719, "\xe2\x88\x8f"),
+ ENTITY_DEF("lsimg", 10895, "\xe2\xaa\x8f"),
+ ENTITY_DEF("verbar", 124, "\x7c"),
+ ENTITY_DEF("ofcir", 10687, "\xe2\xa6\xbf"),
+ ENTITY_DEF("curlyeqprec", 8926, "\xe2\x8b\x9e"),
+ ENTITY_DEF("ldquo", 8220, "\xe2\x80\x9c"),
+ ENTITY_DEF("bot", 8869, "\xe2\x8a\xa5"),
+ ENTITY_DEF("Psi", 936, "\xce\xa8"),
+ ENTITY_DEF("OElig", 338, "\xc5\x92"),
+ ENTITY_DEF("DownRightVectorBar", 10583, "\xe2\xa5\x97"),
+ ENTITY_DEF("minusb", 8863, "\xe2\x8a\x9f"),
+ ENTITY_DEF("Iscr", 8464, "\xe2\x84\x90"),
+ ENTITY_DEF("Tcedil", 354, "\xc5\xa2"),
+ ENTITY_DEF("ffilig", 64259, "\xef\xac\x83"),
+ ENTITY_DEF("Gcy", 1043, "\xd0\x93"),
+ ENTITY_DEF("oline", 8254, "\xe2\x80\xbe"),
+ ENTITY_DEF("bottom", 8869, "\xe2\x8a\xa5"),
+ ENTITY_DEF("nVDash", 8879, "\xe2\x8a\xaf"),
+ ENTITY_DEF("lessdot", 8918, "\xe2\x8b\x96"),
+ ENTITY_DEF("cups", 8746, "\xe2\x88\xaa\xef\xb8\x80"),
+ ENTITY_DEF("gla", 10917, "\xe2\xaa\xa5"),
+ ENTITY_DEF("hellip", 8230, "\xe2\x80\xa6"),
+ ENTITY_DEF("hookleftarrow", 8617, "\xe2\x86\xa9"),
+ ENTITY_DEF("Cup", 8915, "\xe2\x8b\x93"),
+ ENTITY_DEF("upsi", 965, "\xcf\x85"),
+ ENTITY_DEF("DownArrowBar", 10515, "\xe2\xa4\x93"),
+ ENTITY_DEF("lowast", 8727, "\xe2\x88\x97"),
+ ENTITY_DEF("profline", 8978, "\xe2\x8c\x92"),
+ ENTITY_DEF("ngsim", 8821, "\xe2\x89\xb5"),
+ ENTITY_DEF("boxhu", 9524, "\xe2\x94\xb4"),
+ ENTITY_DEF("operp", 10681, "\xe2\xa6\xb9"),
+ ENTITY_DEF("cap", 8745, "\xe2\x88\xa9"),
+ ENTITY_DEF("Hcirc", 292, "\xc4\xa4"),
+ ENTITY_DEF("Ncy", 1053, "\xd0\x9d"),
+ ENTITY_DEF("zeetrf", 8488, "\xe2\x84\xa8"),
+ ENTITY_DEF("cuepr", 8926, "\xe2\x8b\x9e"),
+ ENTITY_DEF("supsetneq", 8843, "\xe2\x8a\x8b"),
+ ENTITY_DEF("lfloor", 8970, "\xe2\x8c\x8a"),
+ ENTITY_DEF("ngtr", 8815, "\xe2\x89\xaf"),
+ ENTITY_DEF("ccups", 10828, "\xe2\xa9\x8c"),
+ ENTITY_DEF("pscr", 120005, "\xf0\x9d\x93\x85"),
+ ENTITY_DEF("Cfr", 8493, "\xe2\x84\xad"),
+ ENTITY_DEF("dtri", 9663, "\xe2\x96\xbf"),
+ ENTITY_DEF("icirc", 238, "\xc3\xae"),
+ ENTITY_DEF("leftarrow", 8592, "\xe2\x86\x90"),
+ ENTITY_DEF("vdash", 8866, "\xe2\x8a\xa2"),
+ ENTITY_DEF("leftrightharpoons", 8651, "\xe2\x87\x8b"),
+ ENTITY_DEF("rightrightarrows", 8649, "\xe2\x87\x89"),
+ ENTITY_DEF("strns", 175, "\xc2\xaf"),
+ ENTITY_DEF("intlarhk", 10775, "\xe2\xa8\x97"),
+ ENTITY_DEF("downharpoonright", 8642, "\xe2\x87\x82"),
+ ENTITY_DEF_HEUR("yacute", 253, "\xc3\xbd"),
+ ENTITY_DEF("boxUr", 9561, "\xe2\x95\x99"),
+ ENTITY_DEF("triangleleft", 9667, "\xe2\x97\x83"),
+ ENTITY_DEF("DiacriticalDot", 729, "\xcb\x99"),
+ ENTITY_DEF("thetav", 977, "\xcf\x91"),
+ ENTITY_DEF("OverBracket", 9140, "\xe2\x8e\xb4"),
+ ENTITY_DEF("PrecedesTilde", 8830, "\xe2\x89\xbe"),
+ ENTITY_DEF("rtrie", 8885, "\xe2\x8a\xb5"),
+ ENTITY_DEF("Scirc", 348, "\xc5\x9c"),
+ ENTITY_DEF("vsupne", 8843, "\xe2\x8a\x8b\xef\xb8\x80"),
+ ENTITY_DEF("OverBrace", 9182, "\xe2\x8f\x9e"),
+ ENTITY_DEF("Yfr", 120092, "\xf0\x9d\x94\x9c"),
+ ENTITY_DEF("scnE", 10934, "\xe2\xaa\xb6"),
+ ENTITY_DEF("simlE", 10911, "\xe2\xaa\x9f"),
+ ENTITY_DEF("Proportional", 8733, "\xe2\x88\x9d"),
+ ENTITY_DEF("edot", 279, "\xc4\x97"),
+ ENTITY_DEF("loang", 10220, "\xe2\x9f\xac"),
+ ENTITY_DEF("gesdot", 10880, "\xe2\xaa\x80"),
+ ENTITY_DEF("DownBreve", 785, "\xcc\x91"),
+ ENTITY_DEF("pcy", 1087, "\xd0\xbf"),
+ ENTITY_DEF("Succeeds", 8827, "\xe2\x89\xbb"),
+ ENTITY_DEF("mfr", 120106, "\xf0\x9d\x94\xaa"),
+ ENTITY_DEF("Leftarrow", 8656, "\xe2\x87\x90"),
+ ENTITY_DEF("boxDr", 9555, "\xe2\x95\x93"),
+ ENTITY_DEF("Nscr", 119977, "\xf0\x9d\x92\xa9"),
+ ENTITY_DEF("diam", 8900, "\xe2\x8b\x84"),
+ ENTITY_DEF("CHcy", 1063, "\xd0\xa7"),
+ ENTITY_DEF("boxdr", 9484, "\xe2\x94\x8c"),
+ ENTITY_DEF("rlm", 8207, "\xe2\x80\x8f"),
+ ENTITY_DEF("Coproduct", 8720, "\xe2\x88\x90"),
+ ENTITY_DEF("RightTeeArrow", 8614, "\xe2\x86\xa6"),
+ ENTITY_DEF("tridot", 9708, "\xe2\x97\xac"),
+ ENTITY_DEF("ldquor", 8222, "\xe2\x80\x9e"),
+ ENTITY_DEF("sol", 47, "\x2f"),
+ ENTITY_DEF_HEUR("ecirc", 234, "\xc3\xaa"),
+ ENTITY_DEF("DoubleLeftArrow", 8656, "\xe2\x87\x90"),
+ ENTITY_DEF("Gscr", 119970, "\xf0\x9d\x92\xa2"),
+ ENTITY_DEF("ap", 8776, "\xe2\x89\x88"),
+ ENTITY_DEF("rbrke", 10636, "\xe2\xa6\x8c"),
+ ENTITY_DEF("LeftFloor", 8970, "\xe2\x8c\x8a"),
+ ENTITY_DEF("blk12", 9618, "\xe2\x96\x92"),
+ ENTITY_DEF("Conint", 8751, "\xe2\x88\xaf"),
+ ENTITY_DEF("triangledown", 9663, "\xe2\x96\xbf"),
+ ENTITY_DEF("Icy", 1048, "\xd0\x98"),
+ ENTITY_DEF("backprime", 8245, "\xe2\x80\xb5"),
+ ENTITY_DEF("longleftrightarrow", 10231, "\xe2\x9f\xb7"),
+ ENTITY_DEF("ntriangleleft", 8938, "\xe2\x8b\xaa"),
+ ENTITY_DEF_HEUR("copy", 169, "\xc2\xa9"),
+ ENTITY_DEF("mapstodown", 8615, "\xe2\x86\xa7"),
+ ENTITY_DEF("seArr", 8664, "\xe2\x87\x98"),
+ ENTITY_DEF("ENG", 330, "\xc5\x8a"),
+ ENTITY_DEF("DoubleRightArrow", 8658, "\xe2\x87\x92"),
+ ENTITY_DEF("tfr", 120113, "\xf0\x9d\x94\xb1"),
+ ENTITY_DEF("rharul", 10604, "\xe2\xa5\xac"),
+ ENTITY_DEF("bfr", 120095, "\xf0\x9d\x94\x9f"),
+ ENTITY_DEF("origof", 8886, "\xe2\x8a\xb6"),
+ ENTITY_DEF("Therefore", 8756, "\xe2\x88\xb4"),
+ ENTITY_DEF("glE", 10898, "\xe2\xaa\x92"),
+ ENTITY_DEF("leftarrowtail", 8610, "\xe2\x86\xa2"),
+ ENTITY_DEF("NotEqual", 8800, "\xe2\x89\xa0"),
+ ENTITY_DEF("LeftCeiling", 8968, "\xe2\x8c\x88"),
+ ENTITY_DEF("lArr", 8656, "\xe2\x87\x90"),
+ ENTITY_DEF("subseteq", 8838, "\xe2\x8a\x86"),
+ ENTITY_DEF("larrbfs", 10527, "\xe2\xa4\x9f"),
+ ENTITY_DEF("Gammad", 988, "\xcf\x9c"),
+ ENTITY_DEF("rtriltri", 10702, "\xe2\xa7\x8e"),
+ ENTITY_DEF("Fcy", 1060, "\xd0\xa4"),
+ ENTITY_DEF("Vopf", 120141, "\xf0\x9d\x95\x8d"),
+ ENTITY_DEF("lrarr", 8646, "\xe2\x87\x86"),
+ ENTITY_DEF("delta", 948, "\xce\xb4"),
+ ENTITY_DEF("xodot", 10752, "\xe2\xa8\x80"),
+ ENTITY_DEF("larrtl", 8610, "\xe2\x86\xa2"),
+ ENTITY_DEF("gsim", 8819, "\xe2\x89\xb3"),
+ ENTITY_DEF("ratail", 10522, "\xe2\xa4\x9a"),
+ ENTITY_DEF("vsubne", 8842, "\xe2\x8a\x8a\xef\xb8\x80"),
+ ENTITY_DEF("boxur", 9492, "\xe2\x94\x94"),
+ ENTITY_DEF("succsim", 8831, "\xe2\x89\xbf"),
+ ENTITY_DEF("triplus", 10809, "\xe2\xa8\xb9"),
+ ENTITY_DEF("nless", 8814, "\xe2\x89\xae"),
+ ENTITY_DEF("uharr", 8638, "\xe2\x86\xbe"),
+ ENTITY_DEF("lambda", 955, "\xce\xbb"),
+ ENTITY_DEF_HEUR("uuml", 252, "\xc3\xbc"),
+ ENTITY_DEF("horbar", 8213, "\xe2\x80\x95"),
+ ENTITY_DEF("ccirc", 265, "\xc4\x89"),
+ ENTITY_DEF("sqcup", 8852, "\xe2\x8a\x94"),
+ ENTITY_DEF("Pscr", 119979, "\xf0\x9d\x92\xab"),
+ ENTITY_DEF("supsup", 10966, "\xe2\xab\x96"),
+ ENTITY_DEF("Cacute", 262, "\xc4\x86"),
+ ENTITY_DEF("upsih", 978, "\xcf\x92"),
+ ENTITY_DEF("precsim", 8830, "\xe2\x89\xbe"),
+ ENTITY_DEF("longrightarrow", 10230, "\xe2\x9f\xb6"),
+ ENTITY_DEF("circledR", 174, "\xc2\xae"),
+ ENTITY_DEF("UpTeeArrow", 8613, "\xe2\x86\xa5"),
+ ENTITY_DEF("bepsi", 1014, "\xcf\xb6"),
+ ENTITY_DEF("oast", 8859, "\xe2\x8a\x9b"),
+ ENTITY_DEF("yfr", 120118, "\xf0\x9d\x94\xb6"),
+ ENTITY_DEF("rdsh", 8627, "\xe2\x86\xb3"),
+ ENTITY_DEF("Ograve", 210, "\xc3\x92"),
+ ENTITY_DEF("LeftVectorBar", 10578, "\xe2\xa5\x92"),
+ ENTITY_DEF("NotNestedLessLess", 10913, "\xe2\xaa\xa1\xcc\xb8"),
+ ENTITY_DEF("Jscr", 119973, "\xf0\x9d\x92\xa5"),
+ ENTITY_DEF("psi", 968, "\xcf\x88"),
+ ENTITY_DEF("orarr", 8635, "\xe2\x86\xbb"),
+ ENTITY_DEF("Subset", 8912, "\xe2\x8b\x90"),
+ ENTITY_DEF("curarr", 8631, "\xe2\x86\xb7"),
+ ENTITY_DEF("CirclePlus", 8853, "\xe2\x8a\x95"),
+ ENTITY_DEF("gtrless", 8823, "\xe2\x89\xb7"),
+ ENTITY_DEF("nvle", 8804, "\xe2\x89\xa4\xe2\x83\x92"),
+ ENTITY_DEF("prop", 8733, "\xe2\x88\x9d"),
+ ENTITY_DEF("gEl", 10892, "\xe2\xaa\x8c"),
+ ENTITY_DEF("gtlPar", 10645, "\xe2\xa6\x95"),
+ ENTITY_DEF("frasl", 8260, "\xe2\x81\x84"),
+ ENTITY_DEF("nearr", 8599, "\xe2\x86\x97"),
+ ENTITY_DEF("NotSubsetEqual", 8840, "\xe2\x8a\x88"),
+ ENTITY_DEF("planck", 8463, "\xe2\x84\x8f"),
+ ENTITY_DEF_HEUR("Uuml", 220, "\xc3\x9c"),
+ ENTITY_DEF("spadesuit", 9824, "\xe2\x99\xa0"),
+ ENTITY_DEF_HEUR("sect", 167, "\xc2\xa7"),
+ ENTITY_DEF("cdot", 267, "\xc4\x8b"),
+ ENTITY_DEF("boxVh", 9579, "\xe2\x95\xab"),
+ ENTITY_DEF("zscr", 120015, "\xf0\x9d\x93\x8f"),
+ ENTITY_DEF("nsqsube", 8930, "\xe2\x8b\xa2"),
+ ENTITY_DEF("grave", 96, "\x60"),
+ ENTITY_DEF("angrtvb", 8894, "\xe2\x8a\xbe"),
+ ENTITY_DEF("MediumSpace", 8287, "\xe2\x81\x9f"),
+ ENTITY_DEF("Ntilde", 209, "\xc3\x91"),
+ ENTITY_DEF("solb", 10692, "\xe2\xa7\x84"),
+ ENTITY_DEF("angzarr", 9084, "\xe2\x8d\xbc"),
+ ENTITY_DEF("nopf", 120159, "\xf0\x9d\x95\x9f"),
+ ENTITY_DEF("rtrif", 9656, "\xe2\x96\xb8"),
+ ENTITY_DEF("nrightarrow", 8603, "\xe2\x86\x9b"),
+ ENTITY_DEF("Kappa", 922, "\xce\x9a"),
+ ENTITY_DEF("simrarr", 10610, "\xe2\xa5\xb2"),
+ ENTITY_DEF("imacr", 299, "\xc4\xab"),
+ ENTITY_DEF("vrtri", 8883, "\xe2\x8a\xb3"),
+ ENTITY_DEF("part", 8706, "\xe2\x88\x82"),
+ ENTITY_DEF("esim", 8770, "\xe2\x89\x82"),
+ ENTITY_DEF_HEUR("atilde", 227, "\xc3\xa3"),
+ ENTITY_DEF("DownRightTeeVector", 10591, "\xe2\xa5\x9f"),
+ ENTITY_DEF("jcirc", 309, "\xc4\xb5"),
+ ENTITY_DEF("Ecaron", 282, "\xc4\x9a"),
+ ENTITY_DEF("VerticalSeparator", 10072, "\xe2\x9d\x98"),
+ ENTITY_DEF("rHar", 10596, "\xe2\xa5\xa4"),
+ ENTITY_DEF("rcaron", 345, "\xc5\x99"),
+ ENTITY_DEF("subnE", 10955, "\xe2\xab\x8b"),
+ ENTITY_DEF("ii", 8520, "\xe2\x85\x88"),
+ ENTITY_DEF("Cconint", 8752, "\xe2\x88\xb0"),
+ ENTITY_DEF("Mcy", 1052, "\xd0\x9c"),
+ ENTITY_DEF("eqcolon", 8789, "\xe2\x89\x95"),
+ ENTITY_DEF("cupor", 10821, "\xe2\xa9\x85"),
+ ENTITY_DEF("DoubleUpArrow", 8657, "\xe2\x87\x91"),
+ ENTITY_DEF("boxbox", 10697, "\xe2\xa7\x89"),
+ ENTITY_DEF("setminus", 8726, "\xe2\x88\x96"),
+ ENTITY_DEF("Lleftarrow", 8666, "\xe2\x87\x9a"),
+ ENTITY_DEF("nang", 8736, "\xe2\x88\xa0\xe2\x83\x92"),
+ ENTITY_DEF("TRADE", 8482, "\xe2\x84\xa2"),
+ ENTITY_DEF("urcorner", 8989, "\xe2\x8c\x9d"),
+ ENTITY_DEF("lsqb", 91, "\x5b"),
+ ENTITY_DEF("cupcup", 10826, "\xe2\xa9\x8a"),
+ ENTITY_DEF("kjcy", 1116, "\xd1\x9c"),
+ ENTITY_DEF("llhard", 10603, "\xe2\xa5\xab"),
+ ENTITY_DEF("mumap", 8888, "\xe2\x8a\xb8"),
+ ENTITY_DEF("iiint", 8749, "\xe2\x88\xad"),
+ ENTITY_DEF("RightTee", 8866, "\xe2\x8a\xa2"),
+ ENTITY_DEF("Tcaron", 356, "\xc5\xa4"),
+ ENTITY_DEF("bigcirc", 9711, "\xe2\x97\xaf"),
+ ENTITY_DEF("trianglerighteq", 8885, "\xe2\x8a\xb5"),
+ ENTITY_DEF("NotLessGreater", 8824, "\xe2\x89\xb8"),
+ ENTITY_DEF("hArr", 8660, "\xe2\x87\x94"),
+ ENTITY_DEF("ocy", 1086, "\xd0\xbe"),
+ ENTITY_DEF("tosa", 10537, "\xe2\xa4\xa9"),
+ ENTITY_DEF("twixt", 8812, "\xe2\x89\xac"),
+ ENTITY_DEF("square", 9633, "\xe2\x96\xa1"),
+ ENTITY_DEF("Otimes", 10807, "\xe2\xa8\xb7"),
+ ENTITY_DEF("Kcedil", 310, "\xc4\xb6"),
+ ENTITY_DEF("beth", 8502, "\xe2\x84\xb6"),
+ ENTITY_DEF("triminus", 10810, "\xe2\xa8\xba"),
+ ENTITY_DEF("nlArr", 8653, "\xe2\x87\x8d"),
+ ENTITY_DEF("Oacute", 211, "\xc3\x93"),
+ ENTITY_DEF("zwnj", 8204, "\xe2\x80\x8c"),
+ ENTITY_DEF("ll", 8810, "\xe2\x89\xaa"),
+ ENTITY_DEF("smashp", 10803, "\xe2\xa8\xb3"),
+ ENTITY_DEF("ngeqq", 8807, "\xe2\x89\xa7\xcc\xb8"),
+ ENTITY_DEF("rnmid", 10990, "\xe2\xab\xae"),
+ ENTITY_DEF("nwArr", 8662, "\xe2\x87\x96"),
+ ENTITY_DEF("RightUpDownVector", 10575, "\xe2\xa5\x8f"),
+ ENTITY_DEF("lbbrk", 10098, "\xe2\x9d\xb2"),
+ ENTITY_DEF("compfn", 8728, "\xe2\x88\x98"),
+ ENTITY_DEF("eDDot", 10871, "\xe2\xa9\xb7"),
+ ENTITY_DEF("Jsercy", 1032, "\xd0\x88"),
+ ENTITY_DEF("HARDcy", 1066, "\xd0\xaa"),
+ ENTITY_DEF("nexists", 8708, "\xe2\x88\x84"),
+ ENTITY_DEF("theta", 952, "\xce\xb8"),
+ ENTITY_DEF("plankv", 8463, "\xe2\x84\x8f"),
+ ENTITY_DEF_HEUR("sup2", 178, "\xc2\xb2"),
+ ENTITY_DEF("lessapprox", 10885, "\xe2\xaa\x85"),
+ ENTITY_DEF("gdot", 289, "\xc4\xa1"),
+ ENTITY_DEF("angmsdae", 10668, "\xe2\xa6\xac"),
+ ENTITY_DEF("Superset", 8835, "\xe2\x8a\x83"),
+ ENTITY_DEF("prap", 10935, "\xe2\xaa\xb7"),
+ ENTITY_DEF("Zscr", 119989, "\xf0\x9d\x92\xb5"),
+ ENTITY_DEF("nsucc", 8833, "\xe2\x8a\x81"),
+ ENTITY_DEF("supseteqq", 10950, "\xe2\xab\x86"),
+ ENTITY_DEF("UpTee", 8869, "\xe2\x8a\xa5"),
+ ENTITY_DEF("LowerLeftArrow", 8601, "\xe2\x86\x99"),
+ ENTITY_DEF("ssmile", 8995, "\xe2\x8c\xa3"),
+ ENTITY_DEF("niv", 8715, "\xe2\x88\x8b"),
+ ENTITY_DEF("bigvee", 8897, "\xe2\x8b\x81"),
+ ENTITY_DEF("kscr", 120000, "\xf0\x9d\x93\x80"),
+ ENTITY_DEF("xutri", 9651, "\xe2\x96\xb3"),
+ ENTITY_DEF("caret", 8257, "\xe2\x81\x81"),
+ ENTITY_DEF("caron", 711, "\xcb\x87"),
+ ENTITY_DEF("Wedge", 8896, "\xe2\x8b\x80"),
+ ENTITY_DEF("sdotb", 8865, "\xe2\x8a\xa1"),
+ ENTITY_DEF("bigoplus", 10753, "\xe2\xa8\x81"),
+ ENTITY_DEF("Breve", 728, "\xcb\x98"),
+ ENTITY_DEF("ImaginaryI", 8520, "\xe2\x85\x88"),
+ ENTITY_DEF("longmapsto", 10236, "\xe2\x9f\xbc"),
+ ENTITY_DEF("boxVH", 9580, "\xe2\x95\xac"),
+ ENTITY_DEF("lozenge", 9674, "\xe2\x97\x8a"),
+ ENTITY_DEF("toea", 10536, "\xe2\xa4\xa8"),
+ ENTITY_DEF("nbumpe", 8783, "\xe2\x89\x8f\xcc\xb8"),
+ ENTITY_DEF("gcirc", 285, "\xc4\x9d"),
+ ENTITY_DEF("NotHumpEqual", 8783, "\xe2\x89\x8f\xcc\xb8"),
+ ENTITY_DEF("pre", 10927, "\xe2\xaa\xaf"),
+ ENTITY_DEF("ascr", 119990, "\xf0\x9d\x92\xb6"),
+ ENTITY_DEF("Acirc", 194, "\xc3\x82"),
+ ENTITY_DEF("questeq", 8799, "\xe2\x89\x9f"),
+ ENTITY_DEF("ncaron", 328, "\xc5\x88"),
+ ENTITY_DEF("LeftTeeArrow", 8612, "\xe2\x86\xa4"),
+ ENTITY_DEF("xcirc", 9711, "\xe2\x97\xaf"),
+ ENTITY_DEF("swarr", 8601, "\xe2\x86\x99"),
+ ENTITY_DEF("MinusPlus", 8723, "\xe2\x88\x93"),
+ ENTITY_DEF("plus", 43, "\x2b"),
+ ENTITY_DEF("NotDoubleVerticalBar", 8742, "\xe2\x88\xa6"),
+ ENTITY_DEF("rppolint", 10770, "\xe2\xa8\x92"),
+ ENTITY_DEF("NotTildeFullEqual", 8775, "\xe2\x89\x87"),
+ ENTITY_DEF("ltdot", 8918, "\xe2\x8b\x96"),
+ ENTITY_DEF("NotNestedGreaterGreater", 10914, "\xe2\xaa\xa2\xcc\xb8"),
+ ENTITY_DEF("Lscr", 8466, "\xe2\x84\x92"),
+ ENTITY_DEF("pitchfork", 8916, "\xe2\x8b\x94"),
+ ENTITY_DEF("Eopf", 120124, "\xf0\x9d\x94\xbc"),
+ ENTITY_DEF("ropf", 120163, "\xf0\x9d\x95\xa3"),
+ ENTITY_DEF("Delta", 916, "\xce\x94"),
+ ENTITY_DEF("lozf", 10731, "\xe2\xa7\xab"),
+ ENTITY_DEF("RightTeeVector", 10587, "\xe2\xa5\x9b"),
+ ENTITY_DEF("UpDownArrow", 8597, "\xe2\x86\x95"),
+ ENTITY_DEF("bump", 8782, "\xe2\x89\x8e"),
+ ENTITY_DEF("Rscr", 8475, "\xe2\x84\x9b"),
+ ENTITY_DEF("slarr", 8592, "\xe2\x86\x90"),
+ ENTITY_DEF("lcy", 1083, "\xd0\xbb"),
+ ENTITY_DEF("Vee", 8897, "\xe2\x8b\x81"),
+ ENTITY_DEF("Iogon", 302, "\xc4\xae"),
+ ENTITY_DEF("minus", 8722, "\xe2\x88\x92"),
+ ENTITY_DEF("GreaterFullEqual", 8807, "\xe2\x89\xa7"),
+ ENTITY_DEF("xhArr", 10234, "\xe2\x9f\xba"),
+ ENTITY_DEF("shortmid", 8739, "\xe2\x88\xa3"),
+ ENTITY_DEF("DoubleDownArrow", 8659, "\xe2\x87\x93"),
+ ENTITY_DEF("Wscr", 119986, "\xf0\x9d\x92\xb2"),
+ ENTITY_DEF("rang", 10217, "\xe2\x9f\xa9"),
+ ENTITY_DEF("lcub", 123, "\x7b"),
+ ENTITY_DEF("mnplus", 8723, "\xe2\x88\x93"),
+ ENTITY_DEF("ulcrop", 8975, "\xe2\x8c\x8f"),
+ ENTITY_DEF("wfr", 120116, "\xf0\x9d\x94\xb4"),
+ ENTITY_DEF("DifferentialD", 8518, "\xe2\x85\x86"),
+ ENTITY_DEF("ThinSpace", 8201, "\xe2\x80\x89"),
+ ENTITY_DEF("NotGreaterGreater", 8811, "\xe2\x89\xab\xcc\xb8"),
+ ENTITY_DEF("Topf", 120139, "\xf0\x9d\x95\x8b"),
+ ENTITY_DEF("sbquo", 8218, "\xe2\x80\x9a"),
+ ENTITY_DEF("sdot", 8901, "\xe2\x8b\x85"),
+ ENTITY_DEF("DoubleLeftTee", 10980, "\xe2\xab\xa4"),
+ ENTITY_DEF("vBarv", 10985, "\xe2\xab\xa9"),
+ ENTITY_DEF("subne", 8842, "\xe2\x8a\x8a"),
+ ENTITY_DEF("gtrdot", 8919, "\xe2\x8b\x97"),
+ ENTITY_DEF("opar", 10679, "\xe2\xa6\xb7"),
+ ENTITY_DEF("apid", 8779, "\xe2\x89\x8b"),
+ ENTITY_DEF("Cross", 10799, "\xe2\xa8\xaf"),
+ ENTITY_DEF("lhblk", 9604, "\xe2\x96\x84"),
+ ENTITY_DEF("capcap", 10827, "\xe2\xa9\x8b"),
+ ENTITY_DEF("midast", 42, "\x2a"),
+ ENTITY_DEF("lscr", 120001, "\xf0\x9d\x93\x81"),
+ ENTITY_DEF("nGt", 8811, "\xe2\x89\xab\xe2\x83\x92"),
+ ENTITY_DEF_HEUR("Euml", 203, "\xc3\x8b"),
+ ENTITY_DEF("blacktriangledown", 9662, "\xe2\x96\xbe"),
+ ENTITY_DEF("Rcy", 1056, "\xd0\xa0"),
+ ENTITY_DEF("dfisht", 10623, "\xe2\xa5\xbf"),
+ ENTITY_DEF("dashv", 8867, "\xe2\x8a\xa3"),
+ ENTITY_DEF("ast", 42, "\x2a"),
+ ENTITY_DEF("ContourIntegral", 8750, "\xe2\x88\xae"),
+ ENTITY_DEF("Ofr", 120082, "\xf0\x9d\x94\x92"),
+ ENTITY_DEF("Lcy", 1051, "\xd0\x9b"),
+ ENTITY_DEF("nltrie", 8940, "\xe2\x8b\xac"),
+ ENTITY_DEF("ShortUpArrow", 8593, "\xe2\x86\x91"),
+ ENTITY_DEF("acy", 1072, "\xd0\xb0"),
+ ENTITY_DEF("rightarrow", 8594, "\xe2\x86\x92"),
+ ENTITY_DEF("UnderBar", 95, "\x5f"),
+ ENTITY_DEF("LongLeftArrow", 10229, "\xe2\x9f\xb5"),
+ ENTITY_DEF("andd", 10844, "\xe2\xa9\x9c"),
+ ENTITY_DEF("xlarr", 10229, "\xe2\x9f\xb5"),
+ ENTITY_DEF("percnt", 37, "\x25"),
+ ENTITY_DEF("rharu", 8640, "\xe2\x87\x80"),
+ ENTITY_DEF("plusdo", 8724, "\xe2\x88\x94"),
+ ENTITY_DEF("TScy", 1062, "\xd0\xa6"),
+ ENTITY_DEF("kcy", 1082, "\xd0\xba"),
+ ENTITY_DEF("boxVR", 9568, "\xe2\x95\xa0"),
+ ENTITY_DEF("looparrowleft", 8619, "\xe2\x86\xab"),
+ ENTITY_DEF("scirc", 349, "\xc5\x9d"),
+ ENTITY_DEF("drcorn", 8991, "\xe2\x8c\x9f"),
+ ENTITY_DEF("iiota", 8489, "\xe2\x84\xa9"),
+ ENTITY_DEF("Zcy", 1047, "\xd0\x97"),
+ ENTITY_DEF("frac58", 8541, "\xe2\x85\x9d"),
+ ENTITY_DEF("alpha", 945, "\xce\xb1"),
+ ENTITY_DEF("daleth", 8504, "\xe2\x84\xb8"),
+ ENTITY_DEF("gtreqless", 8923, "\xe2\x8b\x9b"),
+ ENTITY_DEF("tstrok", 359, "\xc5\xa7"),
+ ENTITY_DEF("plusb", 8862, "\xe2\x8a\x9e"),
+ ENTITY_DEF("odsold", 10684, "\xe2\xa6\xbc"),
+ ENTITY_DEF("varsupsetneqq", 10956, "\xe2\xab\x8c\xef\xb8\x80"),
+ ENTITY_DEF_HEUR("otilde", 245, "\xc3\xb5"),
+ ENTITY_DEF("gtcir", 10874, "\xe2\xa9\xba"),
+ ENTITY_DEF("lltri", 9722, "\xe2\x97\xba"),
+ ENTITY_DEF("rx", 8478, "\xe2\x84\x9e"),
+ ENTITY_DEF("ljcy", 1113, "\xd1\x99"),
+ ENTITY_DEF("parsim", 10995, "\xe2\xab\xb3"),
+ ENTITY_DEF("NotElement", 8713, "\xe2\x88\x89"),
+ ENTITY_DEF_HEUR("plusmn", 177, "\xc2\xb1"),
+ ENTITY_DEF("varsubsetneq", 8842, "\xe2\x8a\x8a\xef\xb8\x80"),
+ ENTITY_DEF("subset", 8834, "\xe2\x8a\x82"),
+ ENTITY_DEF("awint", 10769, "\xe2\xa8\x91"),
+ ENTITY_DEF("laemptyv", 10676, "\xe2\xa6\xb4"),
+ ENTITY_DEF("phiv", 981, "\xcf\x95"),
+ ENTITY_DEF("sfrown", 8994, "\xe2\x8c\xa2"),
+ ENTITY_DEF("DoubleUpDownArrow", 8661, "\xe2\x87\x95"),
+ ENTITY_DEF("lpar", 40, "\x28"),
+ ENTITY_DEF("frac45", 8536, "\xe2\x85\x98"),
+ ENTITY_DEF("rBarr", 10511, "\xe2\xa4\x8f"),
+ ENTITY_DEF("npolint", 10772, "\xe2\xa8\x94"),
+ ENTITY_DEF("emacr", 275, "\xc4\x93"),
+ ENTITY_DEF("maltese", 10016, "\xe2\x9c\xa0"),
+ ENTITY_DEF("PlusMinus", 177, "\xc2\xb1"),
+ ENTITY_DEF("ReverseEquilibrium", 8651, "\xe2\x87\x8b"),
+ ENTITY_DEF("oscr", 8500, "\xe2\x84\xb4"),
+ ENTITY_DEF("blacksquare", 9642, "\xe2\x96\xaa"),
+ ENTITY_DEF("TSHcy", 1035, "\xd0\x8b"),
+ ENTITY_DEF("gap", 10886, "\xe2\xaa\x86"),
+ ENTITY_DEF("xnis", 8955, "\xe2\x8b\xbb"),
+ ENTITY_DEF("Ll", 8920, "\xe2\x8b\x98"),
+ ENTITY_DEF("PrecedesEqual", 10927, "\xe2\xaa\xaf"),
+ ENTITY_DEF("incare", 8453, "\xe2\x84\x85"),
+ ENTITY_DEF("nharr", 8622, "\xe2\x86\xae"),
+ ENTITY_DEF("varnothing", 8709, "\xe2\x88\x85"),
+ ENTITY_DEF("ShortDownArrow", 8595, "\xe2\x86\x93"),
+ ENTITY_DEF_HEUR("nbsp", 160, " "),
+ ENTITY_DEF("asympeq", 8781, "\xe2\x89\x8d"),
+ ENTITY_DEF("rbrkslu", 10640, "\xe2\xa6\x90"),
+ ENTITY_DEF("rho", 961, "\xcf\x81"),
+ ENTITY_DEF("Mscr", 8499, "\xe2\x84\xb3"),
+ ENTITY_DEF_HEUR("eth", 240, "\xc3\xb0"),
+ ENTITY_DEF("suplarr", 10619, "\xe2\xa5\xbb"),
+ ENTITY_DEF("Tab", 9, "\x09"),
+ ENTITY_DEF("omicron", 959, "\xce\xbf"),
+ ENTITY_DEF("blacktriangle", 9652, "\xe2\x96\xb4"),
+ ENTITY_DEF("nldr", 8229, "\xe2\x80\xa5"),
+ ENTITY_DEF("downharpoonleft", 8643, "\xe2\x87\x83"),
+ ENTITY_DEF("circledcirc", 8858, "\xe2\x8a\x9a"),
+ ENTITY_DEF("leftleftarrows", 8647, "\xe2\x87\x87"),
+ ENTITY_DEF("NotHumpDownHump", 8782, "\xe2\x89\x8e\xcc\xb8"),
+ ENTITY_DEF("nvgt", 62, "\x3e\xe2\x83\x92"),
+ ENTITY_DEF("rhard", 8641, "\xe2\x87\x81"),
+ ENTITY_DEF("nGg", 8921, "\xe2\x8b\x99\xcc\xb8"),
+ ENTITY_DEF("lurdshar", 10570, "\xe2\xa5\x8a"),
+ ENTITY_DEF("cirE", 10691, "\xe2\xa7\x83"),
+ ENTITY_DEF("isinE", 8953, "\xe2\x8b\xb9"),
+ ENTITY_DEF("eparsl", 10723, "\xe2\xa7\xa3"),
+ ENTITY_DEF("RightAngleBracket", 10217, "\xe2\x9f\xa9"),
+ ENTITY_DEF("hcirc", 293, "\xc4\xa5"),
+ ENTITY_DEF("bumpeq", 8783, "\xe2\x89\x8f"),
+ ENTITY_DEF("cire", 8791, "\xe2\x89\x97"),
+ ENTITY_DEF("dotplus", 8724, "\xe2\x88\x94"),
+ ENTITY_DEF("itilde", 297, "\xc4\xa9"),
+ ENTITY_DEF("uwangle", 10663, "\xe2\xa6\xa7"),
+ ENTITY_DEF("rlhar", 8652, "\xe2\x87\x8c"),
+ ENTITY_DEF("rbrace", 125, "\x7d"),
+ ENTITY_DEF("mid", 8739, "\xe2\x88\xa3"),
+ ENTITY_DEF("el", 10905, "\xe2\xaa\x99"),
+ ENTITY_DEF("KJcy", 1036, "\xd0\x8c"),
+ ENTITY_DEF("odiv", 10808, "\xe2\xa8\xb8"),
+ ENTITY_DEF("amacr", 257, "\xc4\x81"),
+ ENTITY_DEF("qprime", 8279, "\xe2\x81\x97"),
+ ENTITY_DEF("tcedil", 355, "\xc5\xa3"),
+ ENTITY_DEF("UpArrowDownArrow", 8645, "\xe2\x87\x85"),
+ ENTITY_DEF("spades", 9824, "\xe2\x99\xa0"),
+ ENTITY_DEF("napos", 329, "\xc5\x89"),
+ ENTITY_DEF("straightepsilon", 1013, "\xcf\xb5"),
+ ENTITY_DEF("CupCap", 8781, "\xe2\x89\x8d"),
+ ENTITY_DEF("Oopf", 120134, "\xf0\x9d\x95\x86"),
+ ENTITY_DEF("sub", 8834, "\xe2\x8a\x82"),
+ ENTITY_DEF("ohm", 937, "\xce\xa9"),
+ ENTITY_DEF("UnderBrace", 9183, "\xe2\x8f\x9f"),
+ ENTITY_DEF("looparrowright", 8620, "\xe2\x86\xac"),
+ ENTITY_DEF("xotime", 10754, "\xe2\xa8\x82"),
+ ENTITY_DEF("ntgl", 8825, "\xe2\x89\xb9"),
+ ENTITY_DEF("minusdu", 10794, "\xe2\xa8\xaa"),
+ ENTITY_DEF("rarrb", 8677, "\xe2\x87\xa5"),
+ ENTITY_DEF("nvlArr", 10498, "\xe2\xa4\x82"),
+ ENTITY_DEF("triangle", 9653, "\xe2\x96\xb5"),
+ ENTITY_DEF("nacute", 324, "\xc5\x84"),
+ ENTITY_DEF("boxHD", 9574, "\xe2\x95\xa6"),
+ ENTITY_DEF("ratio", 8758, "\xe2\x88\xb6"),
+ ENTITY_DEF("larrsim", 10611, "\xe2\xa5\xb3"),
+ ENTITY_DEF("LessLess", 10913, "\xe2\xaa\xa1"),
+ ENTITY_DEF("yacy", 1103, "\xd1\x8f"),
+ ENTITY_DEF("ctdot", 8943, "\xe2\x8b\xaf"),
+ ENTITY_DEF("and", 8743, "\xe2\x88\xa7"),
+ ENTITY_DEF("lrtri", 8895, "\xe2\x8a\xbf"),
+ ENTITY_DEF("eDot", 8785, "\xe2\x89\x91"),
+ ENTITY_DEF("sqsub", 8847, "\xe2\x8a\x8f"),
+ ENTITY_DEF("real", 8476, "\xe2\x84\x9c"),
+ ENTITY_DEF("Dcy", 1044, "\xd0\x94"),
+ ENTITY_DEF("vartheta", 977, "\xcf\x91"),
+ ENTITY_DEF("nsub", 8836, "\xe2\x8a\x84"),
+ ENTITY_DEF("DownTee", 8868, "\xe2\x8a\xa4"),
+ ENTITY_DEF_HEUR("acute", 180, "\xc2\xb4"),
+ ENTITY_DEF("GreaterLess", 8823, "\xe2\x89\xb7"),
+ ENTITY_DEF("supplus", 10944, "\xe2\xab\x80"),
+ ENTITY_DEF("Vbar", 10987, "\xe2\xab\xab"),
+ ENTITY_DEF("divideontimes", 8903, "\xe2\x8b\x87"),
+ ENTITY_DEF("lsim", 8818, "\xe2\x89\xb2"),
+ ENTITY_DEF("nearhk", 10532, "\xe2\xa4\xa4"),
+ ENTITY_DEF("nLtv", 8810, "\xe2\x89\xaa\xcc\xb8"),
+ ENTITY_DEF("RuleDelayed", 10740, "\xe2\xa7\xb4"),
+ ENTITY_DEF("smile", 8995, "\xe2\x8c\xa3"),
+ ENTITY_DEF("coprod", 8720, "\xe2\x88\x90"),
+ ENTITY_DEF("imof", 8887, "\xe2\x8a\xb7"),
+ ENTITY_DEF("ecy", 1101, "\xd1\x8d"),
+ ENTITY_DEF("RightCeiling", 8969, "\xe2\x8c\x89"),
+ ENTITY_DEF("dlcorn", 8990, "\xe2\x8c\x9e"),
+ ENTITY_DEF("Nu", 925, "\xce\x9d"),
+ ENTITY_DEF("frac18", 8539, "\xe2\x85\x9b"),
+ ENTITY_DEF("diamond", 8900, "\xe2\x8b\x84"),
+ ENTITY_DEF("Icirc", 206, "\xc3\x8e"),
+ ENTITY_DEF("ngeq", 8817, "\xe2\x89\xb1"),
+ ENTITY_DEF("epsilon", 949, "\xce\xb5"),
+ ENTITY_DEF("fork", 8916, "\xe2\x8b\x94"),
+ ENTITY_DEF("xrarr", 10230, "\xe2\x9f\xb6"),
+ ENTITY_DEF("racute", 341, "\xc5\x95"),
+ ENTITY_DEF("ntlg", 8824, "\xe2\x89\xb8"),
+ ENTITY_DEF("xvee", 8897, "\xe2\x8b\x81"),
+ ENTITY_DEF("LeftArrowRightArrow", 8646, "\xe2\x87\x86"),
+ ENTITY_DEF("DownLeftRightVector", 10576, "\xe2\xa5\x90"),
+ ENTITY_DEF("Eacute", 201, "\xc3\x89"),
+ ENTITY_DEF("gimel", 8503, "\xe2\x84\xb7"),
+ ENTITY_DEF("rtimes", 8906, "\xe2\x8b\x8a"),
+ ENTITY_DEF("forall", 8704, "\xe2\x88\x80"),
+ ENTITY_DEF("DiacriticalDoubleAcute", 733, "\xcb\x9d"),
+ ENTITY_DEF("dArr", 8659, "\xe2\x87\x93"),
+ ENTITY_DEF("fallingdotseq", 8786, "\xe2\x89\x92"),
+ ENTITY_DEF("Aogon", 260, "\xc4\x84"),
+ ENTITY_DEF("PartialD", 8706, "\xe2\x88\x82"),
+ ENTITY_DEF("mapstoup", 8613, "\xe2\x86\xa5"),
+ ENTITY_DEF("die", 168, "\xc2\xa8"),
+ ENTITY_DEF("ngt", 8815, "\xe2\x89\xaf"),
+ ENTITY_DEF("vcy", 1074, "\xd0\xb2"),
+ ENTITY_DEF("fjlig", (unsigned) -1, "\x66\x6a"),
+ ENTITY_DEF("submult", 10945, "\xe2\xab\x81"),
+ ENTITY_DEF("ubrcy", 1118, "\xd1\x9e"),
+ ENTITY_DEF("ovbar", 9021, "\xe2\x8c\xbd"),
+ ENTITY_DEF("bsime", 8909, "\xe2\x8b\x8d"),
+ ENTITY_DEF("precnsim", 8936, "\xe2\x8b\xa8"),
+ ENTITY_DEF("DiacriticalTilde", 732, "\xcb\x9c"),
+ ENTITY_DEF("cwint", 8753, "\xe2\x88\xb1"),
+ ENTITY_DEF("Scy", 1057, "\xd0\xa1"),
+ ENTITY_DEF("NotGreaterEqual", 8817, "\xe2\x89\xb1"),
+ ENTITY_DEF("boxUR", 9562, "\xe2\x95\x9a"),
+ ENTITY_DEF("LessSlantEqual", 10877, "\xe2\xa9\xbd"),
+ ENTITY_DEF("Barwed", 8966, "\xe2\x8c\x86"),
+ ENTITY_DEF("supdot", 10942, "\xe2\xaa\xbe"),
+ ENTITY_DEF("gel", 8923, "\xe2\x8b\x9b"),
+ ENTITY_DEF("iscr", 119998, "\xf0\x9d\x92\xbe"),
+ ENTITY_DEF("doublebarwedge", 8966, "\xe2\x8c\x86"),
+ ENTITY_DEF("Idot", 304, "\xc4\xb0"),
+ ENTITY_DEF("DoubleDot", 168, "\xc2\xa8"),
+ ENTITY_DEF("rsquo", 8217, "\xe2\x80\x99"),
+ ENTITY_DEF("subsetneqq", 10955, "\xe2\xab\x8b"),
+ ENTITY_DEF("UpEquilibrium", 10606, "\xe2\xa5\xae"),
+ ENTITY_DEF("copysr", 8471, "\xe2\x84\x97"),
+ ENTITY_DEF("RightDoubleBracket", 10215, "\xe2\x9f\xa7"),
+ ENTITY_DEF("LeftRightVector", 10574, "\xe2\xa5\x8e"),
+ ENTITY_DEF("DownLeftVectorBar", 10582, "\xe2\xa5\x96"),
+ ENTITY_DEF("suphsub", 10967, "\xe2\xab\x97"),
+ ENTITY_DEF_HEUR("cedil", 184, "\xc2\xb8"),
+ ENTITY_DEF("prurel", 8880, "\xe2\x8a\xb0"),
+ ENTITY_DEF("imagpart", 8465, "\xe2\x84\x91"),
+ ENTITY_DEF("Hscr", 8459, "\xe2\x84\x8b"),
+ ENTITY_DEF("jmath", 567, "\xc8\xb7"),
+ ENTITY_DEF("nrtrie", 8941, "\xe2\x8b\xad"),
+ ENTITY_DEF("nsup", 8837, "\xe2\x8a\x85"),
+ ENTITY_DEF("Ubrcy", 1038, "\xd0\x8e"),
+ ENTITY_DEF("succnsim", 8937, "\xe2\x8b\xa9"),
+ ENTITY_DEF("nesim", 8770, "\xe2\x89\x82\xcc\xb8"),
+ ENTITY_DEF("varepsilon", 1013, "\xcf\xb5"),
+ ENTITY_DEF("DoubleRightTee", 8872, "\xe2\x8a\xa8"),
+ ENTITY_DEF_HEUR("not", 172, "\xc2\xac"),
+ ENTITY_DEF("lesdot", 10879, "\xe2\xa9\xbf"),
+ ENTITY_DEF("backepsilon", 1014, "\xcf\xb6"),
+ ENTITY_DEF("srarr", 8594, "\xe2\x86\x92"),
+ ENTITY_DEF("varsubsetneqq", 10955, "\xe2\xab\x8b\xef\xb8\x80"),
+ ENTITY_DEF("sqcap", 8851, "\xe2\x8a\x93"),
+ ENTITY_DEF("rightleftarrows", 8644, "\xe2\x87\x84"),
+ ENTITY_DEF("diams", 9830, "\xe2\x99\xa6"),
+ ENTITY_DEF("boxdR", 9554, "\xe2\x95\x92"),
+ ENTITY_DEF("ngeqslant", 10878, "\xe2\xa9\xbe\xcc\xb8"),
+ ENTITY_DEF("boxDR", 9556, "\xe2\x95\x94"),
+ ENTITY_DEF("sext", 10038, "\xe2\x9c\xb6"),
+ ENTITY_DEF("backsim", 8765, "\xe2\x88\xbd"),
+ ENTITY_DEF("nfr", 120107, "\xf0\x9d\x94\xab"),
+ ENTITY_DEF("CloseCurlyDoubleQuote", 8221, "\xe2\x80\x9d"),
+ ENTITY_DEF("npart", 8706, "\xe2\x88\x82\xcc\xb8"),
+ ENTITY_DEF("dharl", 8643, "\xe2\x87\x83"),
+ ENTITY_DEF("NewLine", 10, "\x0a"),
+ ENTITY_DEF("bigotimes", 10754, "\xe2\xa8\x82"),
+ ENTITY_DEF("lAtail", 10523, "\xe2\xa4\x9b"),
+ ENTITY_DEF_HEUR("frac14", 188, "\xc2\xbc"),
+ ENTITY_DEF("or", 8744, "\xe2\x88\xa8"),
+ ENTITY_DEF("subedot", 10947, "\xe2\xab\x83"),
+ ENTITY_DEF("nmid", 8740, "\xe2\x88\xa4"),
+ ENTITY_DEF("DownArrowUpArrow", 8693, "\xe2\x87\xb5"),
+ ENTITY_DEF("icy", 1080, "\xd0\xb8"),
+ ENTITY_DEF("num", 35, "\x23"),
+ ENTITY_DEF("Gdot", 288, "\xc4\xa0"),
+ ENTITY_DEF("urcrop", 8974, "\xe2\x8c\x8e"),
+ ENTITY_DEF("epsiv", 1013, "\xcf\xb5"),
+ ENTITY_DEF("topcir", 10993, "\xe2\xab\xb1"),
+ ENTITY_DEF("ne", 8800, "\xe2\x89\xa0"),
+ ENTITY_DEF("osol", 8856, "\xe2\x8a\x98"),
+ ENTITY_DEF_HEUR("amp", 38, "\x26"),
+ ENTITY_DEF("ncap", 10819, "\xe2\xa9\x83"),
+ ENTITY_DEF("Sscr", 119982, "\xf0\x9d\x92\xae"),
+ ENTITY_DEF("sung", 9834, "\xe2\x99\xaa"),
+ ENTITY_DEF("ltri", 9667, "\xe2\x97\x83"),
+ ENTITY_DEF("frac25", 8534, "\xe2\x85\x96"),
+ ENTITY_DEF("DZcy", 1039, "\xd0\x8f"),
+ ENTITY_DEF("RightUpVector", 8638, "\xe2\x86\xbe"),
+ ENTITY_DEF("rsquor", 8217, "\xe2\x80\x99"),
+ ENTITY_DEF("uplus", 8846, "\xe2\x8a\x8e"),
+ ENTITY_DEF("triangleright", 9657, "\xe2\x96\xb9"),
+ ENTITY_DEF("lAarr", 8666, "\xe2\x87\x9a"),
+ ENTITY_DEF("HilbertSpace", 8459, "\xe2\x84\x8b"),
+ ENTITY_DEF("there4", 8756, "\xe2\x88\xb4"),
+ ENTITY_DEF("vscr", 120011, "\xf0\x9d\x93\x8b"),
+ ENTITY_DEF("cirscir", 10690, "\xe2\xa7\x82"),
+ ENTITY_DEF("roarr", 8702, "\xe2\x87\xbe"),
+ ENTITY_DEF("hslash", 8463, "\xe2\x84\x8f"),
+ ENTITY_DEF("supdsub", 10968, "\xe2\xab\x98"),
+ ENTITY_DEF("simg", 10910, "\xe2\xaa\x9e"),
+ ENTITY_DEF("trade", 8482, "\xe2\x84\xa2"),
+ ENTITY_DEF("searrow", 8600, "\xe2\x86\x98"),
+ ENTITY_DEF("DownLeftVector", 8637, "\xe2\x86\xbd"),
+ ENTITY_DEF("FilledSmallSquare", 9724, "\xe2\x97\xbc"),
+ ENTITY_DEF("prod", 8719, "\xe2\x88\x8f"),
+ ENTITY_DEF("oror", 10838, "\xe2\xa9\x96"),
+ ENTITY_DEF("udarr", 8645, "\xe2\x87\x85"),
+ ENTITY_DEF("jsercy", 1112, "\xd1\x98"),
+ ENTITY_DEF("tprime", 8244, "\xe2\x80\xb4"),
+ ENTITY_DEF("bprime", 8245, "\xe2\x80\xb5"),
+ ENTITY_DEF("malt", 10016, "\xe2\x9c\xa0"),
+ ENTITY_DEF("bigcup", 8899, "\xe2\x8b\x83"),
+ ENTITY_DEF("oint", 8750, "\xe2\x88\xae"),
+ ENTITY_DEF("female", 9792, "\xe2\x99\x80"),
+ ENTITY_DEF("omacr", 333, "\xc5\x8d"),
+ ENTITY_DEF("SquareSubsetEqual", 8849, "\xe2\x8a\x91"),
+ ENTITY_DEF("SucceedsEqual", 10928, "\xe2\xaa\xb0"),
+ ENTITY_DEF("plusacir", 10787, "\xe2\xa8\xa3"),
+ ENTITY_DEF("Gcirc", 284, "\xc4\x9c"),
+ ENTITY_DEF("lesdotor", 10883, "\xe2\xaa\x83"),
+ ENTITY_DEF("escr", 8495, "\xe2\x84\xaf"),
+ ENTITY_DEF_HEUR("THORN", 222, "\xc3\x9e"),
+ ENTITY_DEF("UpArrowBar", 10514, "\xe2\xa4\x92"),
+ ENTITY_DEF("nvrtrie", 8885, "\xe2\x8a\xb5\xe2\x83\x92"),
+ ENTITY_DEF("varkappa", 1008, "\xcf\xb0"),
+ ENTITY_DEF("NotReverseElement", 8716, "\xe2\x88\x8c"),
+ ENTITY_DEF("zdot", 380, "\xc5\xbc"),
+ ENTITY_DEF("ExponentialE", 8519, "\xe2\x85\x87"),
+ ENTITY_DEF("lesseqgtr", 8922, "\xe2\x8b\x9a"),
+ ENTITY_DEF("cscr", 119992, "\xf0\x9d\x92\xb8"),
+ ENTITY_DEF("Dscr", 119967, "\xf0\x9d\x92\x9f"),
+ ENTITY_DEF("lthree", 8907, "\xe2\x8b\x8b"),
+ ENTITY_DEF("Ccedil", 199, "\xc3\x87"),
+ ENTITY_DEF("nge", 8817, "\xe2\x89\xb1"),
+ ENTITY_DEF("UpperLeftArrow", 8598, "\xe2\x86\x96"),
+ ENTITY_DEF("vDash", 8872, "\xe2\x8a\xa8"),
+ ENTITY_DEF("efDot", 8786, "\xe2\x89\x92"),
+ ENTITY_DEF("telrec", 8981, "\xe2\x8c\x95"),
+ ENTITY_DEF("vellip", 8942, "\xe2\x8b\xae"),
+ ENTITY_DEF("nrArr", 8655, "\xe2\x87\x8f"),
+ ENTITY_DEF_HEUR("ugrave", 249, "\xc3\xb9"),
+ ENTITY_DEF("uring", 367, "\xc5\xaf"),
+ ENTITY_DEF("Bernoullis", 8492, "\xe2\x84\xac"),
+ ENTITY_DEF("nles", 10877, "\xe2\xa9\xbd\xcc\xb8"),
+ ENTITY_DEF_HEUR("macr", 175, "\xc2\xaf"),
+ ENTITY_DEF("boxuR", 9560, "\xe2\x95\x98"),
+ ENTITY_DEF("clubsuit", 9827, "\xe2\x99\xa3"),
+ ENTITY_DEF("rightarrowtail", 8611, "\xe2\x86\xa3"),
+ ENTITY_DEF("epar", 8917, "\xe2\x8b\x95"),
+ ENTITY_DEF("ltcc", 10918, "\xe2\xaa\xa6"),
+ ENTITY_DEF("twoheadleftarrow", 8606, "\xe2\x86\x9e"),
+ ENTITY_DEF("aleph", 8501, "\xe2\x84\xb5"),
+ ENTITY_DEF("Colon", 8759, "\xe2\x88\xb7"),
+ ENTITY_DEF("vltri", 8882, "\xe2\x8a\xb2"),
+ ENTITY_DEF("quaternions", 8461, "\xe2\x84\x8d"),
+ ENTITY_DEF("rfr", 120111, "\xf0\x9d\x94\xaf"),
+ ENTITY_DEF_HEUR("Ouml", 214, "\xc3\x96"),
+ ENTITY_DEF("rsh", 8625, "\xe2\x86\xb1"),
+ ENTITY_DEF("emptyv", 8709, "\xe2\x88\x85"),
+ ENTITY_DEF("sqsup", 8848, "\xe2\x8a\x90"),
+ ENTITY_DEF("marker", 9646, "\xe2\x96\xae"),
+ ENTITY_DEF("Efr", 120072, "\xf0\x9d\x94\x88"),
+ ENTITY_DEF("DotEqual", 8784, "\xe2\x89\x90"),
+ ENTITY_DEF("eqsim", 8770, "\xe2\x89\x82"),
+ ENTITY_DEF("NotSucceedsEqual", 10928, "\xe2\xaa\xb0\xcc\xb8"),
+ ENTITY_DEF("primes", 8473, "\xe2\x84\x99"),
+ ENTITY_DEF_HEUR("times", 215, "\xc3\x97"),
+ ENTITY_DEF("rangd", 10642, "\xe2\xa6\x92"),
+ ENTITY_DEF("rightharpoonup", 8640, "\xe2\x87\x80"),
+ ENTITY_DEF("lrhard", 10605, "\xe2\xa5\xad"),
+ ENTITY_DEF("ape", 8778, "\xe2\x89\x8a"),
+ ENTITY_DEF("varsupsetneq", 8843, "\xe2\x8a\x8b\xef\xb8\x80"),
+ ENTITY_DEF("larrlp", 8619, "\xe2\x86\xab"),
+ ENTITY_DEF("NotPrecedesEqual", 10927, "\xe2\xaa\xaf\xcc\xb8"),
+ ENTITY_DEF("ulcorner", 8988, "\xe2\x8c\x9c"),
+ ENTITY_DEF("acd", 8767, "\xe2\x88\xbf"),
+ ENTITY_DEF("Hacek", 711, "\xcb\x87"),
+ ENTITY_DEF("xuplus", 10756, "\xe2\xa8\x84"),
+ ENTITY_DEF("therefore", 8756, "\xe2\x88\xb4"),
+ ENTITY_DEF("YIcy", 1031, "\xd0\x87"),
+ ENTITY_DEF("Tfr", 120087, "\xf0\x9d\x94\x97"),
+ ENTITY_DEF("Jcirc", 308, "\xc4\xb4"),
+ ENTITY_DEF("LessGreater", 8822, "\xe2\x89\xb6"),
+ ENTITY_DEF("Uring", 366, "\xc5\xae"),
+ ENTITY_DEF("Ugrave", 217, "\xc3\x99"),
+ ENTITY_DEF("rarr", 8594, "\xe2\x86\x92"),
+ ENTITY_DEF("wopf", 120168, "\xf0\x9d\x95\xa8"),
+ ENTITY_DEF("imath", 305, "\xc4\xb1"),
+ ENTITY_DEF("Yopf", 120144, "\xf0\x9d\x95\x90"),
+ ENTITY_DEF("colone", 8788, "\xe2\x89\x94"),
+ ENTITY_DEF("csube", 10961, "\xe2\xab\x91"),
+ ENTITY_DEF("odash", 8861, "\xe2\x8a\x9d"),
+ ENTITY_DEF("olarr", 8634, "\xe2\x86\xba"),
+ ENTITY_DEF("angrt", 8735, "\xe2\x88\x9f"),
+ ENTITY_DEF("NotLeftTriangleBar", 10703, "\xe2\xa7\x8f\xcc\xb8"),
+ ENTITY_DEF("GreaterEqual", 8805, "\xe2\x89\xa5"),
+ ENTITY_DEF("scnap", 10938, "\xe2\xaa\xba"),
+ ENTITY_DEF("pi", 960, "\xcf\x80"),
+ ENTITY_DEF("lesg", 8922, "\xe2\x8b\x9a\xef\xb8\x80"),
+ ENTITY_DEF("orderof", 8500, "\xe2\x84\xb4"),
+ ENTITY_DEF_HEUR("uacute", 250, "\xc3\xba"),
+ ENTITY_DEF("Barv", 10983, "\xe2\xab\xa7"),
+ ENTITY_DEF("Theta", 920, "\xce\x98"),
+ ENTITY_DEF("leftrightsquigarrow", 8621, "\xe2\x86\xad"),
+ ENTITY_DEF("Atilde", 195, "\xc3\x83"),
+ ENTITY_DEF("cupdot", 8845, "\xe2\x8a\x8d"),
+ ENTITY_DEF("ntriangleright", 8939, "\xe2\x8b\xab"),
+ ENTITY_DEF("measuredangle", 8737, "\xe2\x88\xa1"),
+ ENTITY_DEF("jscr", 119999, "\xf0\x9d\x92\xbf"),
+ ENTITY_DEF("inodot", 305, "\xc4\xb1"),
+ ENTITY_DEF("mopf", 120158, "\xf0\x9d\x95\x9e"),
+ ENTITY_DEF("hkswarow", 10534, "\xe2\xa4\xa6"),
+ ENTITY_DEF("lopar", 10629, "\xe2\xa6\x85"),
+ ENTITY_DEF("thksim", 8764, "\xe2\x88\xbc"),
+ ENTITY_DEF("bkarow", 10509, "\xe2\xa4\x8d"),
+ ENTITY_DEF("rarrfs", 10526, "\xe2\xa4\x9e"),
+ ENTITY_DEF("ntrianglelefteq", 8940, "\xe2\x8b\xac"),
+ ENTITY_DEF("Bscr", 8492, "\xe2\x84\xac"),
+ ENTITY_DEF("topf", 120165, "\xf0\x9d\x95\xa5"),
+ ENTITY_DEF("Uacute", 218, "\xc3\x9a"),
+ ENTITY_DEF("lap", 10885, "\xe2\xaa\x85"),
+ ENTITY_DEF("djcy", 1106, "\xd1\x92"),
+ ENTITY_DEF("bopf", 120147, "\xf0\x9d\x95\x93"),
+ ENTITY_DEF("empty", 8709, "\xe2\x88\x85"),
+ ENTITY_DEF("LeftAngleBracket", 10216, "\xe2\x9f\xa8"),
+ ENTITY_DEF("Imacr", 298, "\xc4\xaa"),
+ ENTITY_DEF("ltcir", 10873, "\xe2\xa9\xb9"),
+ ENTITY_DEF("trisb", 10701, "\xe2\xa7\x8d"),
+ ENTITY_DEF("gjcy", 1107, "\xd1\x93"),
+ ENTITY_DEF("pr", 8826, "\xe2\x89\xba"),
+ ENTITY_DEF("Mu", 924, "\xce\x9c"),
+ ENTITY_DEF("ogon", 731, "\xcb\x9b"),
+ ENTITY_DEF("pertenk", 8241, "\xe2\x80\xb1"),
+ ENTITY_DEF("plustwo", 10791, "\xe2\xa8\xa7"),
+ ENTITY_DEF("Vfr", 120089, "\xf0\x9d\x94\x99"),
+ ENTITY_DEF("ApplyFunction", 8289, "\xe2\x81\xa1"),
+ ENTITY_DEF("Sub", 8912, "\xe2\x8b\x90"),
+ ENTITY_DEF("DoubleLeftRightArrow", 8660, "\xe2\x87\x94"),
+ ENTITY_DEF("Lmidot", 319, "\xc4\xbf"),
+ ENTITY_DEF("nwarrow", 8598, "\xe2\x86\x96"),
+ ENTITY_DEF("angrtvbd", 10653, "\xe2\xa6\x9d"),
+ ENTITY_DEF("fcy", 1092, "\xd1\x84"),
+ ENTITY_DEF("ltlarr", 10614, "\xe2\xa5\xb6"),
+ ENTITY_DEF("CircleMinus", 8854, "\xe2\x8a\x96"),
+ ENTITY_DEF("angmsdab", 10665, "\xe2\xa6\xa9"),
+ ENTITY_DEF("wedgeq", 8793, "\xe2\x89\x99"),
+ ENTITY_DEF("iogon", 303, "\xc4\xaf"),
+ ENTITY_DEF_HEUR("laquo", 171, "\xc2\xab"),
+ ENTITY_DEF("NestedGreaterGreater", 8811, "\xe2\x89\xab"),
+ ENTITY_DEF("UnionPlus", 8846, "\xe2\x8a\x8e"),
+ ENTITY_DEF("CircleDot", 8857, "\xe2\x8a\x99"),
+ ENTITY_DEF("coloneq", 8788, "\xe2\x89\x94"),
+ ENTITY_DEF("csupe", 10962, "\xe2\xab\x92"),
+ ENTITY_DEF("tcaron", 357, "\xc5\xa5"),
+ ENTITY_DEF("GreaterTilde", 8819, "\xe2\x89\xb3"),
+ ENTITY_DEF("Map", 10501, "\xe2\xa4\x85"),
+ ENTITY_DEF("DoubleLongLeftArrow", 10232, "\xe2\x9f\xb8"),
+ ENTITY_DEF("Uparrow", 8657, "\xe2\x87\x91"),
+ ENTITY_DEF("scy", 1089, "\xd1\x81"),
+ ENTITY_DEF("llarr", 8647, "\xe2\x87\x87"),
+ ENTITY_DEF("rangle", 10217, "\xe2\x9f\xa9"),
+ ENTITY_DEF("sstarf", 8902, "\xe2\x8b\x86"),
+ ENTITY_DEF("InvisibleTimes", 8290, "\xe2\x81\xa2"),
+ ENTITY_DEF("egsdot", 10904, "\xe2\xaa\x98"),
+ ENTITY_DEF("target", 8982, "\xe2\x8c\x96"),
+ ENTITY_DEF("lesges", 10899, "\xe2\xaa\x93"),
+ ENTITY_DEF_HEUR("curren", 164, "\xc2\xa4"),
+ ENTITY_DEF("yopf", 120170, "\xf0\x9d\x95\xaa"),
+ ENTITY_DEF("frac23", 8532, "\xe2\x85\x94"),
+ ENTITY_DEF("NotSucceedsTilde", 8831, "\xe2\x89\xbf\xcc\xb8"),
+ ENTITY_DEF("napprox", 8777, "\xe2\x89\x89"),
+ ENTITY_DEF("odblac", 337, "\xc5\x91"),
+ ENTITY_DEF("gammad", 989, "\xcf\x9d"),
+ ENTITY_DEF("dscr", 119993, "\xf0\x9d\x92\xb9"),
+ ENTITY_DEF("SupersetEqual", 8839, "\xe2\x8a\x87"),
+ ENTITY_DEF("squf", 9642, "\xe2\x96\xaa"),
+ ENTITY_DEF("Because", 8757, "\xe2\x88\xb5"),
+ ENTITY_DEF("sccue", 8829, "\xe2\x89\xbd"),
+ ENTITY_DEF("KHcy", 1061, "\xd0\xa5"),
+ ENTITY_DEF("Wcirc", 372, "\xc5\xb4"),
+ ENTITY_DEF("uparrow", 8593, "\xe2\x86\x91"),
+ ENTITY_DEF("lessgtr", 8822, "\xe2\x89\xb6"),
+ ENTITY_DEF("thickapprox", 8776, "\xe2\x89\x88"),
+ ENTITY_DEF("lbrksld", 10639, "\xe2\xa6\x8f"),
+ ENTITY_DEF_HEUR("oslash", 248, "\xc3\xb8"),
+ ENTITY_DEF("NotCupCap", 8813, "\xe2\x89\xad"),
+ ENTITY_DEF("elinters", 9191, "\xe2\x8f\xa7"),
+ ENTITY_DEF("Assign", 8788, "\xe2\x89\x94"),
+ ENTITY_DEF("ClockwiseContourIntegral", 8754, "\xe2\x88\xb2"),
+ ENTITY_DEF("lfisht", 10620, "\xe2\xa5\xbc"),
+ ENTITY_DEF("DownArrow", 8595, "\xe2\x86\x93"),
+ ENTITY_DEF("Zdot", 379, "\xc5\xbb"),
+ ENTITY_DEF("xscr", 120013, "\xf0\x9d\x93\x8d"),
+ ENTITY_DEF("DiacriticalGrave", 96, "\x60"),
+ ENTITY_DEF("DoubleLongLeftRightArrow", 10234, "\xe2\x9f\xba"),
+ ENTITY_DEF("angle", 8736, "\xe2\x88\xa0"),
+ ENTITY_DEF("race", 8765, "\xe2\x88\xbd\xcc\xb1"),
+ ENTITY_DEF("Ascr", 119964, "\xf0\x9d\x92\x9c"),
+ ENTITY_DEF("Xscr", 119987, "\xf0\x9d\x92\xb3"),
+ ENTITY_DEF_HEUR("acirc", 226, "\xc3\xa2"),
+ ENTITY_DEF("otimesas", 10806, "\xe2\xa8\xb6"),
+ ENTITY_DEF("gscr", 8458, "\xe2\x84\x8a"),
+ ENTITY_DEF("gcy", 1075, "\xd0\xb3"),
+ ENTITY_DEF("angmsdag", 10670, "\xe2\xa6\xae"),
+ ENTITY_DEF("tshcy", 1115, "\xd1\x9b"),
+ ENTITY_DEF("Acy", 1040, "\xd0\x90"),
+ ENTITY_DEF("NotGreaterLess", 8825, "\xe2\x89\xb9"),
+ ENTITY_DEF("dtdot", 8945, "\xe2\x8b\xb1"),
+ ENTITY_DEF_HEUR("quot", 34, "\x22"),
+ ENTITY_DEF_HEUR("micro", 181, "\xc2\xb5"),
+ ENTITY_DEF("simplus", 10788, "\xe2\xa8\xa4"),
+ ENTITY_DEF("nsupseteq", 8841, "\xe2\x8a\x89"),
+ ENTITY_DEF("Ufr", 120088, "\xf0\x9d\x94\x98"),
+ ENTITY_DEF("Pr", 10939, "\xe2\xaa\xbb"),
+ ENTITY_DEF("napid", 8779, "\xe2\x89\x8b\xcc\xb8"),
+ ENTITY_DEF("rceil", 8969, "\xe2\x8c\x89"),
+ ENTITY_DEF("boxtimes", 8864, "\xe2\x8a\xa0"),
+ ENTITY_DEF("erarr", 10609, "\xe2\xa5\xb1"),
+ ENTITY_DEF("downdownarrows", 8650, "\xe2\x87\x8a"),
+ ENTITY_DEF("Kfr", 120078, "\xf0\x9d\x94\x8e"),
+ ENTITY_DEF("mho", 8487, "\xe2\x84\xa7"),
+ ENTITY_DEF("scpolint", 10771, "\xe2\xa8\x93"),
+ ENTITY_DEF("vArr", 8661, "\xe2\x87\x95"),
+ ENTITY_DEF("Ccaron", 268, "\xc4\x8c"),
+ ENTITY_DEF("NotRightTriangle", 8939, "\xe2\x8b\xab"),
+ ENTITY_DEF("topbot", 9014, "\xe2\x8c\xb6"),
+ ENTITY_DEF("qopf", 120162, "\xf0\x9d\x95\xa2"),
+ ENTITY_DEF("eogon", 281, "\xc4\x99"),
+ ENTITY_DEF("luruhar", 10598, "\xe2\xa5\xa6"),
+ ENTITY_DEF("gtdot", 8919, "\xe2\x8b\x97"),
+ ENTITY_DEF("Egrave", 200, "\xc3\x88"),
+ ENTITY_DEF("roplus", 10798, "\xe2\xa8\xae"),
+ ENTITY_DEF("Intersection", 8898, "\xe2\x8b\x82"),
+ ENTITY_DEF("Uarr", 8607, "\xe2\x86\x9f"),
+ ENTITY_DEF("dcy", 1076, "\xd0\xb4"),
+ ENTITY_DEF("boxvl", 9508, "\xe2\x94\xa4"),
+ ENTITY_DEF("RightArrowBar", 8677, "\xe2\x87\xa5"),
+ ENTITY_DEF_HEUR("yuml", 255, "\xc3\xbf"),
+ ENTITY_DEF("parallel", 8741, "\xe2\x88\xa5"),
+ ENTITY_DEF("succneqq", 10934, "\xe2\xaa\xb6"),
+ ENTITY_DEF("bemptyv", 10672, "\xe2\xa6\xb0"),
+ ENTITY_DEF("starf", 9733, "\xe2\x98\x85"),
+ ENTITY_DEF("OverBar", 8254, "\xe2\x80\xbe"),
+ ENTITY_DEF("Alpha", 913, "\xce\x91"),
+ ENTITY_DEF("LeftUpVectorBar", 10584, "\xe2\xa5\x98"),
+ ENTITY_DEF("ufr", 120114, "\xf0\x9d\x94\xb2"),
+ ENTITY_DEF("swarhk", 10534, "\xe2\xa4\xa6"),
+ ENTITY_DEF("GreaterEqualLess", 8923, "\xe2\x8b\x9b"),
+ ENTITY_DEF("sscr", 120008, "\xf0\x9d\x93\x88"),
+ ENTITY_DEF("Pi", 928, "\xce\xa0"),
+ ENTITY_DEF("boxh", 9472, "\xe2\x94\x80"),
+ ENTITY_DEF("frac16", 8537, "\xe2\x85\x99"),
+ ENTITY_DEF("lbrack", 91, "\x5b"),
+ ENTITY_DEF("vert", 124, "\x7c"),
+ ENTITY_DEF("precneqq", 10933, "\xe2\xaa\xb5"),
+ ENTITY_DEF("NotGreaterSlantEqual", 10878, "\xe2\xa9\xbe\xcc\xb8"),
+ ENTITY_DEF("Omega", 937, "\xce\xa9"),
+ ENTITY_DEF("uarr", 8593, "\xe2\x86\x91"),
+ ENTITY_DEF("boxVr", 9567, "\xe2\x95\x9f"),
+ ENTITY_DEF("ruluhar", 10600, "\xe2\xa5\xa8"),
+ ENTITY_DEF("ShortLeftArrow", 8592, "\xe2\x86\x90"),
+ ENTITY_DEF("Qfr", 120084, "\xf0\x9d\x94\x94"),
+ ENTITY_DEF("olt", 10688, "\xe2\xa7\x80"),
+ ENTITY_DEF("nequiv", 8802, "\xe2\x89\xa2"),
+ ENTITY_DEF("fscr", 119995, "\xf0\x9d\x92\xbb"),
+ ENTITY_DEF("rarrhk", 8618, "\xe2\x86\xaa"),
+ ENTITY_DEF("nsqsupe", 8931, "\xe2\x8b\xa3"),
+ ENTITY_DEF("nsubseteq", 8840, "\xe2\x8a\x88"),
+ ENTITY_DEF("numero", 8470, "\xe2\x84\x96"),
+ ENTITY_DEF("emsp14", 8197, "\xe2\x80\x85"),
+ ENTITY_DEF("gl", 8823, "\xe2\x89\xb7"),
+ ENTITY_DEF("ocirc", 244, "\xc3\xb4"),
+ ENTITY_DEF("weierp", 8472, "\xe2\x84\x98"),
+ ENTITY_DEF("boxvL", 9569, "\xe2\x95\xa1"),
+ ENTITY_DEF("RightArrowLeftArrow", 8644, "\xe2\x87\x84"),
+ ENTITY_DEF("Precedes", 8826, "\xe2\x89\xba"),
+ ENTITY_DEF("RightVector", 8640, "\xe2\x87\x80"),
+ ENTITY_DEF("xcup", 8899, "\xe2\x8b\x83"),
+ ENTITY_DEF("angmsdad", 10667, "\xe2\xa6\xab"),
+ ENTITY_DEF("gtrsim", 8819, "\xe2\x89\xb3"),
+ ENTITY_DEF("natural", 9838, "\xe2\x99\xae"),
+ ENTITY_DEF("nVdash", 8878, "\xe2\x8a\xae"),
+ ENTITY_DEF("RightTriangleEqual", 8885, "\xe2\x8a\xb5"),
+ ENTITY_DEF("dscy", 1109, "\xd1\x95"),
+ ENTITY_DEF("leftthreetimes", 8907, "\xe2\x8b\x8b"),
+ ENTITY_DEF("prsim", 8830, "\xe2\x89\xbe"),
+ ENTITY_DEF("Bcy", 1041, "\xd0\x91"),
+ ENTITY_DEF("Chi", 935, "\xce\xa7"),
+ ENTITY_DEF("timesb", 8864, "\xe2\x8a\xa0"),
+ ENTITY_DEF("Del", 8711, "\xe2\x88\x87"),
+ ENTITY_DEF("lmidot", 320, "\xc5\x80"),
+ ENTITY_DEF("RightDownVector", 8642, "\xe2\x87\x82"),
+ ENTITY_DEF("simdot", 10858, "\xe2\xa9\xaa"),
+ ENTITY_DEF("FilledVerySmallSquare", 9642, "\xe2\x96\xaa"),
+ ENTITY_DEF("NotLessSlantEqual", 10877, "\xe2\xa9\xbd\xcc\xb8"),
+ ENTITY_DEF("SucceedsTilde", 8831, "\xe2\x89\xbf"),
+ ENTITY_DEF("duarr", 8693, "\xe2\x87\xb5"),
+ ENTITY_DEF("apE", 10864, "\xe2\xa9\xb0"),
+ ENTITY_DEF("odot", 8857, "\xe2\x8a\x99"),
+ ENTITY_DEF("mldr", 8230, "\xe2\x80\xa6"),
+ ENTITY_DEF("Uarrocir", 10569, "\xe2\xa5\x89"),
+ ENTITY_DEF("nLl", 8920, "\xe2\x8b\x98\xcc\xb8"),
+ ENTITY_DEF("rarrpl", 10565, "\xe2\xa5\x85"),
+ ENTITY_DEF("cir", 9675, "\xe2\x97\x8b"),
+ ENTITY_DEF("blk14", 9617, "\xe2\x96\x91"),
+ ENTITY_DEF("VerticalLine", 124, "\x7c"),
+ ENTITY_DEF("jcy", 1081, "\xd0\xb9"),
+ ENTITY_DEF("filig", 64257, "\xef\xac\x81"),
+ ENTITY_DEF("LongRightArrow", 10230, "\xe2\x9f\xb6"),
+ ENTITY_DEF("beta", 946, "\xce\xb2"),
+ ENTITY_DEF("ccupssm", 10832, "\xe2\xa9\x90"),
+ ENTITY_DEF("supsub", 10964, "\xe2\xab\x94"),
+ ENTITY_DEF("spar", 8741, "\xe2\x88\xa5"),
+ ENTITY_DEF("Tstrok", 358, "\xc5\xa6"),
+ ENTITY_DEF("isinv", 8712, "\xe2\x88\x88"),
+ ENTITY_DEF("rightsquigarrow", 8605, "\xe2\x86\x9d"),
+ ENTITY_DEF("Diamond", 8900, "\xe2\x8b\x84"),
+ ENTITY_DEF("curlyeqsucc", 8927, "\xe2\x8b\x9f"),
+ ENTITY_DEF("ijlig", 307, "\xc4\xb3"),
+ ENTITY_DEF("puncsp", 8200, "\xe2\x80\x88"),
+ ENTITY_DEF("hamilt", 8459, "\xe2\x84\x8b"),
+ ENTITY_DEF("mapstoleft", 8612, "\xe2\x86\xa4"),
+ ENTITY_DEF("Copf", 8450, "\xe2\x84\x82"),
+ ENTITY_DEF("prnsim", 8936, "\xe2\x8b\xa8"),
+ ENTITY_DEF("DotDot", 8412, "\xe2\x83\x9c"),
+ ENTITY_DEF("lobrk", 10214, "\xe2\x9f\xa6"),
+ ENTITY_DEF("twoheadrightarrow", 8608, "\xe2\x86\xa0"),
+ ENTITY_DEF("ngE", 8807, "\xe2\x89\xa7\xcc\xb8"),
+ ENTITY_DEF("cylcty", 9005, "\xe2\x8c\xad"),
+ ENTITY_DEF("sube", 8838, "\xe2\x8a\x86"),
+ ENTITY_DEF("NotEqualTilde", 8770, "\xe2\x89\x82\xcc\xb8"),
+ ENTITY_DEF_HEUR("Yuml", 376, "\xc5\xb8"),
+ ENTITY_DEF("comp", 8705, "\xe2\x88\x81"),
+ ENTITY_DEF("dotminus", 8760, "\xe2\x88\xb8"),
+ ENTITY_DEF("crarr", 8629, "\xe2\x86\xb5"),
+ ENTITY_DEF("imped", 437, "\xc6\xb5"),
+ ENTITY_DEF("barwedge", 8965, "\xe2\x8c\x85"),
+ ENTITY_DEF("harrcir", 10568, "\xe2\xa5\x88")};
+
+class html_entities_storage {
+ ankerl::unordered_dense::map<std::string_view, html_entity_def> entity_by_name;
+ ankerl::unordered_dense::map<std::string_view, html_entity_def> entity_by_name_heur;
+ ankerl::unordered_dense::map<unsigned, html_entity_def> entity_by_id;
+
+public:
+ html_entities_storage()
+ {
+ auto nelts = G_N_ELEMENTS(html_entities_array);
+ entity_by_name.reserve(nelts);
+ entity_by_id.reserve(nelts);
+
+ for (const auto &e: html_entities_array) {
+ entity_by_name[e.name] = e;
+ entity_by_id[e.code] = e;
+
+ if (e.allow_heuristic) {
+ entity_by_name_heur[e.name] = e;
+ }
+ }
+ }
+
+ auto by_name(std::string_view name, bool use_heuristic = false) const -> const html_entity_def *
+ {
+ const decltype(entity_by_name) *htb;
+
+ if (use_heuristic) {
+ htb = &entity_by_name_heur;
+ }
+ else {
+ htb = &entity_by_name;
+ }
+ auto it = htb->find(name);
+
+ if (it != htb->end()) {
+ return &(it->second);
+ }
+
+ return nullptr;
+ }
+
+ auto by_id(int id) const -> const html_entity_def *
+ {
+ auto it = entity_by_id.find(id);
+ if (it != entity_by_id.end()) {
+ return &(it->second);
+ }
+
+ return nullptr;
+ }
+};
+
+static const html_entities_storage html_entities_defs;
+
+std::size_t
+decode_html_entitles_inplace(char *s, std::size_t len, bool norm_spaces)
+{
+ /*
+ * t - tortoise (destination ptr)
+ * h - hare (source ptr)
+ * e - begin of entity
+ */
+ char *t = s, *h = s, *e = s;
+ const gchar *end;
+ bool seen_hash = false, seen_hex = false;
+ enum {
+ do_undefined,
+ do_digits_only,
+ do_mixed,
+ } seen_digit_only;
+ enum class parser_state {
+ normal_content,
+ ampersand,
+ skip_multi_spaces,
+ skip_start_spaces,
+ } state = parser_state::normal_content;
+
+ end = s + len;
+
+ auto replace_named_entity = [&](const char *entity, std::size_t len) -> bool {
+ const auto *entity_def = html_entities_defs.by_name({entity,
+ (std::size_t)(h - entity)},
+ false);
+
+ auto replace_entity = [&]() -> void {
+ auto l = strlen(entity_def->replacement);
+ memcpy(t, entity_def->replacement, l);
+ t += l;
+ };
+
+ if (entity_def) {
+ replace_entity();
+ return true;
+ }
+ else {
+ /* Try heuristic */
+ auto heuristic_lookup_func = [&](std::size_t lookup_len) -> bool {
+ if (!entity_def && h - e > lookup_len) {
+ entity_def = html_entities_defs.by_name({entity, lookup_len}, true);
+
+ if (entity_def) {
+ replace_entity();
+ /* Adjust h back */
+ h = e + lookup_len;
+
+ return true;
+ }
+
+ entity_def = nullptr;
+ }
+
+ return false;
+ };
+
+ heuristic_lookup_func(5);
+ heuristic_lookup_func(4);
+ heuristic_lookup_func(3);
+ heuristic_lookup_func(2);
+
+ /* Leave undecoded */
+ if (!entity_def && (end - t > h - e)) {
+ memmove(t, e, h - e);
+ t += h - e;
+ }
+ else if (entity_def) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ /* Strtoul works merely for 0 terminated strings, so leave it alone... */
+ auto dec_to_int = [](const char *str, std::size_t len) -> std::optional<int> {
+ int n = 0;
+
+ /* Avoid INT_MIN overflow by moving to negative numbers */
+ while (len > 0 && g_ascii_isdigit(*str)) {
+ n = 10 * n - (*str++ - '0');
+ len--;
+ }
+
+ if (len == 0) {
+ return -(n);
+ }
+ else {
+ return std::nullopt;
+ }
+ };
+ auto hex_to_int = [](const char *str, std::size_t len) -> std::optional<int> {
+ int n = 0;
+
+ /* Avoid INT_MIN overflow by moving to negative numbers */
+ while (len > 0 && g_ascii_isxdigit(*str)) {
+ if (*str <= 0x39) {
+ n = 16 * n - (*str++ - '0');
+ }
+ else {
+ n = 16 * n - (((*str++) | ' ') - 'a' + 10);
+ }
+ len--;
+ }
+
+ if (len == 0) {
+ return -(n);
+ }
+ else {
+ return std::nullopt;
+ }
+ };
+ auto oct_to_int = [](const char *str, std::size_t len) -> std::optional<int> {
+ int n = 0;
+
+ /* Avoid INT_MIN overflow by moving to negative numbers */
+ while (len > 0 && g_ascii_isdigit(*str)) {
+ if (*str > '7') {
+ break;
+ }
+ else {
+ n = 8 * n - (*str++ - '0');
+ }
+ len--;
+ }
+
+ if (len == 0) {
+ return -(n);
+ }
+ else {
+ return std::nullopt;
+ }
+ };
+
+ auto replace_numeric_entity = [&](const char *entity) -> bool {
+ UChar32 uc;
+ std::optional<int> maybe_num;
+
+ if (*entity == 'x' || *entity == 'X') {
+ maybe_num = hex_to_int(entity + 1, h - (entity + 1));
+ }
+ else if (*entity == 'o' || *entity == 'O') {
+ maybe_num = oct_to_int(entity + 1, h - (entity + 1));
+ }
+ else {
+ maybe_num = dec_to_int(entity, h - entity);
+ }
+
+ if (!maybe_num) {
+ /* Skip undecoded */
+ if (end - t >= h - e) {
+ memmove(t, e, h - e);
+ t += h - e;
+ }
+
+ return false;
+ }
+ else {
+ uc = maybe_num.value();
+ /* Search for a replacement */
+ const auto *entity_def = html_entities_defs.by_id(uc);
+
+ if (entity_def) {
+ auto rep_len = strlen(entity_def->replacement);
+
+ if (end - t >= rep_len) {
+ memcpy(t, entity_def->replacement,
+ rep_len);
+ t += rep_len;
+ }
+
+ return true;
+ }
+ else {
+ /* Unicode point */
+ goffset off = t - s;
+ UBool is_error = 0;
+
+ if (uc > 0) {
+ U8_APPEND((std::uint8_t *) s, off, len, uc, is_error);
+
+ if (!is_error) {
+ t = s + off;
+ }
+ else if (end - t > 3) {
+ /* Not printable code point replace with 0xFFFD */
+ *t++ = '\357';
+ *t++ = '\277';
+ *t++ = '\275';
+
+ return true;
+ }
+ }
+ else if (end - t > 3) {
+ /* Not printable code point replace with 0xFFFD */
+ *t++ = '\357';
+ *t++ = '\277';
+ *t++ = '\275';
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ };
+
+ auto replace_entity = [&]() -> bool {
+ if (e + 1 < end) {
+ const auto *entity_start = e + 1;
+
+ if (*entity_start != '#') {
+ return replace_named_entity(entity_start, (h - entity_start));
+ }
+ else if (entity_start + 1 < h) {
+ return replace_numeric_entity(entity_start + 1);
+ }
+ }
+
+ return false;
+ };
+
+ if (norm_spaces && g_ascii_isspace(*h)) {
+ state = parser_state::skip_start_spaces;
+ }
+
+ while (h - s < len && t <= h) {
+ switch (state) {
+ case parser_state::normal_content:
+ if (*h == '&') {
+ state = parser_state::ampersand;
+ seen_hash = false;
+ seen_hex = false;
+ seen_digit_only = do_undefined;
+ e = h;
+ h++;
+ continue;
+ }
+ else {
+ if (norm_spaces && g_ascii_isspace(*h)) {
+ *t++ = ' ';
+ state = parser_state::skip_multi_spaces;
+ h++;
+ }
+ else {
+ *t++ = *h++;
+ }
+ }
+ break;
+ case parser_state::ampersand:
+ if ((*h == ';' || g_ascii_isspace(*h)) && h > e) {
+ replace_entity();
+ state = parser_state::normal_content;
+
+ if (g_ascii_isspace(*h)) {
+ /* Avoid increase of h */
+ continue;
+ }
+ }
+ else if (*h == '&') {
+ /* Previous `&` was bogus */
+ state = parser_state::ampersand;
+
+ if (end - t > h - e) {
+ memmove(t, e, h - e);
+ t += h - e;
+ }
+
+ e = h;
+ }
+ else if (*h == '#') {
+ seen_hash = true;
+
+ if (h + 1 < end && h[1] == 'x') {
+ seen_hex = true;
+ /* Skip one more character */
+ h++;
+ }
+ }
+ else if (seen_digit_only != do_mixed &&
+ (g_ascii_isdigit(*h) || (seen_hex && g_ascii_isxdigit(*h)))) {
+ seen_digit_only = do_digits_only;
+ }
+ else {
+ if (seen_digit_only == do_digits_only && seen_hash && h > e) {
+ /* We have seen some digits, so we can try to decode, eh */
+ /* Fuck retarded email clients... */
+ replace_entity();
+ state = parser_state::normal_content;
+ continue;
+ }
+
+ seen_digit_only = do_mixed;
+ }
+
+ h++;
+
+ break;
+ case parser_state::skip_multi_spaces:
+ if (g_ascii_isspace(*h)) {
+ h++;
+ }
+ else {
+ state = parser_state::normal_content;
+ }
+ break;
+ case parser_state::skip_start_spaces:
+ if (g_ascii_isspace(*h)) {
+ h++;
+ }
+ else {
+ state = parser_state::normal_content;
+ }
+ break;
+ }
+ }
+
+ /* Leftover */
+ if (state == parser_state::ampersand && h > e) {
+ /* Unfinished entity, copy as is */
+ if (replace_entity()) {
+ /* To follow FSM semantics */
+ h++;
+ }
+ else {
+ h = e; /* Include the last & */
+ }
+
+ /* Leftover after replacement */
+ if (h < end && t + (end - h) <= end) {
+ memmove(t, h, end - h);
+ t += end - h;
+ }
+ }
+
+ if (norm_spaces) {
+ bool seen_spaces = false;
+
+ while (t > s && g_ascii_isspace(*(t - 1))) {
+ seen_spaces = true;
+ t--;
+ }
+
+ if (seen_spaces) {
+ *t++ = ' ';
+ }
+ }
+
+ return (t - s);
+}
+
+auto decode_html_entitles_inplace(std::string &st) -> void
+{
+ auto nlen = decode_html_entitles_inplace(st.data(), st.size());
+ st.resize(nlen);
+}
+
+TEST_SUITE("html entities")
+{
+
+ TEST_CASE("html entities decode")
+ {
+ std::vector<std::pair<std::string, std::string>> cases{
+ {"", ""},
+ {"abc", "abc"},
+ {"abc def", "abc def"},
+ {"abc def", "abc def"},
+ {"abc\ndef", "abc def"},
+ {"abc\n \tdef", "abc def"},
+ {" abc def ", "abc def "},
+ {"FOO&gt;BAR", "FOO>BAR"},
+ {"FOO&gtBAR", "FOO>BAR"},
+ {"FOO&gt BAR", "FOO> BAR"},
+ {"FOO&gt;;;BAR", "FOO>;;BAR"},
+ {"I'm &notit;", "I'm ¬it;"},
+ {"I'm &notin;", "I'm ∉"},
+ {"FOO& BAR", "FOO& BAR"},
+ {"FOO&&&&gt;BAR", "FOO&&&>BAR"},
+ {"FOO&#41;BAR", "FOO)BAR"},
+ {"FOO&#x41;BAR", "FOOABAR"},
+ {"FOO&#X41;BAR", "FOOABAR"},
+ {"FOO&#BAR", "FOO&#BAR"},
+ {"FOO&#ZOO", "FOO&#ZOO"},
+ {"FOO&#xBAR", "FOOºR"},
+ {"FOO&#x41BAR", "FOO䆺R"},
+ {"FOO&#x0000;ZOO", "FOO\uFFFDZOO"},
+ {"FOO&#x0081;ZOO", "FOO\u0081ZOO"},
+ {"FOO&#xD800;ZOO", "FOO\uFFFDZOO"},
+ {"FOO&#xFFFFFF;ZOO", "FOO\uFFFDZOO"},
+ {"ZZ&pound_id=23", "ZZ£_id=23"},
+ {"ZZ&prod_id=23", "ZZ&prod_id=23"},
+ {"ZZ&gt", "ZZ>"},
+ {"ZZ&", "ZZ&"},
+ {"ZZ&AElig=", "ZZÆ="},
+ };
+
+ for (const auto &c: cases) {
+ SUBCASE(("decode entities: " + c.first).c_str())
+ {
+ auto *cpy = new char[c.first.size()];
+ memcpy(cpy, c.first.data(), c.first.size());
+ auto nlen = decode_html_entitles_inplace(cpy, c.first.size(), true);
+ CHECK(std::string{cpy, nlen} == c.second);
+ delete[] cpy;
+ }
+ }
+ }
+}
+
+}// namespace rspamd::html \ No newline at end of file
diff --git a/src/libserver/html/html_entities.hxx b/src/libserver/html/html_entities.hxx
new file mode 100644
index 0000000..fc1f7cc
--- /dev/null
+++ b/src/libserver/html/html_entities.hxx
@@ -0,0 +1,31 @@
+/*-
+ * Copyright 2021 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_HTML_ENTITIES_H
+#define RSPAMD_HTML_ENTITIES_H
+#pragma once
+
+#include <utility>
+#include <string>
+
+namespace rspamd::html {
+
+auto decode_html_entitles_inplace(char *s, std::size_t len, bool norm_spaces = false) -> std::size_t;
+auto decode_html_entitles_inplace(std::string &st) -> void;
+
+}// namespace rspamd::html
+
+#endif
diff --git a/src/libserver/html/html_tag.hxx b/src/libserver/html/html_tag.hxx
new file mode 100644
index 0000000..309d761
--- /dev/null
+++ b/src/libserver/html/html_tag.hxx
@@ -0,0 +1,159 @@
+/*-
+ * Copyright 2021 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_HTML_TAG_HXX
+#define RSPAMD_HTML_TAG_HXX
+#pragma once
+
+#include <utility>
+#include <string_view>
+#include <variant>
+#include <vector>
+#include <optional>
+#include <cstdint>
+
+#include "html_tags.h"
+
+struct rspamd_url;
+struct html_image;
+
+namespace rspamd::html {
+
+struct html_content; /* Forward declaration */
+
+enum class html_component_type : std::uint8_t {
+ RSPAMD_HTML_COMPONENT_NAME = 0,
+ RSPAMD_HTML_COMPONENT_HREF,
+ RSPAMD_HTML_COMPONENT_COLOR,
+ RSPAMD_HTML_COMPONENT_BGCOLOR,
+ RSPAMD_HTML_COMPONENT_STYLE,
+ RSPAMD_HTML_COMPONENT_CLASS,
+ RSPAMD_HTML_COMPONENT_WIDTH,
+ RSPAMD_HTML_COMPONENT_HEIGHT,
+ RSPAMD_HTML_COMPONENT_SIZE,
+ RSPAMD_HTML_COMPONENT_REL,
+ RSPAMD_HTML_COMPONENT_ALT,
+ RSPAMD_HTML_COMPONENT_ID,
+ RSPAMD_HTML_COMPONENT_HIDDEN,
+};
+
+/* Public tags flags */
+/* XML tag */
+#define FL_XML (1u << CM_USER_SHIFT)
+/* Fully closed tag (e.g. <a attrs />) */
+#define FL_CLOSED (1 << (CM_USER_SHIFT + 1))
+#define FL_BROKEN (1 << (CM_USER_SHIFT + 2))
+#define FL_IGNORE (1 << (CM_USER_SHIFT + 3))
+#define FL_BLOCK (1 << (CM_USER_SHIFT + 4))
+#define FL_HREF (1 << (CM_USER_SHIFT + 5))
+#define FL_COMMENT (1 << (CM_USER_SHIFT + 6))
+#define FL_VIRTUAL (1 << (CM_USER_SHIFT + 7))
+
+/**
+ * Returns component type from a string
+ * @param st
+ * @return
+ */
+auto html_component_from_string(const std::string_view &st) -> std::optional<html_component_type>;
+
+using html_tag_extra_t = std::variant<std::monostate, struct rspamd_url *, struct html_image *>;
+struct html_tag_component {
+ html_component_type type;
+ std::string_view value;
+
+ html_tag_component(html_component_type type, std::string_view value)
+ : type(type), value(value)
+ {
+ }
+};
+
+/* Pairing closing tag representation */
+struct html_closing_tag {
+ int start = -1;
+ int end = -1;
+
+ auto clear() -> void
+ {
+ start = end = -1;
+ }
+};
+
+struct html_tag {
+ unsigned int tag_start = 0;
+ unsigned int content_offset = 0;
+ std::uint32_t flags = 0;
+ std::int32_t id = Tag_UNKNOWN;
+ html_closing_tag closing;
+
+ std::vector<html_tag_component> components;
+
+ html_tag_extra_t extra;
+ mutable struct html_block *block = nullptr;
+ std::vector<struct html_tag *> children;
+ struct html_tag *parent;
+
+ auto find_component(html_component_type what) const -> std::optional<std::string_view>
+ {
+ for (const auto &comp: components) {
+ if (comp.type == what) {
+ return comp.value;
+ }
+ }
+
+ return std::nullopt;
+ }
+
+ auto find_component(std::optional<html_component_type> what) const -> std::optional<std::string_view>
+ {
+ if (what) {
+ return find_component(what.value());
+ }
+
+ return std::nullopt;
+ }
+
+ auto clear(void) -> void
+ {
+ id = Tag_UNKNOWN;
+ tag_start = content_offset = 0;
+ extra = std::monostate{};
+ components.clear();
+ flags = 0;
+ block = nullptr;
+ children.clear();
+ closing.clear();
+ }
+
+ constexpr auto get_content_length() const -> std::size_t
+ {
+ if (flags & (FL_IGNORE | CM_HEAD)) {
+ return 0;
+ }
+ if (closing.start > content_offset) {
+ return closing.start - content_offset;
+ }
+
+ return 0;
+ }
+
+ auto get_content(const struct html_content *hc) const -> std::string_view;
+};
+
+static_assert(CM_USER_SHIFT + 7 < sizeof(html_tag::flags) * NBBY);
+
+}// namespace rspamd::html
+
+#endif//RSPAMD_HTML_TAG_HXX
diff --git a/src/libserver/html/html_tag_defs.hxx b/src/libserver/html/html_tag_defs.hxx
new file mode 100644
index 0000000..647f7c3
--- /dev/null
+++ b/src/libserver/html/html_tag_defs.hxx
@@ -0,0 +1,194 @@
+/*-
+ * Copyright 2021 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_HTML_TAG_DEFS_HXX
+#define RSPAMD_HTML_TAG_DEFS_HXX
+
+#include "config.h"
+#include "html_tags.h"
+#include "libutil/cxx/util.hxx"
+
+#include <string>
+#include "contrib/ankerl/unordered_dense.h"
+
+namespace rspamd::html {
+
+struct html_tag_def {
+ std::string name;
+ tag_id_t id;
+ guint flags;
+};
+
+#define TAG_DEF(id, name, flags) \
+ html_tag_def \
+ { \
+ (name), (id), (flags) \
+ }
+
+static const auto html_tag_defs_array = rspamd::array_of(
+ /* W3C defined elements */
+ TAG_DEF(Tag_A, "a", FL_HREF),
+ TAG_DEF(Tag_ABBR, "abbr", (CM_INLINE)),
+ TAG_DEF(Tag_ACRONYM, "acronym", (CM_INLINE)),
+ TAG_DEF(Tag_ADDRESS, "address", (CM_BLOCK)),
+ TAG_DEF(Tag_APPLET, "applet", (CM_IMG | CM_INLINE | CM_PARAM)),
+ TAG_DEF(Tag_AREA, "area", (CM_BLOCK | CM_EMPTY | FL_HREF)),
+ TAG_DEF(Tag_B, "b", (CM_INLINE | FL_BLOCK)),
+ TAG_DEF(Tag_BASE, "base", (CM_HEAD | CM_EMPTY)),
+ TAG_DEF(Tag_BASEFONT, "basefont", (CM_INLINE | CM_EMPTY)),
+ TAG_DEF(Tag_BDO, "bdo", (CM_INLINE)),
+ TAG_DEF(Tag_BIG, "big", (CM_INLINE)),
+ TAG_DEF(Tag_BLOCKQUOTE, "blockquote", (CM_BLOCK)),
+ TAG_DEF(Tag_BODY, "body", (CM_HTML | CM_OPT | CM_OMITST | CM_UNIQUE | FL_BLOCK)),
+ TAG_DEF(Tag_BR, "br", (CM_INLINE | CM_EMPTY)),
+ TAG_DEF(Tag_BUTTON, "button", (CM_INLINE | FL_BLOCK)),
+ TAG_DEF(Tag_CAPTION, "caption", (CM_TABLE)),
+ TAG_DEF(Tag_CENTER, "center", (CM_BLOCK)),
+ TAG_DEF(Tag_CITE, "cite", (CM_INLINE)),
+ TAG_DEF(Tag_CODE, "code", (CM_INLINE)),
+ TAG_DEF(Tag_COL, "col", (CM_TABLE | CM_EMPTY)),
+ TAG_DEF(Tag_COLGROUP, "colgroup", (CM_TABLE | CM_OPT)),
+ TAG_DEF(Tag_DD, "dd", (CM_DEFLIST | CM_OPT | CM_NO_INDENT)),
+ TAG_DEF(Tag_DEL, "del", (CM_INLINE | CM_BLOCK)),
+ TAG_DEF(Tag_DFN, "dfn", (CM_INLINE)),
+ TAG_DEF(Tag_DIR, "dir", (CM_BLOCK)),
+ TAG_DEF(Tag_DIV, "div", (CM_BLOCK | FL_BLOCK)),
+ TAG_DEF(Tag_DL, "dl", (CM_BLOCK | FL_BLOCK)),
+ TAG_DEF(Tag_DT, "dt", (CM_DEFLIST | CM_OPT | CM_NO_INDENT)),
+ TAG_DEF(Tag_EM, "em", (CM_INLINE)),
+ TAG_DEF(Tag_FIELDSET, "fieldset", (CM_BLOCK)),
+ TAG_DEF(Tag_FONT, "font", (FL_BLOCK)),
+ TAG_DEF(Tag_FORM, "form", (CM_BLOCK | FL_HREF)),
+ TAG_DEF(Tag_FRAME, "frame", (CM_EMPTY | FL_HREF)),
+ TAG_DEF(Tag_FRAMESET, "frameset", (CM_HTML)),
+ TAG_DEF(Tag_H1, "h1", (CM_BLOCK)),
+ TAG_DEF(Tag_H2, "h2", (CM_BLOCK)),
+ TAG_DEF(Tag_H3, "h3", (CM_BLOCK)),
+ TAG_DEF(Tag_H4, "h4", (CM_BLOCK)),
+ TAG_DEF(Tag_H5, "h5", (CM_BLOCK)),
+ TAG_DEF(Tag_H6, "h6", (CM_BLOCK)),
+ TAG_DEF(Tag_HEAD, "head", (CM_HTML | CM_OPT | CM_OMITST | CM_UNIQUE)),
+ TAG_DEF(Tag_HR, "hr", (CM_BLOCK | CM_EMPTY)),
+ TAG_DEF(Tag_HTML, "html", (CM_HTML | CM_OPT | CM_OMITST | CM_UNIQUE)),
+ TAG_DEF(Tag_I, "i", (CM_INLINE)),
+ TAG_DEF(Tag_IFRAME, "iframe", (FL_HREF)),
+ TAG_DEF(Tag_IMG, "img", (CM_INLINE | CM_IMG | CM_EMPTY)),
+ TAG_DEF(Tag_INPUT, "input", (CM_INLINE | CM_IMG | CM_EMPTY)),
+ TAG_DEF(Tag_INS, "ins", (CM_INLINE | CM_BLOCK)),
+ TAG_DEF(Tag_ISINDEX, "isindex", (CM_BLOCK | CM_EMPTY)),
+ TAG_DEF(Tag_KBD, "kbd", (CM_INLINE)),
+ TAG_DEF(Tag_LABEL, "label", (CM_INLINE)),
+ TAG_DEF(Tag_LEGEND, "legend", (CM_INLINE)),
+ TAG_DEF(Tag_LI, "li", (CM_LIST | CM_OPT | CM_NO_INDENT | FL_BLOCK)),
+ TAG_DEF(Tag_LINK, "link", (CM_EMPTY | FL_HREF)),
+ TAG_DEF(Tag_LISTING, "listing", (CM_BLOCK)),
+ TAG_DEF(Tag_MAP, "map", (CM_INLINE | FL_HREF)),
+ TAG_DEF(Tag_MENU, "menu", (CM_BLOCK)),
+ TAG_DEF(Tag_META, "meta", (CM_HEAD | CM_INLINE | CM_EMPTY)),
+ TAG_DEF(Tag_NOFRAMES, "noframes", (CM_BLOCK)),
+ TAG_DEF(Tag_NOSCRIPT, "noscript", (CM_BLOCK | CM_INLINE | CM_RAW)),
+ TAG_DEF(Tag_OBJECT, "object", (CM_HEAD | CM_IMG | CM_INLINE | CM_PARAM)),
+ TAG_DEF(Tag_OL, "ol", (CM_BLOCK | FL_BLOCK)),
+ TAG_DEF(Tag_OPTGROUP, "optgroup", (CM_FIELD | CM_OPT)),
+ TAG_DEF(Tag_OPTION, "option", (CM_FIELD | CM_OPT)),
+ TAG_DEF(Tag_P, "p", (CM_BLOCK | CM_OPT | FL_BLOCK)),
+ TAG_DEF(Tag_PARAM, "param", (CM_INLINE | CM_EMPTY)),
+ TAG_DEF(Tag_PLAINTEXT, "plaintext", (CM_BLOCK)),
+ TAG_DEF(Tag_PRE, "pre", (CM_BLOCK)),
+ TAG_DEF(Tag_Q, "q", (CM_INLINE)),
+ TAG_DEF(Tag_RB, "rb", (CM_INLINE)),
+ TAG_DEF(Tag_RBC, "rbc", (CM_INLINE)),
+ TAG_DEF(Tag_RP, "rp", (CM_INLINE)),
+ TAG_DEF(Tag_RT, "rt", (CM_INLINE)),
+ TAG_DEF(Tag_RTC, "rtc", (CM_INLINE)),
+ TAG_DEF(Tag_RUBY, "ruby", (CM_INLINE)),
+ TAG_DEF(Tag_S, "s", (CM_INLINE)),
+ TAG_DEF(Tag_SAMP, "samp", (CM_INLINE)),
+ TAG_DEF(Tag_SCRIPT, "script", (CM_HEAD | CM_RAW)),
+ TAG_DEF(Tag_SELECT, "select", (CM_INLINE | CM_FIELD)),
+ TAG_DEF(Tag_SMALL, "small", (CM_INLINE)),
+ TAG_DEF(Tag_SPAN, "span", (CM_NO_INDENT | FL_BLOCK)),
+ TAG_DEF(Tag_STRIKE, "strike", (CM_INLINE)),
+ TAG_DEF(Tag_STRONG, "strong", (CM_INLINE)),
+ TAG_DEF(Tag_STYLE, "style", (CM_HEAD | CM_RAW)),
+ TAG_DEF(Tag_SUB, "sub", (CM_INLINE)),
+ TAG_DEF(Tag_SUP, "sup", (CM_INLINE)),
+ TAG_DEF(Tag_TABLE, "table", (CM_BLOCK | FL_BLOCK)),
+ TAG_DEF(Tag_TBODY, "tbody", (CM_TABLE | CM_ROWGRP | CM_OPT | FL_BLOCK)),
+ TAG_DEF(Tag_TD, "td", (CM_ROW | CM_OPT | CM_NO_INDENT | FL_BLOCK)),
+ TAG_DEF(Tag_TEXTAREA, "textarea", (CM_INLINE | CM_FIELD)),
+ TAG_DEF(Tag_TFOOT, "tfoot", (CM_TABLE | CM_ROWGRP | CM_OPT)),
+ TAG_DEF(Tag_TH, "th", (CM_ROW | CM_OPT | CM_NO_INDENT | FL_BLOCK)),
+ TAG_DEF(Tag_THEAD, "thead", (CM_TABLE | CM_ROWGRP | CM_OPT)),
+ TAG_DEF(Tag_TITLE, "title", (CM_HEAD | CM_UNIQUE)),
+ TAG_DEF(Tag_TR, "tr", (CM_TABLE | CM_OPT | FL_BLOCK)),
+ TAG_DEF(Tag_TT, "tt", (CM_INLINE)),
+ TAG_DEF(Tag_U, "u", (CM_INLINE)),
+ TAG_DEF(Tag_UL, "ul", (CM_BLOCK | FL_BLOCK)),
+ TAG_DEF(Tag_VAR, "var", (CM_INLINE)),
+ TAG_DEF(Tag_XMP, "xmp", (CM_BLOCK)),
+ TAG_DEF(Tag_NEXTID, "nextid", (CM_HEAD | CM_EMPTY)));
+
+class html_tags_storage {
+ ankerl::unordered_dense::map<std::string_view, html_tag_def> tag_by_name;
+ ankerl::unordered_dense::map<tag_id_t, html_tag_def> tag_by_id;
+
+public:
+ html_tags_storage()
+ {
+ tag_by_name.reserve(html_tag_defs_array.size());
+ tag_by_id.reserve(html_tag_defs_array.size());
+
+ for (const auto &t: html_tag_defs_array) {
+ tag_by_name[t.name] = t;
+ tag_by_id[t.id] = t;
+ }
+ }
+
+ auto by_name(std::string_view name) const -> const html_tag_def *
+ {
+ auto it = tag_by_name.find(name);
+
+ if (it != tag_by_name.end()) {
+ return &(it->second);
+ }
+
+ return nullptr;
+ }
+
+ auto by_id(int id) const -> const html_tag_def *
+ {
+ auto it = tag_by_id.find(static_cast<tag_id_t>(id));
+ if (it != tag_by_id.end()) {
+ return &(it->second);
+ }
+
+ return nullptr;
+ }
+
+ auto name_by_id_safe(int id) const -> std::string_view
+ {
+ auto it = tag_by_id.find(static_cast<tag_id_t>(id));
+ if (it != tag_by_id.end()) {
+ return it->second.name;
+ }
+
+ return "unknown";
+ }
+};
+
+}// namespace rspamd::html
+
+#endif//RSPAMD_HTML_TAG_DEFS_HXX
diff --git a/src/libserver/html/html_tags.h b/src/libserver/html/html_tags.h
new file mode 100644
index 0000000..c186314
--- /dev/null
+++ b/src/libserver/html/html_tags.h
@@ -0,0 +1,176 @@
+/*-
+ * 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 SRC_LIBSERVER_HTML_TAGS_H_
+#define SRC_LIBSERVER_HTML_TAGS_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Known HTML tags */
+typedef enum {
+ Tag_UNKNOWN = 0, /**< Unknown tag! */
+ Tag_A, /**< A */
+ Tag_ABBR, /**< ABBR */
+ Tag_ACRONYM, /**< ACRONYM */
+ Tag_ADDRESS, /**< ADDRESS */
+ Tag_APPLET, /**< APPLET */
+ Tag_AREA, /**< AREA */
+ Tag_B, /**< B */
+ Tag_BASE, /**< BASE */
+ Tag_BASEFONT, /**< BASEFONT */
+ Tag_BDO, /**< BDO */
+ Tag_BIG, /**< BIG */
+ Tag_BLOCKQUOTE, /**< BLOCKQUOTE */
+ Tag_BODY, /**< BODY */
+ Tag_BR, /**< BR */
+ Tag_BUTTON, /**< BUTTON */
+ Tag_CAPTION, /**< CAPTION */
+ Tag_CENTER, /**< CENTER */
+ Tag_CITE, /**< CITE */
+ Tag_CODE, /**< CODE */
+ Tag_COL, /**< COL */
+ Tag_COLGROUP, /**< COLGROUP */
+ Tag_DD, /**< DD */
+ Tag_DEL, /**< DEL */
+ Tag_DFN, /**< DFN */
+ Tag_DIR, /**< DIR */
+ Tag_DIV, /**< DIF */
+ Tag_DL, /**< DL */
+ Tag_DT, /**< DT */
+ Tag_EM, /**< EM */
+ Tag_FIELDSET, /**< FIELDSET */
+ Tag_FONT, /**< FONT */
+ Tag_FORM, /**< FORM */
+ Tag_FRAME, /**< FRAME */
+ Tag_FRAMESET, /**< FRAMESET */
+ Tag_H1, /**< H1 */
+ Tag_H2, /**< H2 */
+ Tag_H3, /**< H3 */
+ Tag_H4, /**< H4 */
+ Tag_H5, /**< H5 */
+ Tag_H6, /**< H6 */
+ Tag_HEAD, /**< HEAD */
+ Tag_HR, /**< HR */
+ Tag_HTML, /**< HTML */
+ Tag_I, /**< I */
+ Tag_IFRAME, /**< IFRAME */
+ Tag_IMG, /**< IMG */
+ Tag_INPUT, /**< INPUT */
+ Tag_INS, /**< INS */
+ Tag_ISINDEX, /**< ISINDEX */
+ Tag_KBD, /**< KBD */
+ Tag_KEYGEN, /**< KEYGEN */
+ Tag_LABEL, /**< LABEL */
+ Tag_LEGEND, /**< LEGEND */
+ Tag_LI, /**< LI */
+ Tag_LINK, /**< LINK */
+ Tag_LISTING, /**< LISTING */
+ Tag_MAP, /**< MAP */
+ Tag_MENU, /**< MENU */
+ Tag_META, /**< META */
+ Tag_NOFRAMES, /**< NOFRAMES */
+ Tag_NOSCRIPT, /**< NOSCRIPT */
+ Tag_OBJECT, /**< OBJECT */
+ Tag_OL, /**< OL */
+ Tag_OPTGROUP, /**< OPTGROUP */
+ Tag_OPTION, /**< OPTION */
+ Tag_P, /**< P */
+ Tag_PARAM, /**< PARAM */
+ Tag_PLAINTEXT, /**< PLAINTEXT */
+ Tag_PRE, /**< PRE */
+ Tag_Q, /**< Q */
+ Tag_RB, /**< RB */
+ Tag_RBC, /**< RBC */
+ Tag_RP, /**< RP */
+ Tag_RT, /**< RT */
+ Tag_RTC, /**< RTC */
+ Tag_RUBY, /**< RUBY */
+ Tag_S, /**< S */
+ Tag_SAMP, /**< SAMP */
+ Tag_SCRIPT, /**< SCRIPT */
+ Tag_SELECT, /**< SELECT */
+ Tag_SMALL, /**< SMALL */
+ Tag_SPAN, /**< SPAN */
+ Tag_STRIKE, /**< STRIKE */
+ Tag_STRONG, /**< STRONG */
+ Tag_STYLE, /**< STYLE */
+ Tag_SUB, /**< SUB */
+ Tag_SUP, /**< SUP */
+ Tag_TABLE, /**< TABLE */
+ Tag_TBODY, /**< TBODY */
+ Tag_TD, /**< TD */
+ Tag_TEXTAREA, /**< TEXTAREA */
+ Tag_TFOOT, /**< TFOOT */
+ Tag_TH, /**< TH */
+ Tag_THEAD, /**< THEAD */
+ Tag_TITLE, /**< TITLE */
+ Tag_TR, /**< TR */
+ Tag_TT, /**< TT */
+ Tag_U, /**< U */
+ Tag_UL, /**< UL */
+ Tag_VAR, /**< VAR */
+ Tag_XMP, /**< XMP */
+ Tag_NEXTID, /**< NEXTID */
+ Tag_MAX,
+
+ N_TAGS = -1 /**< Must be -1 */
+} tag_id_t;
+
+#define CM_UNKNOWN 0
+/* Elements with no content. Map to HTML specification. */
+#define CM_EMPTY (1 << 0)
+/* Elements that appear outside of "BODY". */
+#define CM_HTML (1 << 1)
+/* Elements that can appear within HEAD. */
+#define CM_HEAD (1 << 2)
+/* HTML "block" elements. */
+#define CM_BLOCK (1 << 3)
+/* HTML "inline" elements. */
+#define CM_INLINE (1 << 4)
+/* Elements that mark list item ("LI"). */
+#define CM_LIST (1 << 5)
+/* Elements that mark definition list item ("DL", "DT"). */
+#define CM_DEFLIST (1 << 6)
+/* Elements that can appear inside TABLE. */
+#define CM_TABLE (1 << 7)
+/* Used for "THEAD", "TFOOT" or "TBODY". */
+#define CM_ROWGRP (1 << 8)
+/* Used for "TD", "TH" */
+#define CM_ROW (1 << 9)
+/* Elements whose content must be protected against white space movement.
+ Includes some elements that can found in forms. */
+#define CM_FIELD (1 << 10)
+#define CM_RAW (1 << 11)
+/* Elements that allows "PARAM". */
+#define CM_PARAM (1 << 12)
+/* Elements with an optional end tag. */
+#define CM_OPT (1 << 13)
+/* Elements that use "align" attribute for vertical position. */
+#define CM_IMG (1 << 14)
+#define CM_NO_INDENT (1 << 15)
+/* Elements that cannot be omitted. */
+#define CM_OMITST (1 << 16)
+/* Unique elements */
+#define CM_UNIQUE (1 << 17)
+
+#define CM_USER_SHIFT (18)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SRC_LIBSERVER_HTML_TAGS_H_ */
diff --git a/src/libserver/html/html_tests.cxx b/src/libserver/html/html_tests.cxx
new file mode 100644
index 0000000..2fe6702
--- /dev/null
+++ b/src/libserver/html/html_tests.cxx
@@ -0,0 +1,304 @@
+/*-
+ * Copyright 2021 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 "html.hxx"
+#include "libserver/task.h"
+
+#include <vector>
+#include <fmt/core.h>
+
+
+#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
+#include "doctest/doctest.h"
+
+namespace rspamd::html {
+
+/*
+ * Tests part
+ */
+
+TEST_SUITE("html")
+{
+ TEST_CASE("html parsing")
+ {
+
+ const std::vector<std::pair<std::string, std::string>> cases{
+ {"<html><!DOCTYPE html><body>", "+html;++xml;++body;"},
+ {"<html><div><div></div></div></html>", "+html;++div;+++div;"},
+ {"<html><div><div></div></html>", "+html;++div;+++div;"},
+ {"<html><div><div></div></html></div>", "+html;++div;+++div;"},
+ {"<p><p><a></p></a></a>", "+p;++p;+++a;"},
+ {"<div><a href=\"http://example.com\"></div></a>", "+div;++a;"},
+ /* Broken, as I don't know how the hell this should be really parsed */
+ //{"<html><!DOCTYPE html><body><head><body></body></html></body></html>",
+ // "+html;++xml;++body;+++head;+++body;"}
+ };
+
+ rspamd_url_init(NULL);
+ auto *pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ "html", 0);
+ struct rspamd_task fake_task;
+ memset(&fake_task, 0, sizeof(fake_task));
+ fake_task.task_pool = pool;
+
+ for (const auto &c: cases) {
+ SUBCASE((std::string("extract tags from: ") + c.first).c_str())
+ {
+ GByteArray *tmp = g_byte_array_sized_new(c.first.size());
+ g_byte_array_append(tmp, (const guint8 *) c.first.data(), c.first.size());
+ auto *hc = html_process_input(&fake_task, tmp, nullptr, nullptr, nullptr, true, nullptr);
+ CHECK(hc != nullptr);
+ auto dump = html_debug_structure(*hc);
+ CHECK(c.second == dump);
+ g_byte_array_free(tmp, TRUE);
+ }
+ }
+
+ rspamd_mempool_delete(pool);
+ }
+
+ TEST_CASE("html text extraction")
+ {
+ using namespace std::string_literals;
+ const std::vector<std::pair<std::string, std::string>> cases{
+ {"test", "test"},
+ {"test\0"s, "test\uFFFD"s},
+ {"test\0test"s, "test\uFFFDtest"s},
+ {"test\0\0test"s, "test\uFFFD\uFFFDtest"s},
+ {"test ", "test"},
+ {"test foo, bar", "test foo, bar"},
+ {"<p>text</p>", "text\n"},
+ {"olo<p>text</p>lolo", "olo\ntext\nlolo"},
+ {"<div>foo</div><div>bar</div>", "foo\nbar\n"},
+ {"<b>foo<i>bar</b>baz</i>", "foobarbaz"},
+ {"<b>foo<i>bar</i>baz</b>", "foobarbaz"},
+ {"foo<br>baz", "foo\nbaz"},
+ {"<a href=https://example.com>test</a>", "test"},
+ {"<img alt=test>", "test"},
+ {" <body>\n"
+ " <!-- escape content -->\n"
+ " a&nbsp;b a &gt; b a &lt; b a &amp; b &apos;a &quot;a&quot;\n"
+ " </body>",
+ R"|(a b a > b a < b a & b 'a "a")|"},
+ /* XML tags */
+ {"<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\n"
+ " <!DOCTYPE html\n"
+ " PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n"
+ " \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n"
+ "<body>test</body>",
+ "test"},
+ {"<html><head><meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"></head>"
+ " <body>\n"
+ " <p><br>\n"
+ " </p>\n"
+ " <div class=\"moz-forward-container\"><br>\n"
+ " <br>\n"
+ " test</div>"
+ "</body>",
+ "\n\n\ntest\n"},
+ {"<div>fi<span style=\"FONT-SIZE: 0px\">le </span>"
+ "sh<span style=\"FONT-SIZE: 0px\">aring </span></div>",
+ "fish\n"},
+ /* FIXME: broken until rework of css parser */
+ //{"<div>fi<span style=\"FONT-SIZE: 0px\">le </span>"
+ // "sh<span style=\"FONT-SIZE: 0px\">aring </div>foo</span>", "fish\nfoo"},
+ /* Complex html with bad tags */
+ {"<!DOCTYPE html>\n"
+ "<html lang=\"en\">\n"
+ " <head>\n"
+ " <meta charset=\"utf-8\">\n"
+ " <title>title</title>\n"
+ " <link rel=\"stylesheet\" href=\"style.css\">\n"
+ " <script src=\"script.js\"></script>\n"
+ " </head>\n"
+ " <body>\n"
+ " <!-- page content -->\n"
+ " Hello, world! <b>test</b>\n"
+ " <p>data<>\n"
+ " </P>\n"
+ " <b>stuff</p>?\n"
+ " </body>\n"
+ "</html>",
+ "Hello, world! test \ndata<>\nstuff\n?"},
+ {"<p><!--comment-->test</br></hr><br>", "test\n"},
+ /* Tables */
+ {"<table>\n"
+ " <tr>\n"
+ " <th>heada</th>\n"
+ " <th>headb</th>\n"
+ " </tr>\n"
+ " <tr>\n"
+ " <td>data1</td>\n"
+ " <td>data2</td>\n"
+ " </tr>\n"
+ " </table>",
+ "heada headb\ndata1 data2\n"},
+ /* Invalid closing br and hr + comment */
+ {" <body>\n"
+ " <!-- page content -->\n"
+ " Hello, world!<br>test</br><br>content</hr>more content<br>\n"
+ " <div>\n"
+ " content inside div\n"
+ " </div>\n"
+ " </body>",
+ "Hello, world!\ntest\ncontentmore content\ncontent inside div\n"},
+ /* First closing tag */
+ {"</head>\n"
+ "<body>\n"
+ "<p> Hello. I have some bad news.\n"
+ "<br /> <br /> <br /> <strong> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> </strong><span> <br /> </span>test</p>\n"
+ "</body>\n"
+ "</html>",
+ "Hello. I have some bad news. \n\n\n\n\n\n\n\n\n\n\n\ntest\n"},
+ /* Invalid tags */
+ {"lol <sht> omg </sht> oh my!\n"
+ "<name>words words</name> goodbye",
+ "lol omg oh my! words words goodbye"},
+ /* Invisible stuff */
+ {"<div style=\"color:#555555;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;font-style: italic;\">\n"
+ "<p style=\"font-size: 11px; line-height: 1.2; color: #555555; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; mso-line-height-alt: 14px; margin: 0;\">\n"
+ "<span style=\"color:#FFFFFF; \">F</span>Sincerely,</p>\n"
+ "<p style=\"font-size: 11px; line-height: 1.2; color: #555555; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; mso-line-height-alt: 14px; margin: 0;\">\n"
+ "<span style=\"color:#FFFFFF; \">8</span>Sky<span style=\"opacity:1;\"></span>pe<span style=\"color:#FFFFFF; \">F</span>Web<span style=\"color:#FFFFFF; \">F</span></p>\n"
+ "<span style=\"color:#FFFFFF; \">kreyes</span>\n"
+ "<p style=\"font-size: 11px; line-height: 1.2; color: #555555; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; mso-line-height-alt: 14px; margin: 0;\">\n"
+ "&nbsp;</p>",
+ " Sincerely,\n Skype Web\n"},
+ {"lala<p hidden>fafa</p>", "lala"},
+ {"<table style=\"FONT-SIZE: 0px;\"><tbody><tr><td>\n"
+ "DONKEY\n"
+ "</td></tr></tbody></table>",
+ ""},
+ /* bgcolor propagation */
+ {"<a style=\"display: inline-block; color: #ffffff; background-color: #00aff0;\">\n"
+ "<span style=\"color: #00aff0;\">F</span>Rev<span style=\"opacity: 1;\"></span></span>ie<span style=\"opacity: 1;\"></span>"
+ "</span>w<span style=\"color: #00aff0;\">F<span style=\"opacity: 1;\">̹</span></span>",
+ " Review"},
+ {"<td style=\"color:#ffffff\" bgcolor=\"#005595\">\n"
+ "hello world\n"
+ "</td>",
+ "hello world"},
+ /* Colors */
+ {"goodbye <span style=\"COLOR: rgb(64,64,64)\">cruel</span>"
+ "<span>world</span>",
+ "goodbye cruelworld"},
+ /* Font-size propagation */
+ {"<p style=\"font-size: 11pt;line-height:22px\">goodbye <span style=\"font-size:0px\">cruel</span>world</p>",
+ "goodbye world\n"},
+ /* Newline before tag -> must be space */
+ {"goodbye <span style=\"COLOR: rgb(64,64,64)\">cruel</span>\n"
+ "<span>world</span>",
+ "goodbye cruel world"},
+ /* Head tag with some stuff */
+ {"<html><head><p>oh my god</head><body></body></html>", "oh my god\n"},
+ {"<html><head><title>oh my god</head><body></body></html>", ""},
+ {"<html><body><html><head>displayed</body></html></body></html>", "displayed"},
+
+ };
+
+ rspamd_url_init(NULL);
+ auto *pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ "html", 0);
+ struct rspamd_task fake_task;
+ memset(&fake_task, 0, sizeof(fake_task));
+ fake_task.task_pool = pool;
+
+ auto replace_newlines = [](std::string &str) {
+ auto start_pos = 0;
+ while ((start_pos = str.find("\n", start_pos, 1)) != std::string::npos) {
+ str.replace(start_pos, 1, "\\n", 2);
+ start_pos += 2;
+ }
+ };
+
+ auto i = 1;
+ for (const auto &c: cases) {
+ SUBCASE((fmt::format("html extraction case {}", i)).c_str())
+ {
+ GByteArray *tmp = g_byte_array_sized_new(c.first.size());
+ g_byte_array_append(tmp, (const guint8 *) c.first.data(), c.first.size());
+ auto *hc = html_process_input(&fake_task, tmp, nullptr, nullptr, nullptr, true, nullptr);
+ CHECK(hc != nullptr);
+ replace_newlines(hc->parsed);
+ auto expected = c.second;
+ replace_newlines(expected);
+ CHECK(hc->parsed == expected);
+ g_byte_array_free(tmp, TRUE);
+ }
+ i++;
+ }
+
+ rspamd_mempool_delete(pool);
+ }
+
+ TEST_CASE("html urls extraction")
+ {
+ using namespace std::string_literals;
+ const std::vector<std::tuple<std::string, std::vector<std::string>, std::optional<std::string>>> cases{
+ {"<style></style><a href=\"https://www.example.com\">yolo</a>",
+ {"https://www.example.com"},
+ "yolo"},
+ {"<a href=\"https://example.com\">test</a>", {"https://example.com"}, "test"},
+ {"<a <poo href=\"http://example.com\">hello</a>", {"http://example.com"}, "hello"},
+ {"<html>\n"
+ "<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=utf-8\">\n"
+ "<body>\n"
+ "<a href=\"https://www.example.com\">hello</a>\n"
+ "</body>\n"
+ "</html>",
+ {"https://www.example.com"},
+ "hello"},
+ };
+
+ rspamd_url_init(NULL);
+ auto *pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ "html", 0);
+ struct rspamd_task fake_task;
+ memset(&fake_task, 0, sizeof(fake_task));
+ fake_task.task_pool = pool;
+
+ auto i = 1;
+ for (const auto &c: cases) {
+ SUBCASE((fmt::format("html url extraction case {}", i)).c_str())
+ {
+ GPtrArray *purls = g_ptr_array_new();
+ auto input = std::get<0>(c);
+ GByteArray *tmp = g_byte_array_sized_new(input.size());
+ g_byte_array_append(tmp, (const guint8 *) input.data(), input.size());
+ auto *hc = html_process_input(&fake_task, tmp, nullptr, nullptr, purls, true, nullptr);
+ CHECK(hc != nullptr);
+ auto &expected_text = std::get<2>(c);
+ if (expected_text.has_value()) {
+ CHECK(hc->parsed == expected_text.value());
+ }
+ const auto &expected_urls = std::get<1>(c);
+ CHECK(expected_urls.size() == purls->len);
+ for (auto j = 0; j < expected_urls.size(); ++j) {
+ auto *url = (rspamd_url *) g_ptr_array_index(purls, j);
+ CHECK(expected_urls[j] == std::string{url->string, url->urllen});
+ }
+ g_byte_array_free(tmp, TRUE);
+ g_ptr_array_free(purls, TRUE);
+ }
+ ++i;
+ }
+
+ rspamd_mempool_delete(pool);
+ }
+}
+
+} /* namespace rspamd::html */
diff --git a/src/libserver/html/html_url.cxx b/src/libserver/html/html_url.cxx
new file mode 100644
index 0000000..8f29f2c
--- /dev/null
+++ b/src/libserver/html/html_url.cxx
@@ -0,0 +1,496 @@
+/*-
+ * Copyright 2021 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 "html_url.hxx"
+#include "libutil/str_util.h"
+#include "libserver/url.h"
+#include "libserver/logger.h"
+#include "rspamd.h"
+
+#include <unicode/idna.h>
+
+namespace rspamd::html {
+
+static auto
+rspamd_url_is_subdomain(std::string_view t1, std::string_view t2) -> bool
+{
+ const auto *p1 = t1.data() + t1.size() - 1;
+ const auto *p2 = t2.data() + t2.size() - 1;
+
+ /* Skip trailing dots */
+ while (p1 > t1.data()) {
+ if (*p1 != '.') {
+ break;
+ }
+
+ p1--;
+ }
+
+ while (p2 > t2.data()) {
+ if (*p2 != '.') {
+ break;
+ }
+
+ p2--;
+ }
+
+ while (p1 > t1.data() && p2 > t2.data()) {
+ if (*p1 != *p2) {
+ break;
+ }
+
+ p1--;
+ p2--;
+ }
+
+ if (p2 == t2.data()) {
+ /* p2 can be subdomain of p1 if *p1 is '.' */
+ if (p1 != t1.data() && *(p1 - 1) == '.') {
+ return true;
+ }
+ }
+ else if (p1 == t1.data()) {
+ if (p2 != t2.data() && *(p2 - 1) == '.') {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+static auto
+get_icu_idna_instance(void) -> auto
+{
+ auto uc_err = U_ZERO_ERROR;
+ static auto *udn = icu::IDNA::createUTS46Instance(UIDNA_DEFAULT, uc_err);
+
+ return udn;
+}
+
+static auto
+convert_idna_hostname_maybe(rspamd_mempool_t *pool, struct rspamd_url *url, bool use_tld)
+ -> std::string_view
+{
+ std::string_view ret = use_tld ? std::string_view{rspamd_url_tld_unsafe(url), url->tldlen} : std::string_view{rspamd_url_host_unsafe(url), url->hostlen};
+
+ /* Handle IDN url's */
+ if (ret.size() > 4 &&
+ rspamd_substring_search_caseless(ret.data(), ret.size(), "xn--", 4) != -1) {
+
+ const auto buf_capacity = ret.size() * 2 + 1;
+ auto *idn_hbuf = (char *) rspamd_mempool_alloc(pool, buf_capacity);
+ icu::CheckedArrayByteSink byte_sink{idn_hbuf, (int) buf_capacity};
+
+ /* We need to convert it to the normal value first */
+ icu::IDNAInfo info;
+ auto uc_err = U_ZERO_ERROR;
+ auto *udn = get_icu_idna_instance();
+ udn->nameToUnicodeUTF8(icu::StringPiece(ret.data(), ret.size()),
+ byte_sink, info, uc_err);
+
+ if (uc_err == U_ZERO_ERROR && !info.hasErrors()) {
+ /* idn_hbuf is allocated in mempool, so it is safe to use */
+ ret = std::string_view{idn_hbuf, (std::size_t) byte_sink.NumberOfBytesWritten()};
+ }
+ else {
+ msg_err_pool("cannot convert to IDN: %s (0x%xd)",
+ u_errorName(uc_err), info.getErrors());
+ }
+ }
+
+ return ret;
+};
+
+constexpr auto sv_equals(std::string_view s1, std::string_view s2) -> auto
+{
+ return (s1.size() == s2.size()) &&
+ std::equal(s1.begin(), s1.end(), s2.begin(), s2.end(),
+ [](const auto c1, const auto c2) {
+ return g_ascii_tolower(c1) == g_ascii_tolower(c2);
+ });
+}
+
+constexpr auto
+is_transfer_proto(struct rspamd_url *u) -> bool
+{
+ return (u->protocol & (PROTOCOL_HTTP | PROTOCOL_HTTPS | PROTOCOL_FTP)) != 0;
+}
+
+auto html_url_is_phished(rspamd_mempool_t *pool,
+ struct rspamd_url *href_url,
+ std::string_view text_data) -> std::optional<rspamd_url *>
+{
+ struct rspamd_url *text_url;
+ std::string_view disp_tok, href_tok;
+ goffset url_pos;
+ gchar *url_str = NULL;
+
+ auto sz = text_data.size();
+ const auto *trimmed = rspamd_string_unicode_trim_inplace(text_data.data(), &sz);
+ text_data = std::string_view(trimmed, sz);
+
+ if (text_data.size() > 4 &&
+ rspamd_url_find(pool, text_data.data(), text_data.size(), &url_str,
+ RSPAMD_URL_FIND_ALL,
+ &url_pos, NULL) &&
+ url_str != nullptr) {
+
+ if (url_pos > 0) {
+ /*
+ * We have some url at some offset, so we need to check what is
+ * at the start of the text
+ */
+ return std::nullopt;
+ }
+
+ text_url = rspamd_mempool_alloc0_type(pool, struct rspamd_url);
+ auto rc = rspamd_url_parse(text_url, url_str, strlen(url_str), pool,
+ RSPAMD_URL_PARSE_TEXT);
+
+ if (rc == URI_ERRNO_OK) {
+ text_url->flags |= RSPAMD_URL_FLAG_HTML_DISPLAYED;
+ href_url->flags |= RSPAMD_URL_FLAG_DISPLAY_URL;
+
+ /* Check for phishing */
+ if (is_transfer_proto(text_url) == is_transfer_proto(href_url)) {
+ disp_tok = convert_idna_hostname_maybe(pool, text_url, false);
+ href_tok = convert_idna_hostname_maybe(pool, href_url, false);
+
+ if (!sv_equals(disp_tok, href_tok) &&
+ text_url->tldlen > 0 && href_url->tldlen > 0) {
+
+ /* Apply the same logic for TLD */
+ disp_tok = convert_idna_hostname_maybe(pool, text_url, true);
+ href_tok = convert_idna_hostname_maybe(pool, href_url, true);
+
+ if (!sv_equals(disp_tok, href_tok)) {
+ /* Check if one url is a subdomain for another */
+
+ if (!rspamd_url_is_subdomain(disp_tok, href_tok)) {
+ href_url->flags |= RSPAMD_URL_FLAG_PHISHED;
+ text_url->flags |= RSPAMD_URL_FLAG_HTML_DISPLAYED;
+
+ if (href_url->ext == nullptr) {
+ href_url->ext = rspamd_mempool_alloc0_type(pool, rspamd_url_ext);
+ }
+ href_url->ext->linked_url = text_url;
+ }
+ }
+ }
+ }
+
+ return text_url;
+ }
+ else {
+ /*
+ * We have found something that looks like an url but it was
+ * not parsed correctly.
+ * Sometimes it means an obfuscation attempt, so we have to check
+ * what's inside of the text
+ */
+ gboolean obfuscation_found = FALSE;
+
+ if (text_data.size() > 4 && g_ascii_strncasecmp(text_data.begin(), "http", 4) == 0 &&
+ rspamd_substring_search(text_data.begin(), text_data.size(), "://", 3) != -1) {
+ /* Clearly an obfuscation attempt */
+ obfuscation_found = TRUE;
+ }
+
+ msg_info_pool("extract of url '%s' failed: %s; obfuscation detected: %s",
+ url_str,
+ rspamd_url_strerror(rc),
+ obfuscation_found ? "yes" : "no");
+
+ if (obfuscation_found) {
+ href_url->flags |= RSPAMD_URL_FLAG_PHISHED | RSPAMD_URL_FLAG_OBSCURED;
+ }
+ }
+ }
+
+ return std::nullopt;
+}
+
+void html_check_displayed_url(rspamd_mempool_t *pool,
+ GList **exceptions,
+ void *url_set,
+ std::string_view visible_part,
+ goffset href_offset,
+ struct rspamd_url *url)
+{
+ struct rspamd_url *displayed_url = nullptr;
+ struct rspamd_url *turl;
+ struct rspamd_process_exception *ex;
+ guint saved_flags = 0;
+ gsize dlen;
+
+ if (visible_part.empty()) {
+ /* No displayed url, just some text within <a> tag */
+ return;
+ }
+
+ if (url->ext == nullptr) {
+ url->ext = rspamd_mempool_alloc0_type(pool, rspamd_url_ext);
+ }
+ url->ext->visible_part = rspamd_mempool_alloc_buffer(pool, visible_part.size() + 1);
+ rspamd_strlcpy(url->ext->visible_part,
+ visible_part.data(),
+ visible_part.size() + 1);
+ dlen = visible_part.size();
+
+ /* Strip unicode spaces from the start and the end */
+ url->ext->visible_part = const_cast<char *>(
+ rspamd_string_unicode_trim_inplace(url->ext->visible_part,
+ &dlen));
+ auto maybe_url = html_url_is_phished(pool, url,
+ {url->ext->visible_part, dlen});
+
+ if (maybe_url) {
+ url->flags |= saved_flags;
+ displayed_url = maybe_url.value();
+ }
+
+ if (exceptions && displayed_url != nullptr) {
+ ex = rspamd_mempool_alloc_type(pool, struct rspamd_process_exception);
+ ex->pos = href_offset;
+ ex->len = dlen;
+ ex->type = RSPAMD_EXCEPTION_URL;
+ ex->ptr = url;
+
+ *exceptions = g_list_prepend(*exceptions, ex);
+ }
+
+ if (displayed_url && url_set) {
+ turl = rspamd_url_set_add_or_return((khash_t(rspamd_url_hash) *) url_set, displayed_url);
+
+ if (turl != nullptr) {
+ /* Here, we assume the following:
+ * if we have a URL in the text part which
+ * is the same as displayed URL in the
+ * HTML part, we assume that it is also
+ * hint only.
+ */
+ if (turl->flags & RSPAMD_URL_FLAG_FROM_TEXT) {
+
+ /*
+ * We have the same URL for href and displayed url, so we
+ * know that this url cannot be both target and display (as
+ * it breaks logic in many places), so we do not
+ * propagate html flags
+ */
+ if (!(turl->flags & RSPAMD_URL_FLAG_DISPLAY_URL)) {
+ turl->flags |= displayed_url->flags;
+ }
+ turl->flags &= ~RSPAMD_URL_FLAG_FROM_TEXT;
+ }
+
+ turl->count++;
+ }
+ else {
+ /* Already inserted by `rspamd_url_set_add_or_return` */
+ }
+ }
+
+ rspamd_normalise_unicode_inplace(url->ext->visible_part, &dlen);
+}
+
+auto html_process_url(rspamd_mempool_t *pool, std::string_view &input)
+ -> std::optional<struct rspamd_url *>
+{
+ struct rspamd_url *url;
+ guint saved_flags = 0;
+ gint rc;
+ const gchar *s, *prefix = "http://";
+ gchar *d;
+ gsize dlen;
+ gboolean has_bad_chars = FALSE, no_prefix = FALSE;
+ static const gchar hexdigests[] = "0123456789abcdef";
+
+ auto sz = input.length();
+ const auto *trimmed = rspamd_string_unicode_trim_inplace(input.data(), &sz);
+ input = {trimmed, sz};
+
+ const auto *start = input.data();
+ s = start;
+ dlen = 0;
+
+ for (auto i = 0; i < sz; i++) {
+ if (G_UNLIKELY(((guint) s[i]) < 0x80 && !g_ascii_isgraph(s[i]))) {
+ dlen += 3;
+ }
+ else {
+ dlen++;
+ }
+ }
+
+ if (rspamd_substring_search(start, sz, "://", 3) == -1) {
+ if (sz >= sizeof("mailto:") &&
+ (memcmp(start, "mailto:", sizeof("mailto:") - 1) == 0 ||
+ memcmp(start, "tel:", sizeof("tel:") - 1) == 0 ||
+ memcmp(start, "callto:", sizeof("callto:") - 1) == 0)) {
+ /* Exclusion, has valid but 'strange' prefix */
+ }
+ else {
+ for (auto i = 0; i < sz; i++) {
+ if (!((s[i] & 0x80) || g_ascii_isalnum(s[i]))) {
+ if (i == 0 && sz > 2 && s[i] == '/' && s[i + 1] == '/') {
+ prefix = "http:";
+ dlen += sizeof("http:") - 1;
+ no_prefix = TRUE;
+ }
+ else if (s[i] == '@') {
+ /* Likely email prefix */
+ prefix = "mailto://";
+ dlen += sizeof("mailto://") - 1;
+ no_prefix = TRUE;
+ }
+ else if (s[i] == ':' && i != 0) {
+ /* Special case */
+ no_prefix = FALSE;
+ }
+ else {
+ if (i == 0) {
+ /* No valid data */
+ return std::nullopt;
+ }
+ else {
+ no_prefix = TRUE;
+ dlen += strlen(prefix);
+ }
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ auto *decoded = rspamd_mempool_alloc_buffer(pool, dlen + 1);
+ d = decoded;
+
+ if (no_prefix) {
+ gsize plen = strlen(prefix);
+ memcpy(d, prefix, plen);
+ d += plen;
+ }
+
+ /*
+ * We also need to remove all internal newlines, spaces
+ * and encode unsafe characters
+ * Another obfuscation find in the wild was encoding of the SAFE url characters,
+ * including essential ones
+ */
+ for (auto i = 0; i < sz; i++) {
+ if (G_UNLIKELY(g_ascii_isspace(s[i]))) {
+ continue;
+ }
+ else if (G_UNLIKELY(((guint) s[i]) < 0x80 && !g_ascii_isgraph(s[i]))) {
+ /* URL encode */
+ *d++ = '%';
+ *d++ = hexdigests[(s[i] >> 4) & 0xf];
+ *d++ = hexdigests[s[i] & 0xf];
+ has_bad_chars = TRUE;
+ }
+ else if (G_UNLIKELY(s[i] == '%')) {
+ if (i + 2 < sz) {
+ auto c1 = s[i + 1];
+ auto c2 = s[i + 2];
+
+ if (g_ascii_isxdigit(c1) && g_ascii_isxdigit(c2)) {
+ auto codepoint = 0;
+
+ if (c1 >= '0' && c1 <= '9') codepoint = c1 - '0';
+ else if (c1 >= 'A' && c1 <= 'F')
+ codepoint = c1 - 'A' + 10;
+ else if (c1 >= 'a' && c1 <= 'f')
+ codepoint = c1 - 'a' + 10;
+
+ codepoint <<= 4;
+
+ if (c2 >= '0' && c2 <= '9') codepoint += c2 - '0';
+ else if (c2 >= 'A' && c2 <= 'F')
+ codepoint += c2 - 'A' + 10;
+ else if (c2 >= 'a' && c2 <= 'f')
+ codepoint += c2 - 'a' + 10;
+
+ /* Now check for 'interesting' codepoints */
+ if (codepoint == '@' || codepoint == ':' || codepoint == '|' ||
+ codepoint == '?' || codepoint == '\\' || codepoint == '/') {
+ /* Replace it back */
+ *d++ = (char) (codepoint & 0xff);
+ i += 2;
+ }
+ else {
+ *d++ = s[i];
+ }
+ }
+ else {
+ *d++ = s[i];
+ }
+ }
+ else {
+ *d++ = s[i];
+ }
+ }
+ else {
+ *d++ = s[i];
+ }
+ }
+
+ *d = '\0';
+ dlen = d - decoded;
+
+ url = rspamd_mempool_alloc0_type(pool, struct rspamd_url);
+ rspamd_url_normalise_propagate_flags(pool, decoded, &dlen, saved_flags);
+ rc = rspamd_url_parse(url, decoded, dlen, pool, RSPAMD_URL_PARSE_HREF);
+
+ /* Filter some completely damaged urls */
+ if (rc == URI_ERRNO_OK && url->hostlen > 0 &&
+ !((url->protocol & PROTOCOL_UNKNOWN))) {
+ url->flags |= saved_flags;
+
+ if (has_bad_chars) {
+ url->flags |= RSPAMD_URL_FLAG_OBSCURED;
+ }
+
+ if (no_prefix) {
+ url->flags |= RSPAMD_URL_FLAG_SCHEMALESS;
+
+ if (url->tldlen == 0 || (url->flags & RSPAMD_URL_FLAG_NO_TLD)) {
+ /* Ignore urls with both no schema and no tld */
+ return std::nullopt;
+ }
+ }
+
+ decoded = url->string;
+
+ input = {decoded, url->urllen};
+
+ /* Spaces in href usually mean an attempt to obfuscate URL */
+ /* See https://github.com/vstakhov/rspamd/issues/593 */
+#if 0
+ if (has_spaces) {
+ url->flags |= RSPAMD_URL_FLAG_OBSCURED;
+ }
+#endif
+
+ return url;
+ }
+
+ return std::nullopt;
+}
+
+}// namespace rspamd::html \ No newline at end of file
diff --git a/src/libserver/html/html_url.hxx b/src/libserver/html/html_url.hxx
new file mode 100644
index 0000000..46dde6d
--- /dev/null
+++ b/src/libserver/html/html_url.hxx
@@ -0,0 +1,68 @@
+/*-
+ * Copyright 2021 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_HTML_URL_HXX
+#define RSPAMD_HTML_URL_HXX
+#pragma once
+
+#include "libutil/mem_pool.h"
+
+#include <string_view>
+#include <optional>
+
+struct rspamd_url; /* Forward declaration */
+
+namespace rspamd::html {
+
+
+/**
+ * Checks if an html url is likely phished by some displayed url
+ * @param pool
+ * @param href_url
+ * @param text_data
+ * @return
+ */
+auto html_url_is_phished(rspamd_mempool_t *pool,
+ struct rspamd_url *href_url,
+ std::string_view text_data) -> std::optional<rspamd_url *>;
+
+/**
+ * Check displayed part of the url at specified offset
+ * @param pool
+ * @param exceptions
+ * @param url_set
+ * @param visible_part
+ * @param href_offset
+ * @param url
+ */
+auto html_check_displayed_url(rspamd_mempool_t *pool,
+ GList **exceptions,
+ void *url_set,
+ std::string_view visible_part,
+ goffset href_offset,
+ struct rspamd_url *url) -> void;
+
+/**
+ * Process HTML url (e.g. for href component)
+ * @param pool
+ * @param input may be modified during the process
+ * @return
+ */
+auto html_process_url(rspamd_mempool_t *pool, std::string_view &input)
+ -> std::optional<struct rspamd_url *>;
+}// namespace rspamd::html
+
+#endif//RSPAMD_HTML_URL_HXX \ No newline at end of file
diff --git a/src/libserver/http/http_connection.c b/src/libserver/http/http_connection.c
new file mode 100644
index 0000000..5557fbf
--- /dev/null
+++ b/src/libserver/http/http_connection.c
@@ -0,0 +1,2649 @@
+/*-
+ * 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 "http_connection.h"
+#include "http_private.h"
+#include "http_message.h"
+#include "utlist.h"
+#include "util.h"
+#include "printf.h"
+#include "logger.h"
+#include "ref.h"
+#include "ottery.h"
+#include "keypair_private.h"
+#include "cryptobox.h"
+#include "libutil/libev_helper.h"
+#include "libserver/ssl_util.h"
+#include "libserver/url.h"
+
+#include "contrib/mumhash/mum.h"
+#include "contrib/http-parser/http_parser.h"
+#include "unix-std.h"
+
+#include <openssl/err.h>
+
+#define ENCRYPTED_VERSION " HTTP/1.0"
+
+struct _rspamd_http_privbuf {
+ rspamd_fstring_t *data;
+ const gchar *zc_buf;
+ gsize zc_remain;
+ ref_entry_t ref;
+};
+
+enum rspamd_http_priv_flags {
+ RSPAMD_HTTP_CONN_FLAG_ENCRYPTED = 1u << 0u,
+ RSPAMD_HTTP_CONN_FLAG_NEW_HEADER = 1u << 1u,
+ RSPAMD_HTTP_CONN_FLAG_RESETED = 1u << 2u,
+ RSPAMD_HTTP_CONN_FLAG_TOO_LARGE = 1u << 3u,
+ RSPAMD_HTTP_CONN_FLAG_ENCRYPTION_NEEDED = 1u << 4u,
+ RSPAMD_HTTP_CONN_FLAG_PROXY = 1u << 5u,
+ RSPAMD_HTTP_CONN_FLAG_PROXY_REQUEST = 1u << 6u,
+ RSPAMD_HTTP_CONN_OWN_SOCKET = 1u << 7u,
+};
+
+#define IS_CONN_ENCRYPTED(c) ((c)->flags & RSPAMD_HTTP_CONN_FLAG_ENCRYPTED)
+#define IS_CONN_RESETED(c) ((c)->flags & RSPAMD_HTTP_CONN_FLAG_RESETED)
+
+struct rspamd_http_connection_private {
+ struct rspamd_http_context *ctx;
+ struct rspamd_ssl_connection *ssl;
+ struct _rspamd_http_privbuf *buf;
+ struct rspamd_keypair_cache *cache;
+ struct rspamd_cryptobox_pubkey *peer_key;
+ struct rspamd_cryptobox_keypair *local_key;
+ struct rspamd_http_header *header;
+ struct http_parser parser;
+ struct http_parser_settings parser_cb;
+ struct rspamd_io_ev ev;
+ ev_tstamp timeout;
+ struct rspamd_http_message *msg;
+ struct iovec *out;
+ guint outlen;
+ enum rspamd_http_priv_flags flags;
+ gsize wr_pos;
+ gsize wr_total;
+};
+
+static const rspamd_ftok_t key_header = {
+ .begin = "Key",
+ .len = 3};
+static const rspamd_ftok_t date_header = {
+ .begin = "Date",
+ .len = 4};
+static const rspamd_ftok_t last_modified_header = {
+ .begin = "Last-Modified",
+ .len = 13};
+
+static void rspamd_http_event_handler(int fd, short what, gpointer ud);
+static void rspamd_http_ssl_err_handler(gpointer ud, GError *err);
+
+
+#define HTTP_ERROR http_error_quark()
+GQuark
+http_error_quark(void)
+{
+ return g_quark_from_static_string("http-error-quark");
+}
+
+static void
+rspamd_http_privbuf_dtor(gpointer ud)
+{
+ struct _rspamd_http_privbuf *p = (struct _rspamd_http_privbuf *) ud;
+
+ if (p->data) {
+ rspamd_fstring_free(p->data);
+ }
+
+ g_free(p);
+}
+
+static const gchar *
+rspamd_http_code_to_str(gint code)
+{
+ if (code == 200) {
+ return "OK";
+ }
+ else if (code == 404) {
+ return "Not found";
+ }
+ else if (code == 403 || code == 401) {
+ return "Not authorized";
+ }
+ else if (code >= 400 && code < 500) {
+ return "Bad request";
+ }
+ else if (code >= 300 && code < 400) {
+ return "See Other";
+ }
+ else if (code >= 500 && code < 600) {
+ return "Internal server error";
+ }
+
+ return "Unknown error";
+}
+
+static void
+rspamd_http_parse_key(rspamd_ftok_t *data, struct rspamd_http_connection *conn,
+ struct rspamd_http_connection_private *priv)
+{
+ guchar *decoded_id;
+ const gchar *eq_pos;
+ gsize id_len;
+ struct rspamd_cryptobox_pubkey *pk;
+
+ if (priv->local_key == NULL) {
+ /* In this case we cannot do anything, e.g. we cannot decrypt payload */
+ priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_ENCRYPTED;
+ }
+ else {
+ /* Check sanity of what we have */
+ eq_pos = memchr(data->begin, '=', data->len);
+ if (eq_pos != NULL) {
+ decoded_id = rspamd_decode_base32(data->begin, eq_pos - data->begin,
+ &id_len, RSPAMD_BASE32_DEFAULT);
+
+ if (decoded_id != NULL && id_len >= RSPAMD_KEYPAIR_SHORT_ID_LEN) {
+ pk = rspamd_pubkey_from_base32(eq_pos + 1,
+ data->begin + data->len - eq_pos - 1,
+ RSPAMD_KEYPAIR_KEX,
+ RSPAMD_CRYPTOBOX_MODE_25519);
+ if (pk != NULL) {
+ if (memcmp(rspamd_keypair_get_id(priv->local_key),
+ decoded_id,
+ RSPAMD_KEYPAIR_SHORT_ID_LEN) == 0) {
+ priv->msg->peer_key = pk;
+
+ if (priv->cache && priv->msg->peer_key) {
+ rspamd_keypair_cache_process(priv->cache,
+ priv->local_key,
+ priv->msg->peer_key);
+ }
+ }
+ else {
+ rspamd_pubkey_unref(pk);
+ }
+ }
+ }
+
+ priv->flags |= RSPAMD_HTTP_CONN_FLAG_ENCRYPTED;
+ g_free(decoded_id);
+ }
+ }
+}
+
+static inline void
+rspamd_http_check_special_header(struct rspamd_http_connection *conn,
+ struct rspamd_http_connection_private *priv)
+{
+ if (rspamd_ftok_casecmp(&priv->header->name, &date_header) == 0) {
+ priv->msg->date = rspamd_http_parse_date(priv->header->value.begin,
+ priv->header->value.len);
+ }
+ else if (rspamd_ftok_casecmp(&priv->header->name, &key_header) == 0) {
+ rspamd_http_parse_key(&priv->header->value, conn, priv);
+ }
+ else if (rspamd_ftok_casecmp(&priv->header->name, &last_modified_header) == 0) {
+ priv->msg->last_modified = rspamd_http_parse_date(
+ priv->header->value.begin,
+ priv->header->value.len);
+ }
+}
+
+static gint
+rspamd_http_on_url(http_parser *parser, const gchar *at, size_t length)
+{
+ struct rspamd_http_connection *conn =
+ (struct rspamd_http_connection *) parser->data;
+ struct rspamd_http_connection_private *priv;
+
+ priv = conn->priv;
+
+ priv->msg->url = rspamd_fstring_append(priv->msg->url, at, length);
+
+ return 0;
+}
+
+static gint
+rspamd_http_on_status(http_parser *parser, const gchar *at, size_t length)
+{
+ struct rspamd_http_connection *conn =
+ (struct rspamd_http_connection *) parser->data;
+ struct rspamd_http_connection_private *priv;
+
+ priv = conn->priv;
+
+ if (parser->status_code != 200) {
+ if (priv->msg->status == NULL) {
+ priv->msg->status = rspamd_fstring_new();
+ }
+
+ priv->msg->status = rspamd_fstring_append(priv->msg->status, at, length);
+ }
+
+ return 0;
+}
+
+static void
+rspamd_http_finish_header(struct rspamd_http_connection *conn,
+ struct rspamd_http_connection_private *priv)
+{
+ struct rspamd_http_header *hdr;
+ khiter_t k;
+ gint r;
+
+ priv->header->combined = rspamd_fstring_append(priv->header->combined,
+ "\r\n", 2);
+ priv->header->value.len = priv->header->combined->len -
+ priv->header->name.len - 4;
+ priv->header->value.begin = priv->header->combined->str +
+ priv->header->name.len + 2;
+ priv->header->name.begin = priv->header->combined->str;
+
+ k = kh_put(rspamd_http_headers_hash, priv->msg->headers, &priv->header->name,
+ &r);
+
+ if (r != 0) {
+ kh_value(priv->msg->headers, k) = priv->header;
+ hdr = NULL;
+ }
+ else {
+ hdr = kh_value(priv->msg->headers, k);
+ }
+
+ DL_APPEND(hdr, priv->header);
+
+ rspamd_http_check_special_header(conn, priv);
+}
+
+static void
+rspamd_http_init_header(struct rspamd_http_connection_private *priv)
+{
+ priv->header = g_malloc0(sizeof(struct rspamd_http_header));
+ priv->header->combined = rspamd_fstring_new();
+}
+
+static gint
+rspamd_http_on_header_field(http_parser *parser,
+ const gchar *at,
+ size_t length)
+{
+ struct rspamd_http_connection *conn =
+ (struct rspamd_http_connection *) parser->data;
+ struct rspamd_http_connection_private *priv;
+
+ priv = conn->priv;
+
+ if (priv->header == NULL) {
+ rspamd_http_init_header(priv);
+ }
+ else if (priv->flags & RSPAMD_HTTP_CONN_FLAG_NEW_HEADER) {
+ rspamd_http_finish_header(conn, priv);
+ rspamd_http_init_header(priv);
+ }
+
+ priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_NEW_HEADER;
+ priv->header->combined = rspamd_fstring_append(priv->header->combined,
+ at, length);
+
+ return 0;
+}
+
+static gint
+rspamd_http_on_header_value(http_parser *parser,
+ const gchar *at,
+ size_t length)
+{
+ struct rspamd_http_connection *conn =
+ (struct rspamd_http_connection *) parser->data;
+ struct rspamd_http_connection_private *priv;
+
+ priv = conn->priv;
+
+ if (priv->header == NULL) {
+ /* Should not happen */
+ return -1;
+ }
+
+ if (!(priv->flags & RSPAMD_HTTP_CONN_FLAG_NEW_HEADER)) {
+ priv->flags |= RSPAMD_HTTP_CONN_FLAG_NEW_HEADER;
+ priv->header->combined = rspamd_fstring_append(priv->header->combined,
+ ": ", 2);
+ priv->header->name.len = priv->header->combined->len - 2;
+ }
+
+ priv->header->combined = rspamd_fstring_append(priv->header->combined,
+ at, length);
+
+ return 0;
+}
+
+static int
+rspamd_http_on_headers_complete(http_parser *parser)
+{
+ struct rspamd_http_connection *conn =
+ (struct rspamd_http_connection *) parser->data;
+ struct rspamd_http_connection_private *priv;
+ struct rspamd_http_message *msg;
+ int ret;
+
+ priv = conn->priv;
+ msg = priv->msg;
+
+ if (priv->header != NULL) {
+ rspamd_http_finish_header(conn, priv);
+
+ priv->header = NULL;
+ priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_NEW_HEADER;
+ }
+
+ if (msg->method == HTTP_HEAD) {
+ /* We don't care about the rest */
+ rspamd_ev_watcher_stop(priv->ctx->event_loop, &priv->ev);
+
+ msg->code = parser->status_code;
+ rspamd_http_connection_ref(conn);
+ ret = conn->finish_handler(conn, msg);
+
+ if (conn->opts & RSPAMD_HTTP_CLIENT_KEEP_ALIVE) {
+ rspamd_http_context_push_keepalive(conn->priv->ctx, conn,
+ msg, conn->priv->ctx->event_loop);
+ rspamd_http_connection_reset(conn);
+ }
+ else {
+ conn->finished = TRUE;
+ }
+
+ rspamd_http_connection_unref(conn);
+
+ return ret;
+ }
+
+ /*
+ * HTTP parser sets content length to (-1) when it doesn't know the real
+ * length, for example, in case of chunked encoding.
+ *
+ * Hence, we skip body setup here
+ */
+ if (parser->content_length != ULLONG_MAX && parser->content_length != 0 &&
+ msg->method != HTTP_HEAD) {
+ if (conn->max_size > 0 &&
+ parser->content_length > conn->max_size) {
+ /* Too large message */
+ priv->flags |= RSPAMD_HTTP_CONN_FLAG_TOO_LARGE;
+ return -1;
+ }
+
+ if (!rspamd_http_message_set_body(msg, NULL, parser->content_length)) {
+ return -1;
+ }
+ }
+
+ if (parser->flags & F_SPAMC) {
+ msg->flags |= RSPAMD_HTTP_FLAG_SPAMC;
+ }
+
+
+ msg->method = parser->method;
+ msg->code = parser->status_code;
+
+ return 0;
+}
+
+static void
+rspamd_http_switch_zc(struct _rspamd_http_privbuf *pbuf,
+ struct rspamd_http_message *msg)
+{
+ pbuf->zc_buf = msg->body_buf.begin + msg->body_buf.len;
+ pbuf->zc_remain = msg->body_buf.allocated_len - msg->body_buf.len;
+}
+
+static int
+rspamd_http_on_body(http_parser *parser, const gchar *at, size_t length)
+{
+ struct rspamd_http_connection *conn =
+ (struct rspamd_http_connection *) parser->data;
+ struct rspamd_http_connection_private *priv;
+ struct rspamd_http_message *msg;
+ struct _rspamd_http_privbuf *pbuf;
+ const gchar *p;
+
+ priv = conn->priv;
+ msg = priv->msg;
+ pbuf = priv->buf;
+ p = at;
+
+ if (!(msg->flags & RSPAMD_HTTP_FLAG_HAS_BODY)) {
+ if (!rspamd_http_message_set_body(msg, NULL, parser->content_length)) {
+ return -1;
+ }
+ }
+
+ if (conn->finished) {
+ return 0;
+ }
+
+ if (conn->max_size > 0 &&
+ msg->body_buf.len + length > conn->max_size) {
+ /* Body length overflow */
+ priv->flags |= RSPAMD_HTTP_CONN_FLAG_TOO_LARGE;
+ return -1;
+ }
+
+ if (!pbuf->zc_buf) {
+ if (!rspamd_http_message_append_body(msg, at, length)) {
+ return -1;
+ }
+
+ /* We might have some leftover in our private buffer */
+ if (pbuf->data->len == length) {
+ /* Switch to zero-copy mode */
+ rspamd_http_switch_zc(pbuf, msg);
+ }
+ }
+ else {
+ if (msg->body_buf.begin + msg->body_buf.len != at) {
+ /* Likely chunked encoding */
+ memmove((gchar *) msg->body_buf.begin + msg->body_buf.len, at, length);
+ p = msg->body_buf.begin + msg->body_buf.len;
+ }
+
+ /* Adjust zero-copy buf */
+ msg->body_buf.len += length;
+
+ if (!(msg->flags & RSPAMD_HTTP_FLAG_SHMEM)) {
+ msg->body_buf.c.normal->len += length;
+ }
+
+ pbuf->zc_buf = msg->body_buf.begin + msg->body_buf.len;
+ pbuf->zc_remain = msg->body_buf.allocated_len - msg->body_buf.len;
+ }
+
+ if ((conn->opts & RSPAMD_HTTP_BODY_PARTIAL) && !IS_CONN_ENCRYPTED(priv)) {
+ /* Incremental update is impossible for encrypted requests so far */
+ return (conn->body_handler(conn, msg, p, length));
+ }
+
+ return 0;
+}
+
+static int
+rspamd_http_on_body_decrypted(http_parser *parser, const gchar *at, size_t length)
+{
+ struct rspamd_http_connection *conn =
+ (struct rspamd_http_connection *) parser->data;
+ struct rspamd_http_connection_private *priv;
+
+ priv = conn->priv;
+
+ if (priv->header != NULL) {
+ rspamd_http_finish_header(conn, priv);
+ priv->header = NULL;
+ }
+
+ if (conn->finished) {
+ return 0;
+ }
+
+ if (priv->msg->body_buf.len == 0) {
+
+ priv->msg->body_buf.begin = at;
+ priv->msg->method = parser->method;
+ priv->msg->code = parser->status_code;
+ }
+
+ priv->msg->body_buf.len += length;
+
+ return 0;
+}
+
+static int
+rspamd_http_on_headers_complete_decrypted(http_parser *parser)
+{
+ struct rspamd_http_connection *conn =
+ (struct rspamd_http_connection *) parser->data;
+ struct rspamd_http_connection_private *priv;
+ struct rspamd_http_message *msg;
+ int ret;
+
+ priv = conn->priv;
+ msg = priv->msg;
+
+ if (priv->header != NULL) {
+ rspamd_http_finish_header(conn, priv);
+
+ priv->header = NULL;
+ priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_NEW_HEADER;
+ }
+
+ if (parser->flags & F_SPAMC) {
+ priv->msg->flags |= RSPAMD_HTTP_FLAG_SPAMC;
+ }
+
+ if (msg->method == HTTP_HEAD) {
+ /* We don't care about the rest */
+ rspamd_ev_watcher_stop(priv->ctx->event_loop, &priv->ev);
+ msg->code = parser->status_code;
+ rspamd_http_connection_ref(conn);
+ ret = conn->finish_handler(conn, msg);
+
+ if (conn->opts & RSPAMD_HTTP_CLIENT_KEEP_ALIVE) {
+ rspamd_http_context_push_keepalive(conn->priv->ctx, conn,
+ msg, conn->priv->ctx->event_loop);
+ rspamd_http_connection_reset(conn);
+ }
+ else {
+ conn->finished = TRUE;
+ }
+
+ rspamd_http_connection_unref(conn);
+
+ return ret;
+ }
+
+ priv->msg->method = parser->method;
+ priv->msg->code = parser->status_code;
+
+ return 0;
+}
+
+static int
+rspamd_http_decrypt_message(struct rspamd_http_connection *conn,
+ struct rspamd_http_connection_private *priv,
+ struct rspamd_cryptobox_pubkey *peer_key)
+{
+ guchar *nonce, *m;
+ const guchar *nm;
+ gsize dec_len;
+ struct rspamd_http_message *msg = priv->msg;
+ struct rspamd_http_header *hdr, *hcur, *hcurtmp;
+ struct http_parser decrypted_parser;
+ struct http_parser_settings decrypted_cb;
+ enum rspamd_cryptobox_mode mode;
+
+ mode = rspamd_keypair_alg(priv->local_key);
+ nonce = msg->body_buf.str;
+ m = msg->body_buf.str + rspamd_cryptobox_nonce_bytes(mode) +
+ rspamd_cryptobox_mac_bytes(mode);
+ dec_len = msg->body_buf.len - rspamd_cryptobox_nonce_bytes(mode) -
+ rspamd_cryptobox_mac_bytes(mode);
+
+ if ((nm = rspamd_pubkey_get_nm(peer_key, priv->local_key)) == NULL) {
+ nm = rspamd_pubkey_calculate_nm(peer_key, priv->local_key);
+ }
+
+ if (!rspamd_cryptobox_decrypt_nm_inplace(m, dec_len, nonce,
+ nm, m - rspamd_cryptobox_mac_bytes(mode), mode)) {
+ msg_err("cannot verify encrypted message, first bytes of the input: %*xs",
+ (gint) MIN(msg->body_buf.len, 64), msg->body_buf.begin);
+ return -1;
+ }
+
+ /* Cleanup message */
+ kh_foreach_value (msg->headers, hdr, {
+ DL_FOREACH_SAFE (hdr, hcur, hcurtmp) {
+ rspamd_fstring_free (hcur->combined);
+ g_free (hcur);
+}
+});
+
+kh_destroy(rspamd_http_headers_hash, msg->headers);
+msg->headers = kh_init(rspamd_http_headers_hash);
+
+if (msg->url != NULL) {
+ msg->url = rspamd_fstring_assign(msg->url, "", 0);
+}
+
+msg->body_buf.len = 0;
+
+memset(&decrypted_parser, 0, sizeof(decrypted_parser));
+http_parser_init(&decrypted_parser,
+ conn->type == RSPAMD_HTTP_SERVER ? HTTP_REQUEST : HTTP_RESPONSE);
+
+memset(&decrypted_cb, 0, sizeof(decrypted_cb));
+decrypted_cb.on_url = rspamd_http_on_url;
+decrypted_cb.on_status = rspamd_http_on_status;
+decrypted_cb.on_header_field = rspamd_http_on_header_field;
+decrypted_cb.on_header_value = rspamd_http_on_header_value;
+decrypted_cb.on_headers_complete = rspamd_http_on_headers_complete_decrypted;
+decrypted_cb.on_body = rspamd_http_on_body_decrypted;
+decrypted_parser.data = conn;
+decrypted_parser.content_length = dec_len;
+
+if (http_parser_execute(&decrypted_parser, &decrypted_cb, m,
+ dec_len) != (size_t) dec_len) {
+ msg_err("HTTP parser error: %s when parsing encrypted request",
+ http_errno_description(decrypted_parser.http_errno));
+ return -1;
+}
+
+return 0;
+}
+
+static int
+rspamd_http_on_message_complete(http_parser *parser)
+{
+ struct rspamd_http_connection *conn =
+ (struct rspamd_http_connection *) parser->data;
+ struct rspamd_http_connection_private *priv;
+ int ret = 0;
+ enum rspamd_cryptobox_mode mode;
+
+ if (conn->finished) {
+ return 0;
+ }
+
+ priv = conn->priv;
+
+ if ((conn->opts & RSPAMD_HTTP_REQUIRE_ENCRYPTION) && !IS_CONN_ENCRYPTED(priv)) {
+ priv->flags |= RSPAMD_HTTP_CONN_FLAG_ENCRYPTION_NEEDED;
+ msg_err("unencrypted connection when encryption has been requested");
+ return -1;
+ }
+
+ if ((conn->opts & RSPAMD_HTTP_BODY_PARTIAL) == 0 && IS_CONN_ENCRYPTED(priv)) {
+ mode = rspamd_keypair_alg(priv->local_key);
+
+ if (priv->local_key == NULL || priv->msg->peer_key == NULL ||
+ priv->msg->body_buf.len < rspamd_cryptobox_nonce_bytes(mode) +
+ rspamd_cryptobox_mac_bytes(mode)) {
+ msg_err("cannot decrypt message");
+ return -1;
+ }
+
+ /* We have keys, so we can decrypt message */
+ ret = rspamd_http_decrypt_message(conn, priv, priv->msg->peer_key);
+
+ if (ret != 0) {
+ return ret;
+ }
+
+ if (conn->body_handler != NULL) {
+ rspamd_http_connection_ref(conn);
+ ret = conn->body_handler(conn,
+ priv->msg,
+ priv->msg->body_buf.begin,
+ priv->msg->body_buf.len);
+ rspamd_http_connection_unref(conn);
+ }
+ }
+ else if ((conn->opts & RSPAMD_HTTP_BODY_PARTIAL) == 0 && conn->body_handler) {
+ g_assert(conn->body_handler != NULL);
+ rspamd_http_connection_ref(conn);
+ ret = conn->body_handler(conn,
+ priv->msg,
+ priv->msg->body_buf.begin,
+ priv->msg->body_buf.len);
+ rspamd_http_connection_unref(conn);
+ }
+
+ if (ret == 0) {
+ rspamd_ev_watcher_stop(priv->ctx->event_loop, &priv->ev);
+ rspamd_http_connection_ref(conn);
+ ret = conn->finish_handler(conn, priv->msg);
+
+ if (conn->opts & RSPAMD_HTTP_CLIENT_KEEP_ALIVE) {
+ rspamd_http_context_push_keepalive(conn->priv->ctx, conn,
+ priv->msg, conn->priv->ctx->event_loop);
+ rspamd_http_connection_reset(conn);
+ }
+ else {
+ conn->finished = TRUE;
+ }
+
+ rspamd_http_connection_unref(conn);
+ }
+
+ return ret;
+}
+
+static void
+rspamd_http_simple_client_helper(struct rspamd_http_connection *conn)
+{
+ struct rspamd_http_connection_private *priv;
+ gpointer ssl;
+ gint request_method;
+ GString *prev_host = NULL;
+
+ priv = conn->priv;
+ ssl = priv->ssl;
+ priv->ssl = NULL;
+
+ /* Preserve data */
+ if (priv->msg) {
+ request_method = priv->msg->method;
+ /* Preserve host for keepalive */
+ prev_host = priv->msg->host;
+ priv->msg->host = NULL;
+ }
+
+ rspamd_http_connection_reset(conn);
+ priv->ssl = ssl;
+
+ /* Plan read message */
+
+ if (conn->opts & RSPAMD_HTTP_CLIENT_SHARED) {
+ rspamd_http_connection_read_message_shared(conn, conn->ud,
+ conn->priv->timeout);
+ }
+ else {
+ rspamd_http_connection_read_message(conn, conn->ud,
+ conn->priv->timeout);
+ }
+
+ if (priv->msg) {
+ priv->msg->method = request_method;
+ priv->msg->host = prev_host;
+ }
+ else {
+ if (prev_host) {
+ g_string_free(prev_host, TRUE);
+ }
+ }
+}
+
+static void
+rspamd_http_write_helper(struct rspamd_http_connection *conn)
+{
+ struct rspamd_http_connection_private *priv;
+ struct iovec *start;
+ guint niov, i;
+ gint flags = 0;
+ gsize remain;
+ gssize r;
+ GError *err;
+ struct iovec *cur_iov;
+ struct msghdr msg;
+
+ priv = conn->priv;
+
+ if (priv->wr_pos == priv->wr_total) {
+ goto call_finish_handler;
+ }
+
+ start = &priv->out[0];
+ niov = priv->outlen;
+ remain = priv->wr_pos;
+ /* We know that niov is small enough for that */
+ if (priv->ssl) {
+ /* Might be recursive! */
+ cur_iov = g_malloc(niov * sizeof(struct iovec));
+ }
+ else {
+ cur_iov = alloca(niov * sizeof(struct iovec));
+ }
+ memcpy(cur_iov, priv->out, niov * sizeof(struct iovec));
+ for (i = 0; i < priv->outlen && remain > 0; i++) {
+ /* Find out the first iov required */
+ start = &cur_iov[i];
+ if (start->iov_len <= remain) {
+ remain -= start->iov_len;
+ start = &cur_iov[i + 1];
+ niov--;
+ }
+ else {
+ start->iov_base = (void *) ((char *) start->iov_base + remain);
+ start->iov_len -= remain;
+ remain = 0;
+ }
+ }
+
+ memset(&msg, 0, sizeof(msg));
+ msg.msg_iov = start;
+ msg.msg_iovlen = MIN(IOV_MAX, niov);
+ g_assert(niov > 0);
+#ifdef MSG_NOSIGNAL
+ flags = MSG_NOSIGNAL;
+#endif
+
+ if (priv->ssl) {
+ r = rspamd_ssl_writev(priv->ssl, msg.msg_iov, msg.msg_iovlen);
+ g_free(cur_iov);
+ }
+ else {
+ r = sendmsg(conn->fd, &msg, flags);
+ }
+
+ if (r == -1) {
+ if (!priv->ssl) {
+ err = g_error_new(HTTP_ERROR, 500, "IO write error: %s", strerror(errno));
+ rspamd_http_connection_ref(conn);
+ conn->error_handler(conn, err);
+ rspamd_http_connection_unref(conn);
+ g_error_free(err);
+ }
+
+ return;
+ }
+ else {
+ priv->wr_pos += r;
+ }
+
+ if (priv->wr_pos >= priv->wr_total) {
+ goto call_finish_handler;
+ }
+ else {
+ /* Want to write more */
+ priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_RESETED;
+
+ if (priv->ssl && r > 0) {
+ /* We can write more data... */
+ rspamd_http_write_helper(conn);
+ return;
+ }
+ }
+
+ return;
+
+call_finish_handler:
+ rspamd_ev_watcher_stop(priv->ctx->event_loop, &priv->ev);
+
+ if ((conn->opts & RSPAMD_HTTP_CLIENT_SIMPLE) == 0) {
+ rspamd_http_connection_ref(conn);
+ conn->finished = TRUE;
+ conn->finish_handler(conn, priv->msg);
+ rspamd_http_connection_unref(conn);
+ }
+ else {
+ /* Plan read message */
+ rspamd_http_simple_client_helper(conn);
+ }
+}
+
+static gssize
+rspamd_http_try_read(gint fd,
+ struct rspamd_http_connection *conn,
+ struct rspamd_http_connection_private *priv,
+ struct _rspamd_http_privbuf *pbuf,
+ const gchar **buf_ptr)
+{
+ gssize r;
+ gchar *data;
+ gsize len;
+ struct rspamd_http_message *msg;
+
+ msg = priv->msg;
+
+ if (pbuf->zc_buf == NULL) {
+ data = priv->buf->data->str;
+ len = priv->buf->data->allocated;
+ }
+ else {
+ data = (gchar *) pbuf->zc_buf;
+ len = pbuf->zc_remain;
+
+ if (len == 0) {
+ rspamd_http_message_grow_body(priv->msg, priv->buf->data->allocated);
+ rspamd_http_switch_zc(pbuf, msg);
+ data = (gchar *) pbuf->zc_buf;
+ len = pbuf->zc_remain;
+ }
+ }
+
+ if (priv->ssl) {
+ r = rspamd_ssl_read(priv->ssl, data, len);
+ }
+ else {
+ r = read(fd, data, len);
+ }
+
+ if (r <= 0) {
+ return r;
+ }
+ else {
+ if (pbuf->zc_buf == NULL) {
+ priv->buf->data->len = r;
+ }
+ else {
+ pbuf->zc_remain -= r;
+ pbuf->zc_buf += r;
+ }
+ }
+
+ if (buf_ptr) {
+ *buf_ptr = data;
+ }
+
+ return r;
+}
+
+static void
+rspamd_http_ssl_err_handler(gpointer ud, GError *err)
+{
+ struct rspamd_http_connection *conn = (struct rspamd_http_connection *) ud;
+
+ rspamd_http_connection_ref(conn);
+ conn->error_handler(conn, err);
+ rspamd_http_connection_unref(conn);
+}
+
+static void
+rspamd_http_event_handler(int fd, short what, gpointer ud)
+{
+ struct rspamd_http_connection *conn = (struct rspamd_http_connection *) ud;
+ struct rspamd_http_connection_private *priv;
+ struct _rspamd_http_privbuf *pbuf;
+ const gchar *d;
+ gssize r;
+ GError *err;
+
+ priv = conn->priv;
+ pbuf = priv->buf;
+ REF_RETAIN(pbuf);
+ rspamd_http_connection_ref(conn);
+
+ if (what == EV_READ) {
+ r = rspamd_http_try_read(fd, conn, priv, pbuf, &d);
+
+ if (r > 0) {
+ if (http_parser_execute(&priv->parser, &priv->parser_cb,
+ d, r) != (size_t) r ||
+ priv->parser.http_errno != 0) {
+ if (priv->flags & RSPAMD_HTTP_CONN_FLAG_TOO_LARGE) {
+ err = g_error_new(HTTP_ERROR, 413,
+ "Request entity too large: %zu",
+ (size_t) priv->parser.content_length);
+ }
+ else if (priv->flags & RSPAMD_HTTP_CONN_FLAG_ENCRYPTION_NEEDED) {
+ err = g_error_new(HTTP_ERROR, 400,
+ "Encryption required");
+ }
+ else if (priv->parser.http_errno == HPE_CLOSED_CONNECTION) {
+ msg_err("got garbage after end of the message, ignore it");
+
+ REF_RELEASE(pbuf);
+ rspamd_http_connection_unref(conn);
+
+ return;
+ }
+ else {
+ if (priv->parser.http_errno > HPE_CB_status) {
+ err = g_error_new(HTTP_ERROR, 400,
+ "HTTP parser error: %s",
+ http_errno_description(priv->parser.http_errno));
+ }
+ else {
+ err = g_error_new(HTTP_ERROR, 500,
+ "HTTP parser internal error: %s",
+ http_errno_description(priv->parser.http_errno));
+ }
+ }
+
+ if (!conn->finished) {
+ conn->error_handler(conn, err);
+ }
+ else {
+ msg_err("got error after HTTP request is finished: %e", err);
+ }
+
+ g_error_free(err);
+
+ REF_RELEASE(pbuf);
+ rspamd_http_connection_unref(conn);
+
+ return;
+ }
+ }
+ else if (r == 0) {
+ /* We can still call http parser */
+ http_parser_execute(&priv->parser, &priv->parser_cb, d, r);
+
+ if (!conn->finished) {
+ err = g_error_new(HTTP_ERROR,
+ 400,
+ "IO read error: unexpected EOF");
+ conn->error_handler(conn, err);
+ g_error_free(err);
+ }
+ REF_RELEASE(pbuf);
+ rspamd_http_connection_unref(conn);
+
+ return;
+ }
+ else {
+ if (!priv->ssl) {
+ err = g_error_new(HTTP_ERROR,
+ 500,
+ "HTTP IO read error: %s",
+ strerror(errno));
+ conn->error_handler(conn, err);
+ g_error_free(err);
+ }
+
+ REF_RELEASE(pbuf);
+ rspamd_http_connection_unref(conn);
+
+ return;
+ }
+ }
+ else if (what == EV_TIMEOUT) {
+ if (!priv->ssl) {
+ /* Let's try to read from the socket first */
+ r = rspamd_http_try_read(fd, conn, priv, pbuf, &d);
+
+ if (r > 0) {
+ if (http_parser_execute(&priv->parser, &priv->parser_cb,
+ d, r) != (size_t) r ||
+ priv->parser.http_errno != 0) {
+ err = g_error_new(HTTP_ERROR, 400,
+ "HTTP parser error: %s",
+ http_errno_description(priv->parser.http_errno));
+
+ if (!conn->finished) {
+ conn->error_handler(conn, err);
+ }
+ else {
+ msg_err("got error after HTTP request is finished: %e", err);
+ }
+
+ g_error_free(err);
+
+ REF_RELEASE(pbuf);
+ rspamd_http_connection_unref(conn);
+
+ return;
+ }
+ }
+ else {
+ err = g_error_new(HTTP_ERROR, 408,
+ "IO timeout");
+ conn->error_handler(conn, err);
+ g_error_free(err);
+
+ REF_RELEASE(pbuf);
+ rspamd_http_connection_unref(conn);
+
+ return;
+ }
+ }
+ else {
+ /* In case of SSL we disable this logic as we already came from SSL handler */
+ REF_RELEASE(pbuf);
+ rspamd_http_connection_unref(conn);
+
+ return;
+ }
+ }
+ else if (what == EV_WRITE) {
+ rspamd_http_write_helper(conn);
+ }
+
+ REF_RELEASE(pbuf);
+ rspamd_http_connection_unref(conn);
+}
+
+static void
+rspamd_http_parser_reset(struct rspamd_http_connection *conn)
+{
+ struct rspamd_http_connection_private *priv = conn->priv;
+
+ http_parser_init(&priv->parser,
+ conn->type == RSPAMD_HTTP_SERVER ? HTTP_REQUEST : HTTP_RESPONSE);
+
+ priv->parser_cb.on_url = rspamd_http_on_url;
+ priv->parser_cb.on_status = rspamd_http_on_status;
+ priv->parser_cb.on_header_field = rspamd_http_on_header_field;
+ priv->parser_cb.on_header_value = rspamd_http_on_header_value;
+ priv->parser_cb.on_headers_complete = rspamd_http_on_headers_complete;
+ priv->parser_cb.on_body = rspamd_http_on_body;
+ priv->parser_cb.on_message_complete = rspamd_http_on_message_complete;
+}
+
+static struct rspamd_http_connection *
+rspamd_http_connection_new_common(struct rspamd_http_context *ctx,
+ gint fd,
+ rspamd_http_body_handler_t body_handler,
+ rspamd_http_error_handler_t error_handler,
+ rspamd_http_finish_handler_t finish_handler,
+ unsigned opts,
+ enum rspamd_http_connection_type type,
+ enum rspamd_http_priv_flags priv_flags,
+ struct upstream *proxy_upstream)
+{
+ struct rspamd_http_connection *conn;
+ struct rspamd_http_connection_private *priv;
+
+ g_assert(error_handler != NULL && finish_handler != NULL);
+
+ if (ctx == NULL) {
+ ctx = rspamd_http_context_default();
+ }
+
+ conn = g_malloc0(sizeof(struct rspamd_http_connection));
+ conn->opts = opts;
+ conn->type = type;
+ conn->body_handler = body_handler;
+ conn->error_handler = error_handler;
+ conn->finish_handler = finish_handler;
+ conn->fd = fd;
+ conn->ref = 1;
+ conn->finished = FALSE;
+
+ /* Init priv */
+ priv = g_malloc0(sizeof(struct rspamd_http_connection_private));
+ conn->priv = priv;
+ priv->ctx = ctx;
+ priv->flags = priv_flags;
+
+ if (type == RSPAMD_HTTP_SERVER) {
+ priv->cache = ctx->server_kp_cache;
+ }
+ else {
+ priv->cache = ctx->client_kp_cache;
+ if (ctx->client_kp) {
+ priv->local_key = rspamd_keypair_ref(ctx->client_kp);
+ }
+ }
+
+ rspamd_http_parser_reset(conn);
+ priv->parser.data = conn;
+
+ return conn;
+}
+
+struct rspamd_http_connection *
+rspamd_http_connection_new_server(struct rspamd_http_context *ctx,
+ gint fd,
+ rspamd_http_body_handler_t body_handler,
+ rspamd_http_error_handler_t error_handler,
+ rspamd_http_finish_handler_t finish_handler,
+ unsigned opts)
+{
+ return rspamd_http_connection_new_common(ctx, fd, body_handler,
+ error_handler, finish_handler, opts, RSPAMD_HTTP_SERVER, 0, NULL);
+}
+
+struct rspamd_http_connection *
+rspamd_http_connection_new_client_socket(struct rspamd_http_context *ctx,
+ rspamd_http_body_handler_t body_handler,
+ rspamd_http_error_handler_t error_handler,
+ rspamd_http_finish_handler_t finish_handler,
+ unsigned opts,
+ gint fd)
+{
+ return rspamd_http_connection_new_common(ctx, fd, body_handler,
+ error_handler, finish_handler, opts, RSPAMD_HTTP_CLIENT, 0, NULL);
+}
+
+struct rspamd_http_connection *
+rspamd_http_connection_new_client(struct rspamd_http_context *ctx,
+ rspamd_http_body_handler_t body_handler,
+ rspamd_http_error_handler_t error_handler,
+ rspamd_http_finish_handler_t finish_handler,
+ unsigned opts,
+ rspamd_inet_addr_t *addr)
+{
+ gint fd;
+
+ if (ctx == NULL) {
+ ctx = rspamd_http_context_default();
+ }
+
+ if (ctx->http_proxies) {
+ struct upstream *up = rspamd_upstream_get(ctx->http_proxies,
+ RSPAMD_UPSTREAM_ROUND_ROBIN, NULL, 0);
+
+ if (up) {
+ rspamd_inet_addr_t *proxy_addr = rspamd_upstream_addr_next(up);
+
+ fd = rspamd_inet_address_connect(proxy_addr, SOCK_STREAM, TRUE);
+
+ if (fd == -1) {
+ msg_info("cannot connect to http proxy %s: %s",
+ rspamd_inet_address_to_string_pretty(proxy_addr),
+ strerror(errno));
+ rspamd_upstream_fail(up, TRUE, strerror(errno));
+
+ return NULL;
+ }
+
+ return rspamd_http_connection_new_common(ctx, fd, body_handler,
+ error_handler, finish_handler, opts,
+ RSPAMD_HTTP_CLIENT,
+ RSPAMD_HTTP_CONN_OWN_SOCKET | RSPAMD_HTTP_CONN_FLAG_PROXY,
+ up);
+ }
+ }
+
+ /* Unproxied version */
+ fd = rspamd_inet_address_connect(addr, SOCK_STREAM, TRUE);
+
+ if (fd == -1) {
+ msg_info("cannot connect make http connection to %s: %s",
+ rspamd_inet_address_to_string_pretty(addr),
+ strerror(errno));
+
+ return NULL;
+ }
+
+ return rspamd_http_connection_new_common(ctx, fd, body_handler,
+ error_handler, finish_handler, opts,
+ RSPAMD_HTTP_CLIENT,
+ RSPAMD_HTTP_CONN_OWN_SOCKET,
+ NULL);
+}
+
+struct rspamd_http_connection *
+rspamd_http_connection_new_client_keepalive(struct rspamd_http_context *ctx,
+ rspamd_http_body_handler_t body_handler,
+ rspamd_http_error_handler_t error_handler,
+ rspamd_http_finish_handler_t finish_handler,
+ unsigned opts,
+ rspamd_inet_addr_t *addr,
+ const gchar *host)
+{
+ struct rspamd_http_connection *conn;
+
+ if (ctx == NULL) {
+ ctx = rspamd_http_context_default();
+ }
+
+ conn = rspamd_http_context_check_keepalive(ctx, addr, host,
+ opts & RSPAMD_HTTP_CLIENT_SSL);
+
+ if (conn) {
+ return conn;
+ }
+
+ conn = rspamd_http_connection_new_client(ctx,
+ body_handler, error_handler, finish_handler,
+ opts | RSPAMD_HTTP_CLIENT_SIMPLE | RSPAMD_HTTP_CLIENT_KEEP_ALIVE,
+ addr);
+
+ if (conn) {
+ rspamd_http_context_prepare_keepalive(ctx, conn, addr, host,
+ opts & RSPAMD_HTTP_CLIENT_SSL);
+ }
+
+ return conn;
+}
+
+void rspamd_http_connection_reset(struct rspamd_http_connection *conn)
+{
+ struct rspamd_http_connection_private *priv;
+ struct rspamd_http_message *msg;
+
+ priv = conn->priv;
+ msg = priv->msg;
+
+ /* Clear request */
+ if (msg != NULL) {
+ if (msg->peer_key) {
+ priv->peer_key = msg->peer_key;
+ msg->peer_key = NULL;
+ }
+ rspamd_http_message_unref(msg);
+ priv->msg = NULL;
+ }
+
+ conn->finished = FALSE;
+ /* Clear priv */
+ rspamd_ev_watcher_stop(priv->ctx->event_loop, &priv->ev);
+
+ if (!(priv->flags & RSPAMD_HTTP_CONN_FLAG_RESETED)) {
+ rspamd_http_parser_reset(conn);
+ }
+
+ if (priv->buf != NULL) {
+ REF_RELEASE(priv->buf);
+ priv->buf = NULL;
+ }
+
+ if (priv->out != NULL) {
+ g_free(priv->out);
+ priv->out = NULL;
+ }
+
+ priv->flags |= RSPAMD_HTTP_CONN_FLAG_RESETED;
+}
+
+struct rspamd_http_message *
+rspamd_http_connection_steal_msg(struct rspamd_http_connection *conn)
+{
+ struct rspamd_http_connection_private *priv;
+ struct rspamd_http_message *msg;
+
+ priv = conn->priv;
+ msg = priv->msg;
+
+ /* Clear request */
+ if (msg != NULL) {
+ if (msg->peer_key) {
+ priv->peer_key = msg->peer_key;
+ msg->peer_key = NULL;
+ }
+ priv->msg = NULL;
+ }
+
+ return msg;
+}
+
+struct rspamd_http_message *
+rspamd_http_connection_copy_msg(struct rspamd_http_message *msg, GError **err)
+{
+ struct rspamd_http_message *new_msg;
+ struct rspamd_http_header *hdr, *nhdr, *nhdrs, *hcur;
+ const gchar *old_body;
+ gsize old_len;
+ struct stat st;
+ union _rspamd_storage_u *storage;
+
+ new_msg = rspamd_http_new_message(msg->type);
+ new_msg->flags = msg->flags;
+
+ if (msg->body_buf.len > 0) {
+
+ if (msg->flags & RSPAMD_HTTP_FLAG_SHMEM) {
+ /* Avoid copying by just mapping a shared segment */
+ new_msg->flags |= RSPAMD_HTTP_FLAG_SHMEM_IMMUTABLE;
+
+ storage = &new_msg->body_buf.c;
+ storage->shared.shm_fd = dup(msg->body_buf.c.shared.shm_fd);
+
+ if (storage->shared.shm_fd == -1) {
+ rspamd_http_message_unref(new_msg);
+ g_set_error(err, http_error_quark(), errno,
+ "cannot dup shmem fd: %d: %s",
+ msg->body_buf.c.shared.shm_fd, strerror(errno));
+
+ return NULL;
+ }
+
+ if (fstat(storage->shared.shm_fd, &st) == -1) {
+ g_set_error(err, http_error_quark(), errno,
+ "cannot stat shmem fd: %d: %s",
+ storage->shared.shm_fd, strerror(errno));
+ rspamd_http_message_unref(new_msg);
+
+ return NULL;
+ }
+
+ /* We don't own segment, so do not try to touch it */
+
+ if (msg->body_buf.c.shared.name) {
+ storage->shared.name = msg->body_buf.c.shared.name;
+ REF_RETAIN(storage->shared.name);
+ }
+
+ new_msg->body_buf.str = mmap(NULL, st.st_size,
+ PROT_READ, MAP_SHARED,
+ storage->shared.shm_fd, 0);
+
+ if (new_msg->body_buf.str == MAP_FAILED) {
+ g_set_error(err, http_error_quark(), errno,
+ "cannot mmap shmem fd: %d: %s",
+ storage->shared.shm_fd, strerror(errno));
+ rspamd_http_message_unref(new_msg);
+
+ return NULL;
+ }
+
+ new_msg->body_buf.begin = new_msg->body_buf.str;
+ new_msg->body_buf.len = msg->body_buf.len;
+ new_msg->body_buf.begin = new_msg->body_buf.str +
+ (msg->body_buf.begin - msg->body_buf.str);
+ }
+ else {
+ old_body = rspamd_http_message_get_body(msg, &old_len);
+
+ if (!rspamd_http_message_set_body(new_msg, old_body, old_len)) {
+ g_set_error(err, http_error_quark(), errno,
+ "cannot set body for message, length: %zd",
+ old_len);
+ rspamd_http_message_unref(new_msg);
+
+ return NULL;
+ }
+ }
+ }
+
+ if (msg->url) {
+ if (new_msg->url) {
+ new_msg->url = rspamd_fstring_append(new_msg->url, msg->url->str,
+ msg->url->len);
+ }
+ else {
+ new_msg->url = rspamd_fstring_new_init(msg->url->str,
+ msg->url->len);
+ }
+ }
+
+ if (msg->host) {
+ new_msg->host = g_string_new_len(msg->host->str, msg->host->len);
+ }
+
+ new_msg->method = msg->method;
+ new_msg->port = msg->port;
+ new_msg->date = msg->date;
+ new_msg->last_modified = msg->last_modified;
+
+ kh_foreach_value(msg->headers, hdr, {
+ nhdrs = NULL;
+
+ DL_FOREACH(hdr, hcur)
+ {
+ nhdr = g_malloc(sizeof(struct rspamd_http_header));
+
+ nhdr->combined = rspamd_fstring_new_init(hcur->combined->str,
+ hcur->combined->len);
+ nhdr->name.begin = nhdr->combined->str +
+ (hcur->name.begin - hcur->combined->str);
+ nhdr->name.len = hcur->name.len;
+ nhdr->value.begin = nhdr->combined->str +
+ (hcur->value.begin - hcur->combined->str);
+ nhdr->value.len = hcur->value.len;
+ DL_APPEND(nhdrs, nhdr);
+ }
+
+ gint r;
+ khiter_t k = kh_put(rspamd_http_headers_hash, new_msg->headers,
+ &nhdrs->name, &r);
+
+ if (r != 0) {
+ kh_value(new_msg->headers, k) = nhdrs;
+ }
+ else {
+ DL_CONCAT(kh_value(new_msg->headers, k), nhdrs);
+ }
+ });
+
+ return new_msg;
+}
+
+void rspamd_http_connection_free(struct rspamd_http_connection *conn)
+{
+ struct rspamd_http_connection_private *priv;
+
+ priv = conn->priv;
+
+ if (priv != NULL) {
+ rspamd_http_connection_reset(conn);
+
+ if (priv->ssl) {
+ rspamd_ssl_connection_free(priv->ssl);
+ priv->ssl = NULL;
+ }
+
+ if (priv->local_key) {
+ rspamd_keypair_unref(priv->local_key);
+ }
+ if (priv->peer_key) {
+ rspamd_pubkey_unref(priv->peer_key);
+ }
+
+ if (priv->flags & RSPAMD_HTTP_CONN_OWN_SOCKET) {
+ /* Fd is owned by a connection */
+ close(conn->fd);
+ }
+
+ g_free(priv);
+ }
+
+ g_free(conn);
+}
+
+static void
+rspamd_http_connection_read_message_common(struct rspamd_http_connection *conn,
+ gpointer ud, ev_tstamp timeout,
+ gint flags)
+{
+ struct rspamd_http_connection_private *priv = conn->priv;
+ struct rspamd_http_message *req;
+
+ conn->ud = ud;
+ req = rspamd_http_new_message(
+ conn->type == RSPAMD_HTTP_SERVER ? HTTP_REQUEST : HTTP_RESPONSE);
+ priv->msg = req;
+ req->flags = flags;
+
+ if (flags & RSPAMD_HTTP_FLAG_SHMEM) {
+ req->body_buf.c.shared.shm_fd = -1;
+ }
+
+ if (priv->peer_key) {
+ priv->msg->peer_key = priv->peer_key;
+ priv->peer_key = NULL;
+ priv->flags |= RSPAMD_HTTP_CONN_FLAG_ENCRYPTED;
+ }
+
+ priv->timeout = timeout;
+ priv->header = NULL;
+ priv->buf = g_malloc0(sizeof(*priv->buf));
+ REF_INIT_RETAIN(priv->buf, rspamd_http_privbuf_dtor);
+ priv->buf->data = rspamd_fstring_sized_new(8192);
+ priv->flags |= RSPAMD_HTTP_CONN_FLAG_NEW_HEADER;
+
+ if (!priv->ssl) {
+ rspamd_ev_watcher_init(&priv->ev, conn->fd, EV_READ,
+ rspamd_http_event_handler, conn);
+ rspamd_ev_watcher_start(priv->ctx->event_loop, &priv->ev, priv->timeout);
+ }
+ else {
+ rspamd_ssl_connection_restore_handlers(priv->ssl,
+ rspamd_http_event_handler,
+ rspamd_http_ssl_err_handler,
+ conn,
+ EV_READ);
+ }
+
+ priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_RESETED;
+}
+
+void rspamd_http_connection_read_message(struct rspamd_http_connection *conn,
+ gpointer ud, ev_tstamp timeout)
+{
+ rspamd_http_connection_read_message_common(conn, ud, timeout, 0);
+}
+
+void rspamd_http_connection_read_message_shared(struct rspamd_http_connection *conn,
+ gpointer ud, ev_tstamp timeout)
+{
+ rspamd_http_connection_read_message_common(conn, ud, timeout,
+ RSPAMD_HTTP_FLAG_SHMEM);
+}
+
+static void
+rspamd_http_connection_encrypt_message(
+ struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg,
+ struct rspamd_http_connection_private *priv,
+ guchar *pbody,
+ guint bodylen,
+ guchar *pmethod,
+ guint methodlen,
+ guint preludelen,
+ gint hdrcount,
+ guchar *np,
+ guchar *mp,
+ struct rspamd_cryptobox_pubkey *peer_key)
+{
+ struct rspamd_cryptobox_segment *segments;
+ guchar *crlfp;
+ const guchar *nm;
+ gint i, cnt;
+ guint outlen;
+ struct rspamd_http_header *hdr, *hcur;
+ enum rspamd_cryptobox_mode mode;
+
+ mode = rspamd_keypair_alg(priv->local_key);
+ crlfp = mp + rspamd_cryptobox_mac_bytes(mode);
+
+ outlen = priv->out[0].iov_len + priv->out[1].iov_len;
+ /*
+ * Create segments from the following:
+ * Method, [URL], CRLF, nheaders, CRLF, body
+ */
+ segments = g_new(struct rspamd_cryptobox_segment, hdrcount + 5);
+
+ segments[0].data = pmethod;
+ segments[0].len = methodlen;
+
+ if (conn->type != RSPAMD_HTTP_SERVER) {
+ segments[1].data = msg->url->str;
+ segments[1].len = msg->url->len;
+ /* space + HTTP version + crlf */
+ segments[2].data = crlfp;
+ segments[2].len = preludelen - 2;
+ crlfp += segments[2].len;
+ i = 3;
+ }
+ else {
+ /* Here we send just CRLF */
+ segments[1].data = crlfp;
+ segments[1].len = 2;
+ crlfp += segments[1].len;
+
+ i = 2;
+ }
+
+
+ kh_foreach_value (msg->headers, hdr, {
+ DL_FOREACH (hdr, hcur) {
+ segments[i].data = hcur->combined->str;
+ segments[i++].len = hcur->combined->len;
+}
+});
+
+/* crlfp should point now at the second crlf */
+segments[i].data = crlfp;
+segments[i++].len = 2;
+
+if (pbody) {
+ segments[i].data = pbody;
+ segments[i++].len = bodylen;
+}
+
+cnt = i;
+
+if ((nm = rspamd_pubkey_get_nm(peer_key, priv->local_key)) == NULL) {
+ nm = rspamd_pubkey_calculate_nm(peer_key, priv->local_key);
+}
+
+rspamd_cryptobox_encryptv_nm_inplace(segments, cnt, np, nm, mp, mode);
+
+/*
+ * iov[0] = base HTTP request
+ * iov[1] = CRLF
+ * iov[2] = nonce
+ * iov[3] = mac
+ * iov[4..i] = encrypted HTTP request/reply
+ */
+priv->out[2].iov_base = np;
+priv->out[2].iov_len = rspamd_cryptobox_nonce_bytes(mode);
+priv->out[3].iov_base = mp;
+priv->out[3].iov_len = rspamd_cryptobox_mac_bytes(mode);
+
+outlen += rspamd_cryptobox_nonce_bytes(mode) +
+ rspamd_cryptobox_mac_bytes(mode);
+
+for (i = 0; i < cnt; i++) {
+ priv->out[i + 4].iov_base = segments[i].data;
+ priv->out[i + 4].iov_len = segments[i].len;
+ outlen += segments[i].len;
+}
+
+priv->wr_total = outlen;
+
+g_free(segments);
+}
+
+static void
+rspamd_http_detach_shared(struct rspamd_http_message *msg)
+{
+ rspamd_fstring_t *cpy_str;
+
+ cpy_str = rspamd_fstring_new_init(msg->body_buf.begin, msg->body_buf.len);
+ rspamd_http_message_set_body_from_fstring_steal(msg, cpy_str);
+}
+
+gint rspamd_http_message_write_header(const gchar *mime_type, gboolean encrypted,
+ gchar *repbuf, gsize replen, gsize bodylen, gsize enclen, const gchar *host,
+ struct rspamd_http_connection *conn, struct rspamd_http_message *msg,
+ rspamd_fstring_t **buf,
+ struct rspamd_http_connection_private *priv,
+ struct rspamd_cryptobox_pubkey *peer_key)
+{
+ gchar datebuf[64];
+ gint meth_len = 0;
+ const gchar *conn_type = "close";
+
+ if (conn->type == RSPAMD_HTTP_SERVER) {
+ /* Format reply */
+ if (msg->method < HTTP_SYMBOLS) {
+ rspamd_ftok_t status;
+
+ rspamd_http_date_format(datebuf, sizeof(datebuf), msg->date);
+
+ if (mime_type == NULL) {
+ mime_type =
+ encrypted ? "application/octet-stream" : "text/plain";
+ }
+
+ if (msg->status == NULL || msg->status->len == 0) {
+ if (msg->code == 200) {
+ RSPAMD_FTOK_ASSIGN(&status, "OK");
+ }
+ else if (msg->code == 404) {
+ RSPAMD_FTOK_ASSIGN(&status, "Not Found");
+ }
+ else if (msg->code == 403) {
+ RSPAMD_FTOK_ASSIGN(&status, "Forbidden");
+ }
+ else if (msg->code >= 500 && msg->code < 600) {
+ RSPAMD_FTOK_ASSIGN(&status, "Internal Server Error");
+ }
+ else {
+ RSPAMD_FTOK_ASSIGN(&status, "Undefined Error");
+ }
+ }
+ else {
+ status.begin = msg->status->str;
+ status.len = msg->status->len;
+ }
+
+ if (encrypted) {
+ /* Internal reply (encrypted) */
+ if (mime_type) {
+ meth_len =
+ rspamd_snprintf(repbuf, replen,
+ "HTTP/1.1 %d %T\r\n"
+ "Connection: close\r\n"
+ "Server: %s\r\n"
+ "Date: %s\r\n"
+ "Content-Length: %z\r\n"
+ "Content-Type: %s", /* NO \r\n at the end ! */
+ msg->code, &status, priv->ctx->config.server_hdr,
+ datebuf,
+ bodylen, mime_type);
+ }
+ else {
+ meth_len =
+ rspamd_snprintf(repbuf, replen,
+ "HTTP/1.1 %d %T\r\n"
+ "Connection: close\r\n"
+ "Server: %s\r\n"
+ "Date: %s\r\n"
+ "Content-Length: %z", /* NO \r\n at the end ! */
+ msg->code, &status, priv->ctx->config.server_hdr,
+ datebuf,
+ bodylen);
+ }
+ enclen += meth_len;
+ /* External reply */
+ rspamd_printf_fstring(buf,
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n"
+ "Server: %s\r\n"
+ "Date: %s\r\n"
+ "Content-Length: %z\r\n"
+ "Content-Type: application/octet-stream\r\n",
+ priv->ctx->config.server_hdr,
+ datebuf, enclen);
+ }
+ else {
+ if (mime_type) {
+ meth_len =
+ rspamd_printf_fstring(buf,
+ "HTTP/1.1 %d %T\r\n"
+ "Connection: close\r\n"
+ "Server: %s\r\n"
+ "Date: %s\r\n"
+ "Content-Length: %z\r\n"
+ "Content-Type: %s\r\n",
+ msg->code, &status, priv->ctx->config.server_hdr,
+ datebuf,
+ bodylen, mime_type);
+ }
+ else {
+ meth_len =
+ rspamd_printf_fstring(buf,
+ "HTTP/1.1 %d %T\r\n"
+ "Connection: close\r\n"
+ "Server: %s\r\n"
+ "Date: %s\r\n"
+ "Content-Length: %z\r\n",
+ msg->code, &status, priv->ctx->config.server_hdr,
+ datebuf,
+ bodylen);
+ }
+ }
+ }
+ else {
+ /* Legacy spamd reply */
+ if (msg->flags & RSPAMD_HTTP_FLAG_SPAMC) {
+ gsize real_bodylen;
+ goffset eoh_pos;
+ GString tmp;
+
+ /* Unfortunately, spamc protocol is deadly brain damaged */
+ tmp.str = (gchar *) msg->body_buf.begin;
+ tmp.len = msg->body_buf.len;
+
+ if (rspamd_string_find_eoh(&tmp, &eoh_pos) != -1 &&
+ bodylen > eoh_pos) {
+ real_bodylen = bodylen - eoh_pos;
+ }
+ else {
+ real_bodylen = bodylen;
+ }
+
+ rspamd_printf_fstring(buf, "SPAMD/1.1 0 EX_OK\r\n"
+ "Content-length: %z\r\n",
+ real_bodylen);
+ }
+ else {
+ rspamd_printf_fstring(buf, "RSPAMD/1.3 0 EX_OK\r\n");
+ }
+ }
+ }
+ else {
+
+ /* Client request */
+ if (conn->opts & RSPAMD_HTTP_CLIENT_KEEP_ALIVE) {
+ conn_type = "keep-alive";
+ }
+
+ /* Format request */
+ enclen += RSPAMD_FSTRING_LEN(msg->url) +
+ strlen(http_method_str(msg->method)) + 1;
+
+ if (host == NULL && msg->host == NULL) {
+ /* Fallback to HTTP/1.0 */
+ if (encrypted) {
+ rspamd_printf_fstring(buf,
+ "%s %s HTTP/1.0\r\n"
+ "Content-Length: %z\r\n"
+ "Content-Type: application/octet-stream\r\n"
+ "Connection: %s\r\n",
+ "POST",
+ "/post",
+ enclen,
+ conn_type);
+ }
+ else {
+ rspamd_printf_fstring(buf,
+ "%s %V HTTP/1.0\r\n"
+ "Content-Length: %z\r\n"
+ "Connection: %s\r\n",
+ http_method_str(msg->method),
+ msg->url,
+ bodylen,
+ conn_type);
+
+ if (bodylen > 0) {
+ if (mime_type == NULL) {
+ mime_type = "text/plain";
+ }
+
+ rspamd_printf_fstring(buf,
+ "Content-Type: %s\r\n",
+ mime_type);
+ }
+ }
+ }
+ else {
+ /* Normal HTTP/1.1 with Host */
+ if (host == NULL) {
+ host = msg->host->str;
+ }
+
+ if (encrypted) {
+ /* TODO: Add proxy support to HTTPCrypt */
+ if (rspamd_http_message_is_standard_port(msg)) {
+ rspamd_printf_fstring(buf,
+ "%s %s HTTP/1.1\r\n"
+ "Connection: %s\r\n"
+ "Host: %s\r\n"
+ "Content-Length: %z\r\n"
+ "Content-Type: application/octet-stream\r\n",
+ "POST",
+ "/post",
+ conn_type,
+ host,
+ enclen);
+ }
+ else {
+ rspamd_printf_fstring(buf,
+ "%s %s HTTP/1.1\r\n"
+ "Connection: %s\r\n"
+ "Host: %s:%d\r\n"
+ "Content-Length: %z\r\n"
+ "Content-Type: application/octet-stream\r\n",
+ "POST",
+ "/post",
+ conn_type,
+ host,
+ msg->port,
+ enclen);
+ }
+ }
+ else {
+ if (conn->priv->flags & RSPAMD_HTTP_CONN_FLAG_PROXY) {
+ /* Write proxied request */
+ if ((msg->flags & RSPAMD_HTTP_FLAG_HAS_HOST_HEADER)) {
+ rspamd_printf_fstring(buf,
+ "%s %s://%s:%d/%V HTTP/1.1\r\n"
+ "Connection: %s\r\n"
+ "Content-Length: %z\r\n",
+ http_method_str(msg->method),
+ (conn->opts & RSPAMD_HTTP_CLIENT_SSL) ? "https" : "http",
+ host,
+ msg->port,
+ msg->url,
+ conn_type,
+ bodylen);
+ }
+ else {
+ if (rspamd_http_message_is_standard_port(msg)) {
+ rspamd_printf_fstring(buf,
+ "%s %s://%s:%d/%V HTTP/1.1\r\n"
+ "Connection: %s\r\n"
+ "Host: %s\r\n"
+ "Content-Length: %z\r\n",
+ http_method_str(msg->method),
+ (conn->opts & RSPAMD_HTTP_CLIENT_SSL) ? "https" : "http",
+ host,
+ msg->port,
+ msg->url,
+ conn_type,
+ host,
+ bodylen);
+ }
+ else {
+ rspamd_printf_fstring(buf,
+ "%s %s://%s:%d/%V HTTP/1.1\r\n"
+ "Connection: %s\r\n"
+ "Host: %s:%d\r\n"
+ "Content-Length: %z\r\n",
+ http_method_str(msg->method),
+ (conn->opts & RSPAMD_HTTP_CLIENT_SSL) ? "https" : "http",
+ host,
+ msg->port,
+ msg->url,
+ conn_type,
+ host,
+ msg->port,
+ bodylen);
+ }
+ }
+ }
+ else {
+ /* Unproxied version */
+ if ((msg->flags & RSPAMD_HTTP_FLAG_HAS_HOST_HEADER)) {
+ rspamd_printf_fstring(buf,
+ "%s %V HTTP/1.1\r\n"
+ "Connection: %s\r\n"
+ "Content-Length: %z\r\n",
+ http_method_str(msg->method),
+ msg->url,
+ conn_type,
+ bodylen);
+ }
+ else {
+ if (rspamd_http_message_is_standard_port(msg)) {
+ rspamd_printf_fstring(buf,
+ "%s %V HTTP/1.1\r\n"
+ "Connection: %s\r\n"
+ "Host: %s\r\n"
+ "Content-Length: %z\r\n",
+ http_method_str(msg->method),
+ msg->url,
+ conn_type,
+ host,
+ bodylen);
+ }
+ else {
+ rspamd_printf_fstring(buf,
+ "%s %V HTTP/1.1\r\n"
+ "Connection: %s\r\n"
+ "Host: %s:%d\r\n"
+ "Content-Length: %z\r\n",
+ http_method_str(msg->method),
+ msg->url,
+ conn_type,
+ host,
+ msg->port,
+ bodylen);
+ }
+ }
+ }
+
+ if (bodylen > 0) {
+ if (mime_type != NULL) {
+ rspamd_printf_fstring(buf,
+ "Content-Type: %s\r\n",
+ mime_type);
+ }
+ }
+ }
+ }
+
+ if (encrypted) {
+ GString *b32_key, *b32_id;
+
+ b32_key = rspamd_keypair_print(priv->local_key,
+ RSPAMD_KEYPAIR_PUBKEY | RSPAMD_KEYPAIR_BASE32);
+ b32_id = rspamd_pubkey_print(peer_key,
+ RSPAMD_KEYPAIR_ID_SHORT | RSPAMD_KEYPAIR_BASE32);
+ /* XXX: add some fuzz here */
+ rspamd_printf_fstring(&*buf, "Key: %v=%v\r\n", b32_id, b32_key);
+ g_string_free(b32_key, TRUE);
+ g_string_free(b32_id, TRUE);
+ }
+ }
+
+ return meth_len;
+}
+
+static gboolean
+rspamd_http_connection_write_message_common(struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg,
+ const gchar *host,
+ const gchar *mime_type,
+ gpointer ud,
+ ev_tstamp timeout,
+ gboolean allow_shared)
+{
+ struct rspamd_http_connection_private *priv = conn->priv;
+ struct rspamd_http_header *hdr, *hcur;
+ gchar repbuf[512], *pbody;
+ gint i, hdrcount, meth_len = 0, preludelen = 0;
+ gsize bodylen, enclen = 0;
+ rspamd_fstring_t *buf;
+ gboolean encrypted = FALSE;
+ guchar nonce[rspamd_cryptobox_MAX_NONCEBYTES], mac[rspamd_cryptobox_MAX_MACBYTES];
+ guchar *np = NULL, *mp = NULL, *meth_pos = NULL;
+ struct rspamd_cryptobox_pubkey *peer_key = NULL;
+ enum rspamd_cryptobox_mode mode;
+ GError *err;
+
+ conn->ud = ud;
+ priv->msg = msg;
+ priv->timeout = timeout;
+
+ priv->header = NULL;
+ priv->buf = g_malloc0(sizeof(*priv->buf));
+ REF_INIT_RETAIN(priv->buf, rspamd_http_privbuf_dtor);
+ priv->buf->data = rspamd_fstring_sized_new(512);
+ buf = priv->buf->data;
+
+ if ((msg->flags & RSPAMD_HTTP_FLAG_WANT_SSL) && !(conn->opts & RSPAMD_HTTP_CLIENT_SSL)) {
+ err = g_error_new(HTTP_ERROR, 400,
+ "SSL connection requested but not created properly, internal error");
+ rspamd_http_connection_ref(conn);
+ conn->error_handler(conn, err);
+ rspamd_http_connection_unref(conn);
+ g_error_free(err);
+ return FALSE;
+ }
+
+ if (priv->peer_key && priv->local_key) {
+ priv->msg->peer_key = priv->peer_key;
+ priv->peer_key = NULL;
+ priv->flags |= RSPAMD_HTTP_CONN_FLAG_ENCRYPTED;
+ }
+
+ if (msg->peer_key != NULL) {
+ if (priv->local_key == NULL) {
+ /* Automatically generate a temporary keypair */
+ priv->local_key = rspamd_keypair_new(RSPAMD_KEYPAIR_KEX,
+ RSPAMD_CRYPTOBOX_MODE_25519);
+ }
+
+ encrypted = TRUE;
+
+ if (priv->cache) {
+ rspamd_keypair_cache_process(priv->cache,
+ priv->local_key, priv->msg->peer_key);
+ }
+ }
+
+ if (encrypted && (msg->flags &
+ (RSPAMD_HTTP_FLAG_SHMEM_IMMUTABLE | RSPAMD_HTTP_FLAG_SHMEM))) {
+ /* We cannot use immutable body to encrypt message in place */
+ allow_shared = FALSE;
+ rspamd_http_detach_shared(msg);
+ }
+
+ if (allow_shared) {
+ gchar tmpbuf[64];
+
+ if (!(msg->flags & RSPAMD_HTTP_FLAG_SHMEM) ||
+ msg->body_buf.c.shared.name == NULL) {
+ allow_shared = FALSE;
+ }
+ else {
+ /* Insert new headers */
+ rspamd_http_message_add_header(msg, "Shm",
+ msg->body_buf.c.shared.name->shm_name);
+ rspamd_snprintf(tmpbuf, sizeof(tmpbuf), "%d",
+ (int) (msg->body_buf.begin - msg->body_buf.str));
+ rspamd_http_message_add_header(msg, "Shm-Offset",
+ tmpbuf);
+ rspamd_snprintf(tmpbuf, sizeof(tmpbuf), "%z",
+ msg->body_buf.len);
+ rspamd_http_message_add_header(msg, "Shm-Length",
+ tmpbuf);
+ }
+ }
+
+ if (priv->ctx->config.user_agent && conn->type == RSPAMD_HTTP_CLIENT) {
+ rspamd_ftok_t srch;
+ khiter_t k;
+ gint r;
+
+ RSPAMD_FTOK_ASSIGN(&srch, "User-Agent");
+
+ k = kh_put(rspamd_http_headers_hash, msg->headers, &srch, &r);
+
+ if (r != 0) {
+ hdr = g_malloc0(sizeof(struct rspamd_http_header));
+ guint vlen = strlen(priv->ctx->config.user_agent);
+ hdr->combined = rspamd_fstring_sized_new(srch.len + vlen + 4);
+ rspamd_printf_fstring(&hdr->combined, "%T: %*s\r\n", &srch, vlen,
+ priv->ctx->config.user_agent);
+ hdr->name.begin = hdr->combined->str;
+ hdr->name.len = srch.len;
+ hdr->value.begin = hdr->combined->str + srch.len + 2;
+ hdr->value.len = vlen;
+ hdr->prev = hdr; /* for utlists */
+
+ kh_value(msg->headers, k) = hdr;
+ /* as we searched using static buffer */
+ kh_key(msg->headers, k) = &hdr->name;
+ }
+ }
+
+ if (encrypted) {
+ mode = rspamd_keypair_alg(priv->local_key);
+
+ if (msg->body_buf.len == 0) {
+ pbody = NULL;
+ bodylen = 0;
+ msg->method = HTTP_GET;
+ }
+ else {
+ pbody = (gchar *) msg->body_buf.begin;
+ bodylen = msg->body_buf.len;
+ msg->method = HTTP_POST;
+ }
+
+ if (conn->type == RSPAMD_HTTP_SERVER) {
+ /*
+ * iov[0] = base reply
+ * iov[1] = CRLF
+ * iov[2] = nonce
+ * iov[3] = mac
+ * iov[4] = encrypted reply
+ * iov[6] = encrypted crlf
+ * iov[7..n] = encrypted headers
+ * iov[n + 1] = encrypted crlf
+ * [iov[n + 2] = encrypted body]
+ */
+ priv->outlen = 7;
+ enclen = rspamd_cryptobox_nonce_bytes(mode) +
+ rspamd_cryptobox_mac_bytes(mode) +
+ 4 + /* 2 * CRLF */
+ bodylen;
+ }
+ else {
+ /*
+ * iov[0] = base request
+ * iov[1] = CRLF
+ * iov[2] = nonce
+ * iov[3] = mac
+ * iov[4] = encrypted method + space
+ * iov[5] = encrypted url
+ * iov[7] = encrypted prelude
+ * iov[8..n] = encrypted headers
+ * iov[n + 1] = encrypted crlf
+ * [iov[n + 2] = encrypted body]
+ */
+ priv->outlen = 8;
+
+ if (bodylen > 0) {
+ if (mime_type != NULL) {
+ preludelen = rspamd_snprintf(repbuf, sizeof(repbuf), "%s\r\n"
+ "Content-Length: %z\r\n"
+ "Content-Type: %s\r\n"
+ "\r\n",
+ ENCRYPTED_VERSION, bodylen,
+ mime_type);
+ }
+ else {
+ preludelen = rspamd_snprintf(repbuf, sizeof(repbuf), "%s\r\n"
+ "Content-Length: %z\r\n"
+ ""
+ "\r\n",
+ ENCRYPTED_VERSION, bodylen);
+ }
+ }
+ else {
+ preludelen = rspamd_snprintf(repbuf, sizeof(repbuf),
+ "%s\r\n\r\n",
+ ENCRYPTED_VERSION);
+ }
+
+ enclen = rspamd_cryptobox_nonce_bytes(mode) +
+ rspamd_cryptobox_mac_bytes(mode) +
+ preludelen + /* version [content-length] + 2 * CRLF */
+ bodylen;
+ }
+
+ if (bodylen > 0) {
+ priv->outlen++;
+ }
+ }
+ else {
+ if (msg->method < HTTP_SYMBOLS) {
+ if (msg->body_buf.len == 0 || allow_shared) {
+ pbody = NULL;
+ bodylen = 0;
+ priv->outlen = 2;
+
+ if (msg->method == HTTP_INVALID) {
+ msg->method = HTTP_GET;
+ }
+ }
+ else {
+ pbody = (gchar *) msg->body_buf.begin;
+ bodylen = msg->body_buf.len;
+ priv->outlen = 3;
+
+ if (msg->method == HTTP_INVALID) {
+ msg->method = HTTP_POST;
+ }
+ }
+ }
+ else if (msg->body_buf.len > 0) {
+ allow_shared = FALSE;
+ pbody = (gchar *) msg->body_buf.begin;
+ bodylen = msg->body_buf.len;
+ priv->outlen = 2;
+ }
+ else {
+ /* Invalid body for spamc method */
+ abort();
+ }
+ }
+
+ peer_key = msg->peer_key;
+
+ priv->wr_total = bodylen + 2;
+
+ hdrcount = 0;
+
+ if (msg->method < HTTP_SYMBOLS) {
+ kh_foreach_value (msg->headers, hdr, {
+ DL_FOREACH (hdr, hcur) {
+ /* <name: value\r\n> */
+ priv->wr_total += hcur->combined->len;
+ enclen += hcur->combined->len;
+ priv->outlen ++;
+ hdrcount ++;
+ }
+});
+}
+
+/* Allocate iov */
+priv->out = g_malloc0(sizeof(struct iovec) * priv->outlen);
+priv->wr_pos = 0;
+
+meth_len = rspamd_http_message_write_header(mime_type, encrypted,
+ repbuf, sizeof(repbuf), bodylen, enclen,
+ host, conn, msg,
+ &buf, priv, peer_key);
+priv->wr_total += buf->len;
+
+/* Setup external request body */
+priv->out[0].iov_base = buf->str;
+priv->out[0].iov_len = buf->len;
+
+/* Buf will be used eventually for encryption */
+if (encrypted) {
+ gint meth_offset, nonce_offset, mac_offset;
+ mode = rspamd_keypair_alg(priv->local_key);
+
+ ottery_rand_bytes(nonce, rspamd_cryptobox_nonce_bytes(mode));
+ memset(mac, 0, rspamd_cryptobox_mac_bytes(mode));
+ meth_offset = buf->len;
+
+ if (conn->type == RSPAMD_HTTP_SERVER) {
+ buf = rspamd_fstring_append(buf, repbuf, meth_len);
+ }
+ else {
+ meth_len = strlen(http_method_str(msg->method)) + 1; /* + space */
+ buf = rspamd_fstring_append(buf, http_method_str(msg->method),
+ meth_len - 1);
+ buf = rspamd_fstring_append(buf, " ", 1);
+ }
+
+ nonce_offset = buf->len;
+ buf = rspamd_fstring_append(buf, nonce,
+ rspamd_cryptobox_nonce_bytes(mode));
+ mac_offset = buf->len;
+ buf = rspamd_fstring_append(buf, mac,
+ rspamd_cryptobox_mac_bytes(mode));
+
+ /* Need to be encrypted */
+ if (conn->type == RSPAMD_HTTP_SERVER) {
+ buf = rspamd_fstring_append(buf, "\r\n\r\n", 4);
+ }
+ else {
+ buf = rspamd_fstring_append(buf, repbuf, preludelen);
+ }
+
+ meth_pos = buf->str + meth_offset;
+ np = buf->str + nonce_offset;
+ mp = buf->str + mac_offset;
+}
+
+/* During previous writes, buf might be reallocated and changed */
+priv->buf->data = buf;
+
+if (encrypted) {
+ /* Finish external HTTP request */
+ priv->out[1].iov_base = "\r\n";
+ priv->out[1].iov_len = 2;
+ /* Encrypt the real request */
+ rspamd_http_connection_encrypt_message(conn, msg, priv, pbody, bodylen,
+ meth_pos, meth_len, preludelen, hdrcount, np, mp, peer_key);
+}
+else {
+ i = 1;
+ if (msg->method < HTTP_SYMBOLS) {
+ kh_foreach_value (msg->headers, hdr, {
+ DL_FOREACH (hdr, hcur) {
+ priv->out[i].iov_base = hcur->combined->str;
+ priv->out[i++].iov_len = hcur->combined->len;
+ }
+});
+
+priv->out[i].iov_base = "\r\n";
+priv->out[i++].iov_len = 2;
+}
+else
+{
+ /* No CRLF for compatibility reply */
+ priv->wr_total -= 2;
+}
+
+if (pbody != NULL) {
+ priv->out[i].iov_base = pbody;
+ priv->out[i++].iov_len = bodylen;
+}
+}
+
+priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_RESETED;
+
+if ((priv->flags & RSPAMD_HTTP_CONN_FLAG_PROXY) && (conn->opts & RSPAMD_HTTP_CLIENT_SSL)) {
+ /* We need to disable SSL flag! */
+ err = g_error_new(HTTP_ERROR, 400, "cannot use proxy for SSL connections");
+ rspamd_http_connection_ref(conn);
+ conn->error_handler(conn, err);
+ rspamd_http_connection_unref(conn);
+ g_error_free(err);
+ return FALSE;
+}
+
+rspamd_ev_watcher_stop(priv->ctx->event_loop, &priv->ev);
+
+if (conn->opts & RSPAMD_HTTP_CLIENT_SSL) {
+ gpointer ssl_ctx = (msg->flags & RSPAMD_HTTP_FLAG_SSL_NOVERIFY) ? priv->ctx->ssl_ctx_noverify : priv->ctx->ssl_ctx;
+
+ if (!ssl_ctx) {
+ err = g_error_new(HTTP_ERROR, 400, "ssl message requested "
+ "with no ssl ctx");
+ rspamd_http_connection_ref(conn);
+ conn->error_handler(conn, err);
+ rspamd_http_connection_unref(conn);
+ g_error_free(err);
+ return FALSE;
+ }
+ else {
+ if (!priv->ssl) {
+ priv->ssl = rspamd_ssl_connection_new(ssl_ctx, priv->ctx->event_loop,
+ !(msg->flags & RSPAMD_HTTP_FLAG_SSL_NOVERIFY),
+ conn->log_tag);
+ g_assert(priv->ssl != NULL);
+
+ if (!rspamd_ssl_connect_fd(priv->ssl, conn->fd, host, &priv->ev,
+ priv->timeout, rspamd_http_event_handler,
+ rspamd_http_ssl_err_handler, conn)) {
+
+ err = g_error_new(HTTP_ERROR, 400,
+ "ssl connection error: ssl error=%s, errno=%s",
+ ERR_error_string(ERR_get_error(), NULL),
+ strerror(errno));
+ rspamd_http_connection_ref(conn);
+ conn->error_handler(conn, err);
+ rspamd_http_connection_unref(conn);
+ g_error_free(err);
+ return FALSE;
+ }
+ }
+ else {
+ /* Just restore SSL handlers */
+ rspamd_ssl_connection_restore_handlers(priv->ssl,
+ rspamd_http_event_handler,
+ rspamd_http_ssl_err_handler,
+ conn,
+ EV_WRITE);
+ }
+ }
+}
+else {
+ rspamd_ev_watcher_init(&priv->ev, conn->fd, EV_WRITE,
+ rspamd_http_event_handler, conn);
+ rspamd_ev_watcher_start(priv->ctx->event_loop, &priv->ev, priv->timeout);
+}
+
+return TRUE;
+}
+
+gboolean
+rspamd_http_connection_write_message(struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg,
+ const gchar *host,
+ const gchar *mime_type,
+ gpointer ud,
+ ev_tstamp timeout)
+{
+ return rspamd_http_connection_write_message_common(conn, msg, host, mime_type,
+ ud, timeout, FALSE);
+}
+
+gboolean
+rspamd_http_connection_write_message_shared(struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg,
+ const gchar *host,
+ const gchar *mime_type,
+ gpointer ud,
+ ev_tstamp timeout)
+{
+ return rspamd_http_connection_write_message_common(conn, msg, host, mime_type,
+ ud, timeout, TRUE);
+}
+
+
+void rspamd_http_connection_set_max_size(struct rspamd_http_connection *conn,
+ gsize sz)
+{
+ conn->max_size = sz;
+}
+
+void rspamd_http_connection_set_key(struct rspamd_http_connection *conn,
+ struct rspamd_cryptobox_keypair *key)
+{
+ struct rspamd_http_connection_private *priv = conn->priv;
+
+ g_assert(key != NULL);
+ priv->local_key = rspamd_keypair_ref(key);
+}
+
+void rspamd_http_connection_own_socket(struct rspamd_http_connection *conn)
+{
+ struct rspamd_http_connection_private *priv = conn->priv;
+
+ priv->flags |= RSPAMD_HTTP_CONN_OWN_SOCKET;
+}
+
+const struct rspamd_cryptobox_pubkey *
+rspamd_http_connection_get_peer_key(struct rspamd_http_connection *conn)
+{
+ struct rspamd_http_connection_private *priv = conn->priv;
+
+ if (priv->peer_key) {
+ return priv->peer_key;
+ }
+ else if (priv->msg) {
+ return priv->msg->peer_key;
+ }
+
+ return NULL;
+}
+
+gboolean
+rspamd_http_connection_is_encrypted(struct rspamd_http_connection *conn)
+{
+ struct rspamd_http_connection_private *priv = conn->priv;
+
+ if (priv->peer_key != NULL) {
+ return TRUE;
+ }
+ else if (priv->msg) {
+ return priv->msg->peer_key != NULL;
+ }
+
+ return FALSE;
+}
+
+GHashTable *
+rspamd_http_message_parse_query(struct rspamd_http_message *msg)
+{
+ GHashTable *res;
+ rspamd_fstring_t *key = NULL, *value = NULL;
+ rspamd_ftok_t *key_tok = NULL, *value_tok = NULL;
+ const gchar *p, *c, *end;
+ struct http_parser_url u;
+ enum {
+ parse_key,
+ parse_eqsign,
+ parse_value,
+ parse_ampersand
+ } state = parse_key;
+
+ res = g_hash_table_new_full(rspamd_ftok_icase_hash,
+ rspamd_ftok_icase_equal,
+ rspamd_fstring_mapped_ftok_free,
+ rspamd_fstring_mapped_ftok_free);
+
+ if (msg->url && msg->url->len > 0) {
+ http_parser_parse_url(msg->url->str, msg->url->len, TRUE, &u);
+
+ if (u.field_set & (1 << UF_QUERY)) {
+ p = msg->url->str + u.field_data[UF_QUERY].off;
+ c = p;
+ end = p + u.field_data[UF_QUERY].len;
+
+ while (p <= end) {
+ switch (state) {
+ case parse_key:
+ if ((p == end || *p == '&') && p > c) {
+ /* We have a single parameter without a value */
+ key = rspamd_fstring_new_init(c, p - c);
+ key_tok = rspamd_ftok_map(key);
+ key_tok->len = rspamd_url_decode(key->str, key->str,
+ key->len);
+
+ value = rspamd_fstring_new_init("", 0);
+ value_tok = rspamd_ftok_map(value);
+
+ g_hash_table_replace(res, key_tok, value_tok);
+ state = parse_ampersand;
+ }
+ else if (*p == '=' && p > c) {
+ /* We have something like key=value */
+ key = rspamd_fstring_new_init(c, p - c);
+ key_tok = rspamd_ftok_map(key);
+ key_tok->len = rspamd_url_decode(key->str, key->str,
+ key->len);
+
+ state = parse_eqsign;
+ }
+ else {
+ p++;
+ }
+ break;
+
+ case parse_eqsign:
+ if (*p != '=') {
+ c = p;
+ state = parse_value;
+ }
+ else {
+ p++;
+ }
+ break;
+
+ case parse_value:
+ if ((p == end || *p == '&') && p >= c) {
+ g_assert(key != NULL);
+ if (p > c) {
+ value = rspamd_fstring_new_init(c, p - c);
+ value_tok = rspamd_ftok_map(value);
+ value_tok->len = rspamd_url_decode(value->str,
+ value->str,
+ value->len);
+ /* Detect quotes for value */
+ if (value_tok->begin[0] == '"') {
+ memmove(value->str, value->str + 1,
+ value_tok->len - 1);
+ value_tok->len--;
+ }
+ if (value_tok->begin[value_tok->len - 1] == '"') {
+ value_tok->len--;
+ }
+ }
+ else {
+ value = rspamd_fstring_new_init("", 0);
+ value_tok = rspamd_ftok_map(value);
+ }
+
+ g_hash_table_replace(res, key_tok, value_tok);
+ key = value = NULL;
+ key_tok = value_tok = NULL;
+ state = parse_ampersand;
+ }
+ else {
+ p++;
+ }
+ break;
+
+ case parse_ampersand:
+ if (p != end && *p != '&') {
+ c = p;
+ state = parse_key;
+ }
+ else {
+ p++;
+ }
+ break;
+ }
+ }
+ }
+
+ if (state != parse_ampersand && key != NULL) {
+ rspamd_fstring_free(key);
+ }
+ }
+
+ return res;
+}
+
+
+struct rspamd_http_message *
+rspamd_http_message_ref(struct rspamd_http_message *msg)
+{
+ REF_RETAIN(msg);
+
+ return msg;
+}
+
+void rspamd_http_message_unref(struct rspamd_http_message *msg)
+{
+ REF_RELEASE(msg);
+}
+
+void rspamd_http_connection_disable_encryption(struct rspamd_http_connection *conn)
+{
+ struct rspamd_http_connection_private *priv;
+
+ priv = conn->priv;
+
+ if (priv) {
+ if (priv->local_key) {
+ rspamd_keypair_unref(priv->local_key);
+ }
+ if (priv->peer_key) {
+ rspamd_pubkey_unref(priv->peer_key);
+ }
+
+ priv->local_key = NULL;
+ priv->peer_key = NULL;
+ priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_ENCRYPTED;
+ }
+} \ No newline at end of file
diff --git a/src/libserver/http/http_connection.h b/src/libserver/http/http_connection.h
new file mode 100644
index 0000000..e98d164
--- /dev/null
+++ b/src/libserver/http/http_connection.h
@@ -0,0 +1,320 @@
+/*-
+ * 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 HTTP_H_
+#define HTTP_H_
+
+/**
+ * @file http.h
+ *
+ * This is an interface for HTTP client and conn.
+ * This code uses HTTP parser written by Joyent Inc based on nginx code.
+ */
+
+#include "config.h"
+#include "http_context.h"
+#include "fstring.h"
+#include "ref.h"
+#include "http_message.h"
+#include "http_util.h"
+#include "addr.h"
+
+#include "contrib/libev/ev.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum rspamd_http_connection_type {
+ RSPAMD_HTTP_SERVER,
+ RSPAMD_HTTP_CLIENT
+};
+
+struct rspamd_http_header;
+struct rspamd_http_message;
+struct rspamd_http_connection_private;
+struct rspamd_http_connection;
+struct rspamd_http_connection_router;
+struct rspamd_http_connection_entry;
+struct rspamd_keepalive_hash_key;
+
+struct rspamd_storage_shmem {
+ gchar *shm_name;
+ ref_entry_t ref;
+};
+
+/**
+ * Legacy spamc protocol
+ */
+#define RSPAMD_HTTP_FLAG_SPAMC (1 << 0)
+/**
+ * Store body of the message in a shared memory segment
+ */
+#define RSPAMD_HTTP_FLAG_SHMEM (1 << 2)
+/**
+ * Store body of the message in an immutable shared memory segment
+ */
+#define RSPAMD_HTTP_FLAG_SHMEM_IMMUTABLE (1 << 3)
+/**
+ * Body has been set for a message
+ */
+#define RSPAMD_HTTP_FLAG_HAS_BODY (1 << 5)
+/**
+ * Do not verify server's certificate
+ */
+#define RSPAMD_HTTP_FLAG_SSL_NOVERIFY (1 << 6)
+/**
+ * Body has been set for a message
+ */
+#define RSPAMD_HTTP_FLAG_HAS_HOST_HEADER (1 << 7)
+/**
+ * Message is intended for SSL connection
+ */
+#define RSPAMD_HTTP_FLAG_WANT_SSL (1 << 8)
+/**
+ * Options for HTTP connection
+ */
+enum rspamd_http_options {
+ RSPAMD_HTTP_BODY_PARTIAL = 1, /**< Call body handler on all body data portions */
+ RSPAMD_HTTP_CLIENT_SIMPLE = 1u << 1, /**< Read HTTP client reply automatically */
+ RSPAMD_HTTP_CLIENT_ENCRYPTED = 1u << 2, /**< Encrypt data for client */
+ RSPAMD_HTTP_CLIENT_SHARED = 1u << 3, /**< Store reply in shared memory */
+ RSPAMD_HTTP_REQUIRE_ENCRYPTION = 1u << 4,
+ RSPAMD_HTTP_CLIENT_KEEP_ALIVE = 1u << 5,
+ RSPAMD_HTTP_CLIENT_SSL = 1u << 6u,
+};
+
+typedef int (*rspamd_http_body_handler_t)(struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg,
+ const gchar *chunk,
+ gsize len);
+
+typedef void (*rspamd_http_error_handler_t)(struct rspamd_http_connection *conn,
+ GError *err);
+
+typedef int (*rspamd_http_finish_handler_t)(struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg);
+
+/**
+ * HTTP connection structure
+ */
+struct rspamd_http_connection {
+ struct rspamd_http_connection_private *priv;
+ rspamd_http_body_handler_t body_handler;
+ rspamd_http_error_handler_t error_handler;
+ rspamd_http_finish_handler_t finish_handler;
+ gpointer ud;
+ const gchar *log_tag;
+ /* Used for keepalive */
+ struct rspamd_keepalive_hash_key *keepalive_hash_key;
+ gsize max_size;
+ unsigned opts;
+ enum rspamd_http_connection_type type;
+ gboolean finished;
+ gint fd;
+ gint ref;
+};
+
+/**
+ * Creates a new HTTP server connection from an opened FD returned by accept function
+ * @param ctx
+ * @param fd
+ * @param body_handler
+ * @param error_handler
+ * @param finish_handler
+ * @param opts
+ * @return
+ */
+struct rspamd_http_connection *rspamd_http_connection_new_server(
+ struct rspamd_http_context *ctx,
+ gint fd,
+ rspamd_http_body_handler_t body_handler,
+ rspamd_http_error_handler_t error_handler,
+ rspamd_http_finish_handler_t finish_handler,
+ unsigned opts);
+
+/**
+ * Creates or reuses a new keepalive client connection identified by hostname and inet_addr
+ * @param ctx
+ * @param body_handler
+ * @param error_handler
+ * @param finish_handler
+ * @param addr
+ * @param host
+ * @return
+ */
+struct rspamd_http_connection *rspamd_http_connection_new_client_keepalive(
+ struct rspamd_http_context *ctx,
+ rspamd_http_body_handler_t body_handler,
+ rspamd_http_error_handler_t error_handler,
+ rspamd_http_finish_handler_t finish_handler,
+ unsigned opts,
+ rspamd_inet_addr_t *addr,
+ const gchar *host);
+
+/**
+ * Creates an ordinary connection using the address specified (if proxy is not set)
+ * @param ctx
+ * @param body_handler
+ * @param error_handler
+ * @param finish_handler
+ * @param opts
+ * @param addr
+ * @return
+ */
+struct rspamd_http_connection *rspamd_http_connection_new_client(
+ struct rspamd_http_context *ctx,
+ rspamd_http_body_handler_t body_handler,
+ rspamd_http_error_handler_t error_handler,
+ rspamd_http_finish_handler_t finish_handler,
+ unsigned opts,
+ rspamd_inet_addr_t *addr);
+
+/**
+ * Creates an ordinary client connection using ready file descriptor (ignores proxy)
+ * @param ctx
+ * @param body_handler
+ * @param error_handler
+ * @param finish_handler
+ * @param opts
+ * @param addr
+ * @return
+ */
+struct rspamd_http_connection *rspamd_http_connection_new_client_socket(
+ struct rspamd_http_context *ctx,
+ rspamd_http_body_handler_t body_handler,
+ rspamd_http_error_handler_t error_handler,
+ rspamd_http_finish_handler_t finish_handler,
+ unsigned opts,
+ gint fd);
+
+/**
+ * Set key pointed by an opaque pointer
+ * @param conn connection structure
+ * @param key opaque key structure
+ */
+void rspamd_http_connection_set_key(struct rspamd_http_connection *conn,
+ struct rspamd_cryptobox_keypair *key);
+
+/**
+ * Transfer ownership on socket to an HTTP connection
+ * @param conn
+ */
+void rspamd_http_connection_own_socket(struct rspamd_http_connection *conn);
+
+/**
+ * Get peer's public key
+ * @param conn connection structure
+ * @return pubkey structure or NULL
+ */
+const struct rspamd_cryptobox_pubkey *rspamd_http_connection_get_peer_key(
+ struct rspamd_http_connection *conn);
+
+/**
+ * Returns TRUE if a connection is encrypted
+ * @param conn
+ * @return
+ */
+gboolean rspamd_http_connection_is_encrypted(struct rspamd_http_connection *conn);
+
+/**
+ * Handle a request using socket fd and user data ud
+ * @param conn connection structure
+ * @param ud opaque user data
+ * @param fd fd to read/write
+ */
+void rspamd_http_connection_read_message(
+ struct rspamd_http_connection *conn,
+ gpointer ud,
+ ev_tstamp timeout);
+
+void rspamd_http_connection_read_message_shared(
+ struct rspamd_http_connection *conn,
+ gpointer ud,
+ ev_tstamp timeout);
+
+/**
+ * Send reply using initialised connection
+ * @param conn connection structure
+ * @param msg HTTP message
+ * @param ud opaque user data
+ * @param fd fd to read/write
+ */
+gboolean rspamd_http_connection_write_message(
+ struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg,
+ const gchar *host,
+ const gchar *mime_type,
+ gpointer ud,
+ ev_tstamp timeout);
+
+gboolean rspamd_http_connection_write_message_shared(
+ struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg,
+ const gchar *host,
+ const gchar *mime_type,
+ gpointer ud,
+ ev_tstamp timeout);
+
+/**
+ * Free connection structure
+ * @param conn
+ */
+void rspamd_http_connection_free(struct rspamd_http_connection *conn);
+
+/**
+ * Increase refcount for a connection
+ * @param conn
+ * @return
+ */
+static inline struct rspamd_http_connection *
+rspamd_http_connection_ref(struct rspamd_http_connection *conn)
+{
+ conn->ref++;
+ return conn;
+}
+
+/**
+ * Decrease a refcount for a connection and free it if refcount is equal to zero
+ * @param conn
+ */
+static void
+rspamd_http_connection_unref(struct rspamd_http_connection *conn)
+{
+ if (--conn->ref <= 0) {
+ rspamd_http_connection_free(conn);
+ }
+}
+
+/**
+ * Reset connection for a new request
+ * @param conn
+ */
+void rspamd_http_connection_reset(struct rspamd_http_connection *conn);
+
+/**
+ * Sets global maximum size for HTTP message being processed
+ * @param sz
+ */
+void rspamd_http_connection_set_max_size(struct rspamd_http_connection *conn,
+ gsize sz);
+
+void rspamd_http_connection_disable_encryption(struct rspamd_http_connection *conn);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HTTP_H_ */
diff --git a/src/libserver/http/http_context.c b/src/libserver/http/http_context.c
new file mode 100644
index 0000000..f08e33b
--- /dev/null
+++ b/src/libserver/http/http_context.c
@@ -0,0 +1,670 @@
+/*
+ * 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 "http_context.h"
+#include "http_private.h"
+#include "keypair.h"
+#include "keypairs_cache.h"
+#include "cfg_file.h"
+#include "contrib/libottery/ottery.h"
+#include "contrib/http-parser/http_parser.h"
+#include "ssl_util.h"
+#include "rspamd.h"
+#include "libev_helper.h"
+
+INIT_LOG_MODULE(http_context)
+
+#define msg_debug_http_context(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_http_context_log_id, "http_context", NULL, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+
+static struct rspamd_http_context *default_ctx = NULL;
+
+struct rspamd_http_keepalive_cbdata {
+ struct rspamd_http_connection *conn;
+ struct rspamd_http_context *ctx;
+ GQueue *queue;
+ GList *link;
+ struct rspamd_io_ev ev;
+};
+
+static void
+rspamd_http_keepalive_queue_cleanup(GQueue *conns)
+{
+ GList *cur;
+
+ cur = conns->head;
+
+ while (cur) {
+ struct rspamd_http_keepalive_cbdata *cbd;
+
+ cbd = (struct rspamd_http_keepalive_cbdata *) cur->data;
+ /* unref call closes fd, so we need to remove ev watcher first! */
+ rspamd_ev_watcher_stop(cbd->ctx->event_loop, &cbd->ev);
+ rspamd_http_connection_unref(cbd->conn);
+ g_free(cbd);
+
+ cur = cur->next;
+ }
+
+ g_queue_clear(conns);
+}
+
+static void
+rspamd_http_context_client_rotate_ev(struct ev_loop *loop, ev_timer *w, int revents)
+{
+ struct rspamd_http_context *ctx = (struct rspamd_http_context *) w->data;
+ gpointer kp;
+
+ w->repeat = rspamd_time_jitter(ctx->config.client_key_rotate_time, 0);
+ msg_debug_http_context("rotate local keypair, next rotate in %.0f seconds",
+ w->repeat);
+
+ ev_timer_again(loop, w);
+
+ kp = ctx->client_kp;
+ ctx->client_kp = rspamd_keypair_new(RSPAMD_KEYPAIR_KEX,
+ RSPAMD_CRYPTOBOX_MODE_25519);
+ rspamd_keypair_unref(kp);
+}
+
+static struct rspamd_http_context *
+rspamd_http_context_new_default(struct rspamd_config *cfg,
+ struct ev_loop *ev_base,
+ struct upstream_ctx *ups_ctx)
+{
+ struct rspamd_http_context *ctx;
+
+ static const int default_kp_size = 1024;
+ static const gdouble default_rotate_time = 120;
+ static const gdouble default_keepalive_interval = 65;
+ static const gchar *default_user_agent = "rspamd-" RSPAMD_VERSION_FULL;
+ static const gchar *default_server_hdr = "rspamd/" RSPAMD_VERSION_FULL;
+
+ ctx = g_malloc0(sizeof(*ctx));
+ ctx->config.kp_cache_size_client = default_kp_size;
+ ctx->config.kp_cache_size_server = default_kp_size;
+ ctx->config.client_key_rotate_time = default_rotate_time;
+ ctx->config.user_agent = default_user_agent;
+ ctx->config.keepalive_interval = default_keepalive_interval;
+ ctx->config.server_hdr = default_server_hdr;
+ ctx->ups_ctx = ups_ctx;
+
+ if (cfg) {
+ ctx->ssl_ctx = cfg->libs_ctx->ssl_ctx;
+ ctx->ssl_ctx_noverify = cfg->libs_ctx->ssl_ctx_noverify;
+ }
+ else {
+ ctx->ssl_ctx = rspamd_init_ssl_ctx();
+ ctx->ssl_ctx_noverify = rspamd_init_ssl_ctx_noverify();
+ }
+
+ ctx->event_loop = ev_base;
+
+ ctx->keep_alive_hash = kh_init(rspamd_keep_alive_hash);
+
+ return ctx;
+}
+
+static void
+rspamd_http_context_parse_proxy(struct rspamd_http_context *ctx,
+ const gchar *name,
+ struct upstream_list **pls)
+{
+ struct http_parser_url u;
+ struct upstream_list *uls;
+
+ if (!ctx->ups_ctx) {
+ msg_err("cannot parse http_proxy %s - upstreams context is undefined", name);
+ return;
+ }
+
+ memset(&u, 0, sizeof(u));
+
+ if (http_parser_parse_url(name, strlen(name), 1, &u) == 0) {
+ if (!(u.field_set & (1u << UF_HOST)) || u.port == 0) {
+ msg_err("cannot parse http(s) proxy %s - invalid host or port", name);
+
+ return;
+ }
+
+ uls = rspamd_upstreams_create(ctx->ups_ctx);
+
+ if (!rspamd_upstreams_parse_line_len(uls,
+ name + u.field_data[UF_HOST].off,
+ u.field_data[UF_HOST].len, u.port, NULL)) {
+ msg_err("cannot parse http(s) proxy %s - invalid data", name);
+
+ rspamd_upstreams_destroy(uls);
+ }
+ else {
+ *pls = uls;
+ msg_info("set http(s) proxy to %s", name);
+ }
+ }
+ else {
+ uls = rspamd_upstreams_create(ctx->ups_ctx);
+
+ if (!rspamd_upstreams_parse_line(uls,
+ name, 3128, NULL)) {
+ msg_err("cannot parse http(s) proxy %s - invalid data", name);
+
+ rspamd_upstreams_destroy(uls);
+ }
+ else {
+ *pls = uls;
+ msg_info("set http(s) proxy to %s", name);
+ }
+ }
+}
+
+static void
+rspamd_http_context_init(struct rspamd_http_context *ctx)
+{
+ if (ctx->config.kp_cache_size_client > 0) {
+ ctx->client_kp_cache = rspamd_keypair_cache_new(ctx->config.kp_cache_size_client);
+ }
+
+ if (ctx->config.kp_cache_size_server > 0) {
+ ctx->server_kp_cache = rspamd_keypair_cache_new(ctx->config.kp_cache_size_server);
+ }
+
+ if (ctx->config.client_key_rotate_time > 0 && ctx->event_loop) {
+ double jittered = rspamd_time_jitter(ctx->config.client_key_rotate_time,
+ 0);
+
+ ev_timer_init(&ctx->client_rotate_ev,
+ rspamd_http_context_client_rotate_ev, jittered, 0);
+ ev_timer_start(ctx->event_loop, &ctx->client_rotate_ev);
+ ctx->client_rotate_ev.data = ctx;
+ }
+
+ if (ctx->config.http_proxy) {
+ rspamd_http_context_parse_proxy(ctx, ctx->config.http_proxy,
+ &ctx->http_proxies);
+ }
+
+ default_ctx = ctx;
+}
+
+struct rspamd_http_context *
+rspamd_http_context_create(struct rspamd_config *cfg,
+ struct ev_loop *ev_base,
+ struct upstream_ctx *ups_ctx)
+{
+ struct rspamd_http_context *ctx;
+ const ucl_object_t *http_obj;
+
+ ctx = rspamd_http_context_new_default(cfg, ev_base, ups_ctx);
+ http_obj = ucl_object_lookup(cfg->cfg_ucl_obj, "http");
+
+ if (http_obj) {
+ const ucl_object_t *server_obj, *client_obj;
+
+ client_obj = ucl_object_lookup(http_obj, "client");
+
+ if (client_obj) {
+ const ucl_object_t *kp_size;
+
+ kp_size = ucl_object_lookup(client_obj, "cache_size");
+
+ if (kp_size) {
+ ctx->config.kp_cache_size_client = ucl_object_toint(kp_size);
+ }
+
+ const ucl_object_t *rotate_time;
+
+ rotate_time = ucl_object_lookup(client_obj, "rotate_time");
+
+ if (rotate_time) {
+ ctx->config.client_key_rotate_time = ucl_object_todouble(rotate_time);
+ }
+
+ const ucl_object_t *user_agent;
+
+ user_agent = ucl_object_lookup(client_obj, "user_agent");
+
+ if (user_agent) {
+ ctx->config.user_agent = ucl_object_tostring(user_agent);
+
+ if (ctx->config.user_agent && strlen(ctx->config.user_agent) == 0) {
+ ctx->config.user_agent = NULL;
+ }
+ }
+
+ const ucl_object_t *server_hdr;
+ server_hdr = ucl_object_lookup(client_obj, "server_hdr");
+
+ if (server_hdr) {
+ ctx->config.server_hdr = ucl_object_tostring(server_hdr);
+
+ if (ctx->config.server_hdr && strlen(ctx->config.server_hdr) == 0) {
+ ctx->config.server_hdr = "";
+ }
+ }
+
+ const ucl_object_t *keepalive_interval;
+
+ keepalive_interval = ucl_object_lookup(client_obj, "keepalive_interval");
+
+ if (keepalive_interval) {
+ ctx->config.keepalive_interval = ucl_object_todouble(keepalive_interval);
+ }
+
+ const ucl_object_t *http_proxy;
+ http_proxy = ucl_object_lookup(client_obj, "http_proxy");
+
+ if (http_proxy) {
+ ctx->config.http_proxy = ucl_object_tostring(http_proxy);
+ }
+ }
+
+ server_obj = ucl_object_lookup(http_obj, "server");
+
+ if (server_obj) {
+ const ucl_object_t *kp_size;
+
+ kp_size = ucl_object_lookup(server_obj, "cache_size");
+
+ if (kp_size) {
+ ctx->config.kp_cache_size_server = ucl_object_toint(kp_size);
+ }
+ }
+ }
+
+ rspamd_http_context_init(ctx);
+
+ return ctx;
+}
+
+
+void rspamd_http_context_free(struct rspamd_http_context *ctx)
+{
+ if (ctx == default_ctx) {
+ default_ctx = NULL;
+ }
+
+ if (ctx->client_kp_cache) {
+ rspamd_keypair_cache_destroy(ctx->client_kp_cache);
+ }
+
+ if (ctx->server_kp_cache) {
+ rspamd_keypair_cache_destroy(ctx->server_kp_cache);
+ }
+
+ if (ctx->config.client_key_rotate_time > 0) {
+ ev_timer_stop(ctx->event_loop, &ctx->client_rotate_ev);
+
+ if (ctx->client_kp) {
+ rspamd_keypair_unref(ctx->client_kp);
+ }
+ }
+
+ struct rspamd_keepalive_hash_key *hk;
+
+ kh_foreach_key(ctx->keep_alive_hash, hk, {
+ msg_debug_http_context("cleanup keepalive elt %s (%s)",
+ rspamd_inet_address_to_string_pretty(hk->addr),
+ hk->host);
+
+ if (hk->host) {
+ g_free(hk->host);
+ }
+
+ rspamd_inet_address_free(hk->addr);
+ rspamd_http_keepalive_queue_cleanup(&hk->conns);
+ g_free(hk);
+ });
+
+ kh_destroy(rspamd_keep_alive_hash, ctx->keep_alive_hash);
+
+ if (ctx->http_proxies) {
+ rspamd_upstreams_destroy(ctx->http_proxies);
+ }
+
+ g_free(ctx);
+}
+
+struct rspamd_http_context *
+rspamd_http_context_create_config(struct rspamd_http_context_cfg *cfg,
+ struct ev_loop *ev_base,
+ struct upstream_ctx *ups_ctx)
+{
+ struct rspamd_http_context *ctx;
+
+ ctx = rspamd_http_context_new_default(NULL, ev_base, ups_ctx);
+ memcpy(&ctx->config, cfg, sizeof(*cfg));
+ rspamd_http_context_init(ctx);
+
+ return ctx;
+}
+
+struct rspamd_http_context *
+rspamd_http_context_default(void)
+{
+ g_assert(default_ctx != NULL);
+
+ return default_ctx;
+}
+
+gint32
+rspamd_keep_alive_key_hash(struct rspamd_keepalive_hash_key *k)
+{
+ rspamd_cryptobox_fast_hash_state_t hst;
+
+ rspamd_cryptobox_fast_hash_init(&hst, 0);
+
+ if (k->host) {
+ rspamd_cryptobox_fast_hash_update(&hst, k->host, strlen(k->host));
+ }
+
+ rspamd_cryptobox_fast_hash_update(&hst, &k->port, sizeof(k->port));
+ rspamd_cryptobox_fast_hash_update(&hst, &k->is_ssl, sizeof(k->is_ssl));
+
+ return rspamd_cryptobox_fast_hash_final(&hst);
+}
+
+bool rspamd_keep_alive_key_equal(struct rspamd_keepalive_hash_key *k1,
+ struct rspamd_keepalive_hash_key *k2)
+{
+ if (k1->is_ssl != k2->is_ssl) {
+ return false;
+ }
+
+ if (k1->host && k2->host) {
+ if (k1->port == k2->port) {
+ return strcmp(k1->host, k2->host) == 0;
+ }
+ }
+ else if (!k1->host && !k2->host) {
+ return (k1->port == k2->port);
+ }
+
+ /* One has host and another has no host */
+ return false;
+}
+
+struct rspamd_http_connection *
+rspamd_http_context_check_keepalive(struct rspamd_http_context *ctx,
+ const rspamd_inet_addr_t *addr,
+ const gchar *host,
+ bool is_ssl)
+{
+ struct rspamd_keepalive_hash_key hk, *phk;
+ khiter_t k;
+
+ if (ctx == NULL) {
+ ctx = rspamd_http_context_default();
+ }
+
+ hk.addr = (rspamd_inet_addr_t *) addr;
+ hk.host = (gchar *) host;
+ hk.port = rspamd_inet_address_get_port(addr);
+ hk.is_ssl = is_ssl;
+
+ k = kh_get(rspamd_keep_alive_hash, ctx->keep_alive_hash, &hk);
+
+ if (k != kh_end(ctx->keep_alive_hash)) {
+ phk = kh_key(ctx->keep_alive_hash, k);
+ GQueue *conns = &phk->conns;
+
+ /* Use stack based approach */
+
+ if (g_queue_get_length(conns) > 0) {
+ struct rspamd_http_keepalive_cbdata *cbd;
+ struct rspamd_http_connection *conn;
+ gint err;
+ socklen_t len = sizeof(gint);
+
+ cbd = g_queue_pop_head(conns);
+ rspamd_ev_watcher_stop(ctx->event_loop, &cbd->ev);
+ conn = cbd->conn;
+ g_free(cbd);
+
+ if (getsockopt(conn->fd, SOL_SOCKET, SO_ERROR, (void *) &err, &len) == -1) {
+ err = errno;
+ }
+
+ if (err != 0) {
+ rspamd_http_connection_unref(conn);
+
+ msg_debug_http_context("invalid reused keepalive element %s (%s, ssl=%d); "
+ "%s error; "
+ "%d connections queued",
+ rspamd_inet_address_to_string_pretty(phk->addr),
+ phk->host,
+ (int) phk->is_ssl,
+ g_strerror(err),
+ conns->length);
+
+ return NULL;
+ }
+
+ msg_debug_http_context("reused keepalive element %s (%s, ssl=%d), %d connections queued",
+ rspamd_inet_address_to_string_pretty(phk->addr),
+ phk->host,
+ (int) phk->is_ssl,
+ conns->length);
+
+ /* We transfer refcount here! */
+ return conn;
+ }
+ else {
+ msg_debug_http_context("found empty keepalive element %s (%s), cannot reuse",
+ rspamd_inet_address_to_string_pretty(phk->addr),
+ phk->host);
+ }
+ }
+
+ return NULL;
+}
+
+const rspamd_inet_addr_t *
+rspamd_http_context_has_keepalive(struct rspamd_http_context *ctx,
+ const gchar *host,
+ unsigned port,
+ bool is_ssl)
+{
+ struct rspamd_keepalive_hash_key hk, *phk;
+ khiter_t k;
+
+ if (ctx == NULL) {
+ ctx = rspamd_http_context_default();
+ }
+
+ hk.host = (gchar *) host;
+ hk.port = port;
+ hk.is_ssl = is_ssl;
+
+ k = kh_get(rspamd_keep_alive_hash, ctx->keep_alive_hash, &hk);
+
+ if (k != kh_end(ctx->keep_alive_hash)) {
+ phk = kh_key(ctx->keep_alive_hash, k);
+ GQueue *conns = &phk->conns;
+
+ if (g_queue_get_length(conns) > 0) {
+ return phk->addr;
+ }
+ }
+
+ return NULL;
+}
+
+void rspamd_http_context_prepare_keepalive(struct rspamd_http_context *ctx,
+ struct rspamd_http_connection *conn,
+ const rspamd_inet_addr_t *addr,
+ const gchar *host,
+ bool is_ssl)
+{
+ struct rspamd_keepalive_hash_key hk, *phk;
+ khiter_t k;
+
+ hk.addr = (rspamd_inet_addr_t *) addr;
+ hk.host = (gchar *) host;
+ hk.is_ssl = is_ssl;
+ hk.port = rspamd_inet_address_get_port(addr);
+
+ k = kh_get(rspamd_keep_alive_hash, ctx->keep_alive_hash, &hk);
+
+ if (k != kh_end(ctx->keep_alive_hash)) {
+ /* Reuse existing */
+ conn->keepalive_hash_key = kh_key(ctx->keep_alive_hash, k);
+ msg_debug_http_context("use existing keepalive element %s (%s)",
+ rspamd_inet_address_to_string_pretty(conn->keepalive_hash_key->addr),
+ conn->keepalive_hash_key->host);
+ }
+ else {
+ /* Create new one */
+ GQueue empty_init = G_QUEUE_INIT;
+ gint r;
+
+ phk = g_malloc(sizeof(*phk));
+ phk->conns = empty_init;
+ phk->host = g_strdup(host);
+ phk->is_ssl = is_ssl;
+ phk->addr = rspamd_inet_address_copy(addr, NULL);
+ phk->port = hk.port;
+
+
+ kh_put(rspamd_keep_alive_hash, ctx->keep_alive_hash, phk, &r);
+ conn->keepalive_hash_key = phk;
+
+ msg_debug_http_context("create new keepalive element %s (%s)",
+ rspamd_inet_address_to_string_pretty(conn->keepalive_hash_key->addr),
+ conn->keepalive_hash_key->host);
+ }
+}
+
+static void
+rspamd_http_keepalive_handler(gint fd, short what, gpointer ud)
+{
+ struct rspamd_http_keepalive_cbdata *cbdata =
+ (struct rspamd_http_keepalive_cbdata *) ud; /*
+ * We can get here if a remote side reported something or it has
+ * timed out. In both cases we just terminate keepalive connection.
+ */
+
+ g_queue_delete_link(cbdata->queue, cbdata->link);
+ msg_debug_http_context("remove keepalive element %s (%s), %d connections left",
+ rspamd_inet_address_to_string_pretty(cbdata->conn->keepalive_hash_key->addr),
+ cbdata->conn->keepalive_hash_key->host,
+ cbdata->queue->length);
+ /* unref call closes fd, so we need to remove ev watcher first! */
+ rspamd_ev_watcher_stop(cbdata->ctx->event_loop, &cbdata->ev);
+ rspamd_http_connection_unref(cbdata->conn);
+ g_free(cbdata);
+}
+
+/* Non-static for unit testing */
+long rspamd_http_parse_keepalive_timeout(const rspamd_ftok_t *tok)
+{
+ long timeout = -1;
+ goffset pos = rspamd_substring_search(tok->begin,
+ tok->len, "timeout", sizeof("timeout") - 1);
+
+ if (pos != -1) {
+ pos += sizeof("timeout") - 1;
+
+ /* Skip spaces and equal sign */
+ while (pos < tok->len) {
+ if (tok->begin[pos] != '=' && !g_ascii_isspace(tok->begin[pos])) {
+ break;
+ }
+ pos++;
+ }
+
+ gsize ndigits = rspamd_memspn(tok->begin + pos, "0123456789", tok->len - pos);
+ glong real_timeout;
+
+ if (ndigits > 0) {
+ if (rspamd_strtoul(tok->begin + pos, ndigits, &real_timeout)) {
+ timeout = real_timeout;
+ msg_debug_http_context("got timeout attr %l", timeout);
+ }
+ }
+ }
+
+ return timeout;
+}
+
+void rspamd_http_context_push_keepalive(struct rspamd_http_context *ctx,
+ struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg,
+ struct ev_loop *event_loop)
+{
+ struct rspamd_http_keepalive_cbdata *cbdata;
+ gdouble timeout = ctx->config.keepalive_interval;
+
+ g_assert(conn->keepalive_hash_key != NULL);
+
+ if (msg) {
+ const rspamd_ftok_t *tok;
+ rspamd_ftok_t cmp;
+
+ tok = rspamd_http_message_find_header(msg, "Connection");
+
+ if (!tok) {
+ /* Server has not stated that it can do keep alive */
+ conn->finished = TRUE;
+ msg_debug_http_context("no Connection header");
+ return;
+ }
+
+ RSPAMD_FTOK_ASSIGN(&cmp, "keep-alive");
+
+ if (rspamd_ftok_casecmp(&cmp, tok) != 0) {
+ conn->finished = TRUE;
+ msg_debug_http_context("connection header is not `keep-alive`");
+ return;
+ }
+
+ /* We can proceed, check timeout */
+
+ tok = rspamd_http_message_find_header(msg, "Keep-Alive");
+
+ if (tok) {
+ long maybe_timeout = rspamd_http_parse_keepalive_timeout(tok);
+
+ if (maybe_timeout > 0) {
+ timeout = maybe_timeout;
+ }
+ }
+ }
+
+ /* Move connection to the keepalive pool */
+ cbdata = g_malloc0(sizeof(*cbdata));
+
+ cbdata->conn = rspamd_http_connection_ref(conn);
+ /* Use stack like approach to that would easy reading */
+ g_queue_push_head(&conn->keepalive_hash_key->conns, cbdata);
+ cbdata->link = conn->keepalive_hash_key->conns.head;
+
+ cbdata->queue = &conn->keepalive_hash_key->conns;
+ cbdata->ctx = ctx;
+ conn->finished = FALSE;
+
+ rspamd_ev_watcher_init(&cbdata->ev, conn->fd, EV_READ,
+ rspamd_http_keepalive_handler,
+ cbdata);
+ rspamd_ev_watcher_start(event_loop, &cbdata->ev, timeout);
+
+ msg_debug_http_context("push keepalive element %s (%s), %d connections queued, %.1f timeout",
+ rspamd_inet_address_to_string_pretty(cbdata->conn->keepalive_hash_key->addr),
+ cbdata->conn->keepalive_hash_key->host,
+ cbdata->queue->length,
+ timeout);
+} \ No newline at end of file
diff --git a/src/libserver/http/http_context.h b/src/libserver/http/http_context.h
new file mode 100644
index 0000000..f3622ae
--- /dev/null
+++ b/src/libserver/http/http_context.h
@@ -0,0 +1,122 @@
+/*-
+ * Copyright 2019 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_HTTP_CONTEXT_H
+#define RSPAMD_HTTP_CONTEXT_H
+
+#include "config.h"
+#include "ucl.h"
+#include "addr.h"
+
+#include "contrib/libev/ev.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_http_context;
+struct rspamd_config;
+struct rspamd_http_message;
+struct upstream_ctx;
+
+struct rspamd_http_context_cfg {
+ guint kp_cache_size_client;
+ guint kp_cache_size_server;
+ guint ssl_cache_size;
+ gdouble keepalive_interval;
+ gdouble client_key_rotate_time;
+ const gchar *user_agent;
+ const gchar *http_proxy;
+ const gchar *server_hdr;
+};
+
+/**
+ * Creates and configures new HTTP context
+ * @param root_conf configuration object
+ * @param ev_base event base
+ * @return new context used for both client and server HTTP connections
+ */
+struct rspamd_http_context *rspamd_http_context_create(struct rspamd_config *cfg,
+ struct ev_loop *ev_base,
+ struct upstream_ctx *ctx);
+
+struct rspamd_http_context *rspamd_http_context_create_config(
+ struct rspamd_http_context_cfg *cfg,
+ struct ev_loop *ev_base,
+ struct upstream_ctx *ctx);
+
+/**
+ * Destroys context
+ * @param ctx
+ */
+void rspamd_http_context_free(struct rspamd_http_context *ctx);
+
+struct rspamd_http_context *rspamd_http_context_default(void);
+
+/**
+ * Returns preserved keepalive connection if it's available.
+ * Refcount is transferred to caller!
+ * @param ctx
+ * @param addr
+ * @param host
+ * @return
+ */
+struct rspamd_http_connection *rspamd_http_context_check_keepalive(struct rspamd_http_context *ctx,
+ const rspamd_inet_addr_t *addr,
+ const gchar *host,
+ bool is_ssl);
+
+/**
+ * Checks if there is a valid keepalive connection
+ * @param ctx
+ * @param addr
+ * @param host
+ * @param is_ssl
+ * @return
+ */
+const rspamd_inet_addr_t *rspamd_http_context_has_keepalive(struct rspamd_http_context *ctx,
+ const gchar *host,
+ unsigned port,
+ bool is_ssl);
+
+/**
+ * Prepares keepalive key for a connection by creating a new entry or by reusing existent
+ * Bear in mind, that keepalive pool has currently no cleanup methods!
+ * @param ctx
+ * @param conn
+ * @param addr
+ * @param host
+ */
+void rspamd_http_context_prepare_keepalive(struct rspamd_http_context *ctx, struct rspamd_http_connection *conn,
+ const rspamd_inet_addr_t *addr, const gchar *host, bool is_ssl);
+
+/**
+ * Pushes a connection to keepalive pool after client request is finished,
+ * keepalive key *must* be prepared before using of this function
+ * @param ctx
+ * @param conn
+ * @param msg
+ */
+void rspamd_http_context_push_keepalive(struct rspamd_http_context *ctx,
+ struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg,
+ struct ev_loop *ev_base);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/http/http_message.c b/src/libserver/http/http_message.c
new file mode 100644
index 0000000..670122d
--- /dev/null
+++ b/src/libserver/http/http_message.c
@@ -0,0 +1,725 @@
+/*-
+ * Copyright 2019 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 "http_message.h"
+#include "http_connection.h"
+#include "http_private.h"
+#include "libutil/printf.h"
+#include "libserver/logger.h"
+#include "utlist.h"
+#include "unix-std.h"
+
+struct rspamd_http_message *
+rspamd_http_new_message(enum rspamd_http_message_type type)
+{
+ struct rspamd_http_message *new;
+
+ new = g_malloc0(sizeof(struct rspamd_http_message));
+
+ if (type == HTTP_REQUEST) {
+ new->url = rspamd_fstring_new();
+ }
+ else {
+ new->url = NULL;
+ new->code = 200;
+ }
+
+ new->port = 80;
+ new->type = type;
+ new->method = HTTP_INVALID;
+ new->headers = kh_init(rspamd_http_headers_hash);
+
+ REF_INIT_RETAIN(new, rspamd_http_message_free);
+
+ return new;
+}
+
+struct rspamd_http_message *
+rspamd_http_message_from_url(const gchar *url)
+{
+ struct http_parser_url pu;
+ struct rspamd_http_message *msg;
+ const gchar *host, *path;
+ size_t pathlen, urllen;
+ guint flags = 0;
+
+ if (url == NULL) {
+ return NULL;
+ }
+
+ urllen = strlen(url);
+ memset(&pu, 0, sizeof(pu));
+
+ if (http_parser_parse_url(url, urllen, FALSE, &pu) != 0) {
+ msg_warn("cannot parse URL: %s", url);
+ return NULL;
+ }
+
+ if ((pu.field_set & (1 << UF_HOST)) == 0) {
+ msg_warn("no host argument in URL: %s", url);
+ return NULL;
+ }
+
+ if ((pu.field_set & (1 << UF_SCHEMA))) {
+ if (pu.field_data[UF_SCHEMA].len == sizeof("https") - 1 &&
+ memcmp(url + pu.field_data[UF_SCHEMA].off, "https", 5) == 0) {
+ flags |= RSPAMD_HTTP_FLAG_WANT_SSL;
+ }
+ }
+
+ if ((pu.field_set & (1 << UF_PATH)) == 0) {
+ path = "/";
+ pathlen = 1;
+ }
+ else {
+ path = url + pu.field_data[UF_PATH].off;
+ pathlen = urllen - pu.field_data[UF_PATH].off;
+ }
+
+ msg = rspamd_http_new_message(HTTP_REQUEST);
+ host = url + pu.field_data[UF_HOST].off;
+ msg->flags = flags;
+
+ if ((pu.field_set & (1 << UF_PORT)) != 0) {
+ msg->port = pu.port;
+ }
+ else {
+ /* XXX: magic constant */
+ if (flags & RSPAMD_HTTP_FLAG_WANT_SSL) {
+ msg->port = 443;
+ }
+ else {
+ msg->port = 80;
+ }
+ }
+
+ msg->host = g_string_new_len(host, pu.field_data[UF_HOST].len);
+ msg->url = rspamd_fstring_append(msg->url, path, pathlen);
+
+ REF_INIT_RETAIN(msg, rspamd_http_message_free);
+
+ return msg;
+}
+
+const gchar *
+rspamd_http_message_get_body(struct rspamd_http_message *msg,
+ gsize *blen)
+{
+ const gchar *ret = NULL;
+
+ if (msg->body_buf.len > 0) {
+ ret = msg->body_buf.begin;
+ }
+
+ if (blen) {
+ *blen = msg->body_buf.len;
+ }
+
+ return ret;
+}
+
+static void
+rspamd_http_shname_dtor(void *p)
+{
+ struct rspamd_storage_shmem *n = p;
+
+#ifdef HAVE_SANE_SHMEM
+ shm_unlink(n->shm_name);
+#else
+ unlink(n->shm_name);
+#endif
+ g_free(n->shm_name);
+ g_free(n);
+}
+
+struct rspamd_storage_shmem *
+rspamd_http_message_shmem_ref(struct rspamd_http_message *msg)
+{
+ if ((msg->flags & RSPAMD_HTTP_FLAG_SHMEM) && msg->body_buf.c.shared.name) {
+ REF_RETAIN(msg->body_buf.c.shared.name);
+ return msg->body_buf.c.shared.name;
+ }
+
+ return NULL;
+}
+
+guint rspamd_http_message_get_flags(struct rspamd_http_message *msg)
+{
+ return msg->flags;
+}
+
+void rspamd_http_message_shmem_unref(struct rspamd_storage_shmem *p)
+{
+ REF_RELEASE(p);
+}
+
+gboolean
+rspamd_http_message_set_body(struct rspamd_http_message *msg,
+ const gchar *data, gsize len)
+{
+ union _rspamd_storage_u *storage;
+ storage = &msg->body_buf.c;
+
+ rspamd_http_message_storage_cleanup(msg);
+
+ if (msg->flags & RSPAMD_HTTP_FLAG_SHMEM) {
+ storage->shared.name = g_malloc(sizeof(*storage->shared.name));
+ REF_INIT_RETAIN(storage->shared.name, rspamd_http_shname_dtor);
+#ifdef HAVE_SANE_SHMEM
+#if defined(__DragonFly__)
+ // DragonFly uses regular files for shm. User rspamd is not allowed to create
+ // files in the root.
+ storage->shared.name->shm_name = g_strdup("/tmp/rhm.XXXXXXXXXXXXXXXXXXXX");
+#else
+ storage->shared.name->shm_name = g_strdup("/rhm.XXXXXXXXXXXXXXXXXXXX");
+#endif
+ storage->shared.shm_fd = rspamd_shmem_mkstemp(storage->shared.name->shm_name);
+#else
+ /* XXX: assume that tempdir is /tmp */
+ storage->shared.name->shm_name = g_strdup("/tmp/rhm.XXXXXXXXXXXXXXXXXXXX");
+ storage->shared.shm_fd = mkstemp(storage->shared.name->shm_name);
+#endif
+
+ if (storage->shared.shm_fd == -1) {
+ return FALSE;
+ }
+
+ if (len != 0 && len != G_MAXSIZE) {
+ if (ftruncate(storage->shared.shm_fd, len) == -1) {
+ return FALSE;
+ }
+
+ msg->body_buf.str = mmap(NULL, len,
+ PROT_WRITE | PROT_READ, MAP_SHARED,
+ storage->shared.shm_fd, 0);
+
+ if (msg->body_buf.str == MAP_FAILED) {
+ return FALSE;
+ }
+
+ msg->body_buf.begin = msg->body_buf.str;
+ msg->body_buf.allocated_len = len;
+
+ if (data != NULL) {
+ memcpy(msg->body_buf.str, data, len);
+ msg->body_buf.len = len;
+ }
+ }
+ else {
+ msg->body_buf.len = 0;
+ msg->body_buf.begin = NULL;
+ msg->body_buf.str = NULL;
+ msg->body_buf.allocated_len = 0;
+ }
+ }
+ else {
+ if (len != 0 && len != G_MAXSIZE) {
+ if (data == NULL) {
+ storage->normal = rspamd_fstring_sized_new(len);
+ msg->body_buf.len = 0;
+ }
+ else {
+ storage->normal = rspamd_fstring_new_init(data, len);
+ msg->body_buf.len = len;
+ }
+ }
+ else {
+ storage->normal = rspamd_fstring_new();
+ }
+
+ msg->body_buf.begin = storage->normal->str;
+ msg->body_buf.str = storage->normal->str;
+ msg->body_buf.allocated_len = storage->normal->allocated;
+ }
+
+ msg->flags |= RSPAMD_HTTP_FLAG_HAS_BODY;
+
+ return TRUE;
+}
+
+void rspamd_http_message_set_method(struct rspamd_http_message *msg,
+ const gchar *method)
+{
+ gint i;
+
+ /* Linear search: not very efficient method */
+ for (i = 0; i < HTTP_METHOD_MAX; i++) {
+ if (g_ascii_strcasecmp(method, http_method_str(i)) == 0) {
+ msg->method = i;
+ }
+ }
+}
+
+gboolean
+rspamd_http_message_set_body_from_fd(struct rspamd_http_message *msg,
+ gint fd)
+{
+ union _rspamd_storage_u *storage;
+ struct stat st;
+
+ rspamd_http_message_storage_cleanup(msg);
+
+ storage = &msg->body_buf.c;
+ msg->flags |= RSPAMD_HTTP_FLAG_SHMEM | RSPAMD_HTTP_FLAG_SHMEM_IMMUTABLE;
+
+ storage->shared.shm_fd = dup(fd);
+ msg->body_buf.str = MAP_FAILED;
+
+ if (storage->shared.shm_fd == -1) {
+ return FALSE;
+ }
+
+ if (fstat(storage->shared.shm_fd, &st) == -1) {
+ return FALSE;
+ }
+
+ msg->body_buf.str = mmap(NULL, st.st_size,
+ PROT_READ, MAP_SHARED,
+ storage->shared.shm_fd, 0);
+
+ if (msg->body_buf.str == MAP_FAILED) {
+ return FALSE;
+ }
+
+ msg->body_buf.begin = msg->body_buf.str;
+ msg->body_buf.len = st.st_size;
+ msg->body_buf.allocated_len = st.st_size;
+
+ return TRUE;
+}
+
+gboolean
+rspamd_http_message_set_body_from_fstring_steal(struct rspamd_http_message *msg,
+ rspamd_fstring_t *fstr)
+{
+ union _rspamd_storage_u *storage;
+
+ rspamd_http_message_storage_cleanup(msg);
+
+ storage = &msg->body_buf.c;
+ msg->flags &= ~(RSPAMD_HTTP_FLAG_SHMEM | RSPAMD_HTTP_FLAG_SHMEM_IMMUTABLE);
+
+ storage->normal = fstr;
+ msg->body_buf.str = fstr->str;
+ msg->body_buf.begin = msg->body_buf.str;
+ msg->body_buf.len = fstr->len;
+ msg->body_buf.allocated_len = fstr->allocated;
+
+ return TRUE;
+}
+
+gboolean
+rspamd_http_message_set_body_from_fstring_copy(struct rspamd_http_message *msg,
+ const rspamd_fstring_t *fstr)
+{
+ union _rspamd_storage_u *storage;
+
+ rspamd_http_message_storage_cleanup(msg);
+
+ storage = &msg->body_buf.c;
+ msg->flags &= ~(RSPAMD_HTTP_FLAG_SHMEM | RSPAMD_HTTP_FLAG_SHMEM_IMMUTABLE);
+
+ storage->normal = rspamd_fstring_new_init(fstr->str, fstr->len);
+ msg->body_buf.str = storage->normal->str;
+ msg->body_buf.begin = msg->body_buf.str;
+ msg->body_buf.len = storage->normal->len;
+ msg->body_buf.allocated_len = storage->normal->allocated;
+
+ return TRUE;
+}
+
+
+gboolean
+rspamd_http_message_grow_body(struct rspamd_http_message *msg, gsize len)
+{
+ struct stat st;
+ union _rspamd_storage_u *storage;
+ gsize newlen;
+
+ storage = &msg->body_buf.c;
+
+ if (msg->flags & RSPAMD_HTTP_FLAG_SHMEM) {
+ if (storage->shared.shm_fd == -1) {
+ return FALSE;
+ }
+
+ if (fstat(storage->shared.shm_fd, &st) == -1) {
+ return FALSE;
+ }
+
+ /* Check if we need to grow */
+ if ((gsize) st.st_size < msg->body_buf.len + len) {
+ /* Need to grow */
+ newlen = rspamd_fstring_suggest_size(msg->body_buf.len, st.st_size,
+ len);
+ /* Unmap as we need another size of segment */
+ if (msg->body_buf.str != MAP_FAILED) {
+ munmap(msg->body_buf.str, st.st_size);
+ }
+
+ if (ftruncate(storage->shared.shm_fd, newlen) == -1) {
+ return FALSE;
+ }
+
+ msg->body_buf.str = mmap(NULL, newlen,
+ PROT_WRITE | PROT_READ, MAP_SHARED,
+ storage->shared.shm_fd, 0);
+ if (msg->body_buf.str == MAP_FAILED) {
+ return FALSE;
+ }
+
+ msg->body_buf.begin = msg->body_buf.str;
+ msg->body_buf.allocated_len = newlen;
+ }
+ }
+ else {
+ storage->normal = rspamd_fstring_grow(storage->normal, len);
+
+ /* Append might cause realloc */
+ msg->body_buf.begin = storage->normal->str;
+ msg->body_buf.len = storage->normal->len;
+ msg->body_buf.str = storage->normal->str;
+ msg->body_buf.allocated_len = storage->normal->allocated;
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_http_message_append_body(struct rspamd_http_message *msg,
+ const gchar *data, gsize len)
+{
+ union _rspamd_storage_u *storage;
+
+ storage = &msg->body_buf.c;
+
+ if (msg->flags & RSPAMD_HTTP_FLAG_SHMEM) {
+ if (!rspamd_http_message_grow_body(msg, len)) {
+ return FALSE;
+ }
+
+ memcpy(msg->body_buf.str + msg->body_buf.len, data, len);
+ msg->body_buf.len += len;
+ }
+ else {
+ storage->normal = rspamd_fstring_append(storage->normal, data, len);
+
+ /* Append might cause realloc */
+ msg->body_buf.begin = storage->normal->str;
+ msg->body_buf.len = storage->normal->len;
+ msg->body_buf.str = storage->normal->str;
+ msg->body_buf.allocated_len = storage->normal->allocated;
+ }
+
+ return TRUE;
+}
+
+void rspamd_http_message_storage_cleanup(struct rspamd_http_message *msg)
+{
+ union _rspamd_storage_u *storage;
+ struct stat st;
+
+ if (msg->flags & RSPAMD_HTTP_FLAG_SHMEM) {
+ storage = &msg->body_buf.c;
+
+ if (storage->shared.shm_fd > 0) {
+ g_assert(fstat(storage->shared.shm_fd, &st) != -1);
+
+ if (msg->body_buf.str != MAP_FAILED) {
+ munmap(msg->body_buf.str, st.st_size);
+ }
+
+ close(storage->shared.shm_fd);
+ }
+
+ if (storage->shared.name != NULL) {
+ REF_RELEASE(storage->shared.name);
+ }
+
+ storage->shared.shm_fd = -1;
+ msg->body_buf.str = MAP_FAILED;
+ }
+ else {
+ if (msg->body_buf.c.normal) {
+ rspamd_fstring_free(msg->body_buf.c.normal);
+ }
+
+ msg->body_buf.c.normal = NULL;
+ }
+
+ msg->body_buf.len = 0;
+}
+
+void rspamd_http_message_free(struct rspamd_http_message *msg)
+{
+ struct rspamd_http_header *hdr, *hcur, *hcurtmp;
+
+ kh_foreach_value (msg->headers, hdr, {
+ DL_FOREACH_SAFE (hdr, hcur, hcurtmp) {
+ rspamd_fstring_free (hcur->combined);
+ g_free (hcur);
+}
+});
+
+kh_destroy(rspamd_http_headers_hash, msg->headers);
+rspamd_http_message_storage_cleanup(msg);
+
+if (msg->url != NULL) {
+ rspamd_fstring_free(msg->url);
+}
+if (msg->status != NULL) {
+ rspamd_fstring_free(msg->status);
+}
+if (msg->host != NULL) {
+ g_string_free(msg->host, TRUE);
+}
+if (msg->peer_key != NULL) {
+ rspamd_pubkey_unref(msg->peer_key);
+}
+
+g_free(msg);
+}
+
+void rspamd_http_message_set_peer_key(struct rspamd_http_message *msg,
+ struct rspamd_cryptobox_pubkey *pk)
+{
+ if (msg->peer_key != NULL) {
+ rspamd_pubkey_unref(msg->peer_key);
+ }
+
+ if (pk) {
+ msg->peer_key = rspamd_pubkey_ref(pk);
+ }
+ else {
+ msg->peer_key = NULL;
+ }
+}
+
+void rspamd_http_message_add_header_len(struct rspamd_http_message *msg,
+ const gchar *name,
+ const gchar *value,
+ gsize len)
+{
+ struct rspamd_http_header *hdr, *found;
+ guint nlen, vlen;
+ khiter_t k;
+ gint r;
+
+ if (msg != NULL && name != NULL && value != NULL) {
+ hdr = g_malloc0(sizeof(struct rspamd_http_header));
+ nlen = strlen(name);
+ vlen = len;
+
+ if (g_ascii_strcasecmp(name, "host") == 0) {
+ msg->flags |= RSPAMD_HTTP_FLAG_HAS_HOST_HEADER;
+ }
+
+ hdr->combined = rspamd_fstring_sized_new(nlen + vlen + 4);
+ rspamd_printf_fstring(&hdr->combined, "%s: %*s\r\n", name, (gint) vlen,
+ value);
+ hdr->name.begin = hdr->combined->str;
+ hdr->name.len = nlen;
+ hdr->value.begin = hdr->combined->str + nlen + 2;
+ hdr->value.len = vlen;
+
+ k = kh_put(rspamd_http_headers_hash, msg->headers, &hdr->name,
+ &r);
+
+ if (r != 0) {
+ kh_value(msg->headers, k) = hdr;
+ found = NULL;
+ }
+ else {
+ found = kh_value(msg->headers, k);
+ }
+
+ DL_APPEND(found, hdr);
+ }
+}
+
+void rspamd_http_message_add_header(struct rspamd_http_message *msg,
+ const gchar *name,
+ const gchar *value)
+{
+ if (value) {
+ rspamd_http_message_add_header_len(msg, name, value, strlen(value));
+ }
+}
+
+void rspamd_http_message_add_header_fstr(struct rspamd_http_message *msg,
+ const gchar *name,
+ rspamd_fstring_t *value)
+{
+ struct rspamd_http_header *hdr, *found = NULL;
+ guint nlen, vlen;
+ khiter_t k;
+ gint r;
+
+ if (msg != NULL && name != NULL && value != NULL) {
+ hdr = g_malloc0(sizeof(struct rspamd_http_header));
+ nlen = strlen(name);
+ vlen = value->len;
+ hdr->combined = rspamd_fstring_sized_new(nlen + vlen + 4);
+ rspamd_printf_fstring(&hdr->combined, "%s: %V\r\n", name, value);
+ hdr->name.begin = hdr->combined->str;
+ hdr->name.len = nlen;
+ hdr->value.begin = hdr->combined->str + nlen + 2;
+ hdr->value.len = vlen;
+
+ k = kh_put(rspamd_http_headers_hash, msg->headers, &hdr->name,
+ &r);
+
+ if (r != 0) {
+ kh_value(msg->headers, k) = hdr;
+ found = NULL;
+ }
+ else {
+ found = kh_value(msg->headers, k);
+ }
+
+ DL_APPEND(found, hdr);
+ }
+}
+
+const rspamd_ftok_t *
+rspamd_http_message_find_header(struct rspamd_http_message *msg,
+ const gchar *name)
+{
+ const rspamd_ftok_t *res = NULL;
+ rspamd_ftok_t srch;
+ guint slen = strlen(name);
+ khiter_t k;
+
+ if (msg != NULL) {
+ srch.begin = name;
+ srch.len = slen;
+
+ k = kh_get(rspamd_http_headers_hash, msg->headers, &srch);
+
+ if (k != kh_end(msg->headers)) {
+ res = &(kh_value(msg->headers, k)->value);
+ }
+ }
+
+ return res;
+}
+
+GPtrArray *
+rspamd_http_message_find_header_multiple(
+ struct rspamd_http_message *msg,
+ const gchar *name)
+{
+ GPtrArray *res = NULL;
+ struct rspamd_http_header *hdr, *cur;
+ rspamd_ftok_t srch;
+ khiter_t k;
+ guint cnt = 0;
+
+ guint slen = strlen(name);
+
+ if (msg != NULL) {
+ srch.begin = name;
+ srch.len = slen;
+
+ k = kh_get(rspamd_http_headers_hash, msg->headers, &srch);
+
+ if (k != kh_end(msg->headers)) {
+ hdr = kh_value(msg->headers, k);
+
+ LL_COUNT(hdr, cur, cnt);
+ res = g_ptr_array_sized_new(cnt);
+
+ LL_FOREACH(hdr, cur)
+ {
+ g_ptr_array_add(res, &cur->value);
+ }
+ }
+ }
+
+
+ return res;
+}
+
+
+gboolean
+rspamd_http_message_remove_header(struct rspamd_http_message *msg,
+ const gchar *name)
+{
+ struct rspamd_http_header *hdr, *hcur, *hcurtmp;
+ gboolean res = FALSE;
+ guint slen = strlen(name);
+ rspamd_ftok_t srch;
+ khiter_t k;
+
+ if (msg != NULL) {
+ srch.begin = name;
+ srch.len = slen;
+
+ k = kh_get(rspamd_http_headers_hash, msg->headers, &srch);
+
+ if (k != kh_end(msg->headers)) {
+ hdr = kh_value(msg->headers, k);
+ kh_del(rspamd_http_headers_hash, msg->headers, k);
+ res = TRUE;
+
+ DL_FOREACH_SAFE(hdr, hcur, hcurtmp)
+ {
+ rspamd_fstring_free(hcur->combined);
+ g_free(hcur);
+ }
+ }
+ }
+
+ return res;
+}
+
+const gchar *
+rspamd_http_message_get_http_host(struct rspamd_http_message *msg,
+ gsize *hostlen)
+{
+ if (msg->flags & RSPAMD_HTTP_FLAG_HAS_HOST_HEADER) {
+ rspamd_ftok_t srch;
+
+ RSPAMD_FTOK_ASSIGN(&srch, "Host");
+
+ khiter_t k = kh_get(rspamd_http_headers_hash, msg->headers, &srch);
+
+ if (k != kh_end(msg->headers)) {
+ *hostlen = (kh_value(msg->headers, k)->value).len;
+ return (kh_value(msg->headers, k)->value).begin;
+ }
+ else if (msg->host) {
+ *hostlen = msg->host->len;
+ return msg->host->str;
+ }
+ }
+ else {
+ if (msg->host) {
+ *hostlen = msg->host->len;
+ return msg->host->str;
+ }
+ }
+
+ return NULL;
+}
+
+bool rspamd_http_message_is_standard_port(struct rspamd_http_message *msg)
+{
+ if (msg->flags & RSPAMD_HTTP_FLAG_WANT_SSL) {
+ return msg->port == 443;
+ }
+
+ return msg->port == 80;
+} \ No newline at end of file
diff --git a/src/libserver/http/http_message.h b/src/libserver/http/http_message.h
new file mode 100644
index 0000000..fa8ed04
--- /dev/null
+++ b/src/libserver/http/http_message.h
@@ -0,0 +1,254 @@
+/*-
+ * Copyright 2019 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_HTTP_MESSAGE_H
+#define RSPAMD_HTTP_MESSAGE_H
+
+#include "config.h"
+#include "keypair.h"
+#include "keypairs_cache.h"
+#include "fstring.h"
+#include "ref.h"
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_http_connection;
+
+enum rspamd_http_message_type {
+ HTTP_REQUEST = 0,
+ HTTP_RESPONSE
+};
+
+/**
+ * Extract the current message from a connection to deal with separately
+ * @param conn
+ * @return
+ */
+struct rspamd_http_message *rspamd_http_connection_steal_msg(
+ struct rspamd_http_connection *conn);
+
+/**
+ * Copy the current message from a connection to deal with separately
+ * @param conn
+ * @return
+ */
+struct rspamd_http_message *rspamd_http_connection_copy_msg(
+ struct rspamd_http_message *msg, GError **err);
+
+/**
+ * Create new HTTP message
+ * @param type request or response
+ * @return new http message
+ */
+struct rspamd_http_message *rspamd_http_new_message(enum rspamd_http_message_type type);
+
+/**
+ * Increase refcount number for an HTTP message
+ * @param msg message to use
+ * @return
+ */
+struct rspamd_http_message *rspamd_http_message_ref(struct rspamd_http_message *msg);
+
+/**
+ * Decrease number of refcounts for http message
+ * @param msg
+ */
+void rspamd_http_message_unref(struct rspamd_http_message *msg);
+
+/**
+ * Sets a key for peer
+ * @param msg
+ * @param pk
+ */
+void rspamd_http_message_set_peer_key(struct rspamd_http_message *msg,
+ struct rspamd_cryptobox_pubkey *pk);
+
+/**
+ * Create HTTP message from URL
+ * @param url
+ * @return new message or NULL
+ */
+struct rspamd_http_message *rspamd_http_message_from_url(const gchar *url);
+
+/**
+ * Returns body for a message
+ * @param msg
+ * @param blen pointer where to save body length
+ * @return pointer to body start
+ */
+const gchar *rspamd_http_message_get_body(struct rspamd_http_message *msg,
+ gsize *blen);
+
+/**
+ * Set message's body from the string
+ * @param msg
+ * @param data
+ * @param len
+ * @return TRUE if a message's body has been set
+ */
+gboolean rspamd_http_message_set_body(struct rspamd_http_message *msg,
+ const gchar *data, gsize len);
+
+/**
+ * Set message's method by name
+ * @param msg
+ * @param method
+ */
+void rspamd_http_message_set_method(struct rspamd_http_message *msg,
+ const gchar *method);
+
+/**
+ * Maps fd as message's body
+ * @param msg
+ * @param fd
+ * @return TRUE if a message's body has been set
+ */
+gboolean rspamd_http_message_set_body_from_fd(struct rspamd_http_message *msg,
+ gint fd);
+
+/**
+ * Uses rspamd_fstring_t as message's body, string is consumed by this operation
+ * @param msg
+ * @param fstr
+ * @return TRUE if a message's body has been set
+ */
+gboolean rspamd_http_message_set_body_from_fstring_steal(struct rspamd_http_message *msg,
+ rspamd_fstring_t *fstr);
+
+/**
+ * Uses rspamd_fstring_t as message's body, string is copied by this operation
+ * @param msg
+ * @param fstr
+ * @return TRUE if a message's body has been set
+ */
+gboolean rspamd_http_message_set_body_from_fstring_copy(struct rspamd_http_message *msg,
+ const rspamd_fstring_t *fstr);
+
+/**
+ * Appends data to message's body
+ * @param msg
+ * @param data
+ * @param len
+ * @return TRUE if a message's body has been set
+ */
+gboolean rspamd_http_message_append_body(struct rspamd_http_message *msg,
+ const gchar *data, gsize len);
+
+/**
+ * Append a header to http message
+ * @param rep
+ * @param name
+ * @param value
+ */
+void rspamd_http_message_add_header(struct rspamd_http_message *msg,
+ const gchar *name,
+ const gchar *value);
+
+void rspamd_http_message_add_header_len(struct rspamd_http_message *msg,
+ const gchar *name,
+ const gchar *value,
+ gsize len);
+
+void rspamd_http_message_add_header_fstr(struct rspamd_http_message *msg,
+ const gchar *name,
+ rspamd_fstring_t *value);
+
+/**
+ * Search for a specified header in message
+ * @param msg message
+ * @param name name of header
+ */
+const rspamd_ftok_t *rspamd_http_message_find_header(
+ struct rspamd_http_message *msg,
+ const gchar *name);
+
+/**
+ * Search for a header that has multiple values
+ * @param msg
+ * @param name
+ * @return list of rspamd_ftok_t * with values
+ */
+GPtrArray *rspamd_http_message_find_header_multiple(
+ struct rspamd_http_message *msg,
+ const gchar *name);
+
+/**
+ * Remove specific header from a message
+ * @param msg
+ * @param name
+ * @return
+ */
+gboolean rspamd_http_message_remove_header(struct rspamd_http_message *msg,
+ const gchar *name);
+
+/**
+ * Free HTTP message
+ * @param msg
+ */
+void rspamd_http_message_free(struct rspamd_http_message *msg);
+
+/**
+ * Extract arguments from a message's URI contained inside query string decoding
+ * them if needed
+ * @param msg HTTP request message
+ * @return new GHashTable which maps rspamd_ftok_t* to rspamd_ftok_t*
+ * (table must be freed by a caller)
+ */
+GHashTable *rspamd_http_message_parse_query(struct rspamd_http_message *msg);
+
+/**
+ * Increase refcount for shared file (if any) to prevent early memory unlinking
+ * @param msg
+ */
+struct rspamd_storage_shmem *rspamd_http_message_shmem_ref(struct rspamd_http_message *msg);
+
+/**
+ * Decrease external ref for shmem segment associated with a message
+ * @param msg
+ */
+void rspamd_http_message_shmem_unref(struct rspamd_storage_shmem *p);
+
+/**
+ * Returns message's flags
+ * @param msg
+ * @return
+ */
+guint rspamd_http_message_get_flags(struct rspamd_http_message *msg);
+
+/**
+ * Returns an HTTP hostname for a message, derived from a header if it has it
+ * or from a url if it doesn't
+ * @param msg
+ * @param hostlen output of the host length
+ * @return
+ */
+const gchar *rspamd_http_message_get_http_host(struct rspamd_http_message *msg,
+ gsize *hostlen);
+
+/**
+ * Returns true if a message has standard port (80 or 443 for https)
+ * @param msg
+ * @return
+ */
+bool rspamd_http_message_is_standard_port(struct rspamd_http_message *msg);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/http/http_private.h b/src/libserver/http/http_private.h
new file mode 100644
index 0000000..096545e
--- /dev/null
+++ b/src/libserver/http/http_private.h
@@ -0,0 +1,129 @@
+/*-
+ * 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 SRC_LIBUTIL_HTTP_PRIVATE_H_
+#define SRC_LIBUTIL_HTTP_PRIVATE_H_
+
+#include "http_connection.h"
+#include "http_parser.h"
+#include "str_util.h"
+#include "keypair.h"
+#include "keypairs_cache.h"
+#include "ref.h"
+#include "upstream.h"
+#include "khash.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * HTTP header structure
+ */
+struct rspamd_http_header {
+ rspamd_fstring_t *combined;
+ rspamd_ftok_t name;
+ rspamd_ftok_t value;
+ struct rspamd_http_header *prev, *next;
+};
+
+KHASH_INIT(rspamd_http_headers_hash, rspamd_ftok_t *,
+ struct rspamd_http_header *, 1,
+ rspamd_ftok_icase_hash, rspamd_ftok_icase_equal);
+
+/**
+ * HTTP message structure, used for requests and replies
+ */
+struct rspamd_http_message {
+ rspamd_fstring_t *url;
+ GString *host;
+ rspamd_fstring_t *status;
+ khash_t(rspamd_http_headers_hash) * headers;
+
+ struct _rspamd_body_buf_s {
+ /* Data start */
+ const gchar *begin;
+ /* Data len */
+ gsize len;
+ /* Allocated len */
+ gsize allocated_len;
+ /* Data buffer (used to write data inside) */
+ gchar *str;
+
+ /* Internal storage */
+ union _rspamd_storage_u {
+ rspamd_fstring_t *normal;
+ struct _rspamd_storage_shared_s {
+ struct rspamd_storage_shmem *name;
+ gint shm_fd;
+ } shared;
+ } c;
+ } body_buf;
+
+ struct rspamd_cryptobox_pubkey *peer_key;
+ time_t date;
+ time_t last_modified;
+ unsigned port;
+ int type;
+ gint code;
+ enum http_method method;
+ gint flags;
+ ref_entry_t ref;
+};
+
+struct rspamd_keepalive_hash_key {
+ rspamd_inet_addr_t *addr;
+ gchar *host;
+ gboolean is_ssl;
+ unsigned port;
+ GQueue conns;
+};
+
+gint32 rspamd_keep_alive_key_hash(struct rspamd_keepalive_hash_key *k);
+
+bool rspamd_keep_alive_key_equal(struct rspamd_keepalive_hash_key *k1,
+ struct rspamd_keepalive_hash_key *k2);
+
+KHASH_INIT(rspamd_keep_alive_hash, struct rspamd_keepalive_hash_key *,
+ char, 0, rspamd_keep_alive_key_hash, rspamd_keep_alive_key_equal);
+
+struct rspamd_http_context {
+ struct rspamd_http_context_cfg config;
+ struct rspamd_keypair_cache *client_kp_cache;
+ struct rspamd_cryptobox_keypair *client_kp;
+ struct rspamd_keypair_cache *server_kp_cache;
+ struct upstream_ctx *ups_ctx;
+ struct upstream_list *http_proxies;
+ gpointer ssl_ctx;
+ gpointer ssl_ctx_noverify;
+ struct ev_loop *event_loop;
+ ev_timer client_rotate_ev;
+ khash_t(rspamd_keep_alive_hash) * keep_alive_hash;
+};
+
+#define HTTP_ERROR http_error_quark()
+
+GQuark http_error_quark(void);
+
+void rspamd_http_message_storage_cleanup(struct rspamd_http_message *msg);
+
+gboolean rspamd_http_message_grow_body(struct rspamd_http_message *msg,
+ gsize len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SRC_LIBUTIL_HTTP_PRIVATE_H_ */
diff --git a/src/libserver/http/http_router.c b/src/libserver/http/http_router.c
new file mode 100644
index 0000000..2fdfe48
--- /dev/null
+++ b/src/libserver/http/http_router.c
@@ -0,0 +1,559 @@
+/*-
+ * Copyright 2019 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 "http_router.h"
+#include "http_connection.h"
+#include "http_private.h"
+#include "libutil/regexp.h"
+#include "libutil/printf.h"
+#include "libserver/logger.h"
+#include "utlist.h"
+#include "unix-std.h"
+
+enum http_magic_type {
+ HTTP_MAGIC_PLAIN = 0,
+ HTTP_MAGIC_HTML,
+ HTTP_MAGIC_CSS,
+ HTTP_MAGIC_JS,
+ HTTP_MAGIC_ICO,
+ HTTP_MAGIC_PNG,
+ HTTP_MAGIC_JPG
+};
+
+static const struct _rspamd_http_magic {
+ const gchar *ext;
+ const gchar *ct;
+} http_file_types[] = {
+ [HTTP_MAGIC_PLAIN] = {"txt", "text/plain"},
+ [HTTP_MAGIC_HTML] = {"html", "text/html"},
+ [HTTP_MAGIC_CSS] = {"css", "text/css"},
+ [HTTP_MAGIC_JS] = {"js", "application/javascript"},
+ [HTTP_MAGIC_ICO] = {"ico", "image/x-icon"},
+ [HTTP_MAGIC_PNG] = {"png", "image/png"},
+ [HTTP_MAGIC_JPG] = {"jpg", "image/jpeg"},
+};
+
+/*
+ * HTTP router functions
+ */
+
+static void
+rspamd_http_entry_free(struct rspamd_http_connection_entry *entry)
+{
+ if (entry != NULL) {
+ close(entry->conn->fd);
+ rspamd_http_connection_unref(entry->conn);
+ if (entry->rt->finish_handler) {
+ entry->rt->finish_handler(entry);
+ }
+
+ DL_DELETE(entry->rt->conns, entry);
+ g_free(entry);
+ }
+}
+
+static void
+rspamd_http_router_error_handler(struct rspamd_http_connection *conn,
+ GError *err)
+{
+ struct rspamd_http_connection_entry *entry = conn->ud;
+ struct rspamd_http_message *msg;
+
+ if (entry->is_reply) {
+ /* At this point we need to finish this session and close owned socket */
+ if (entry->rt->error_handler != NULL) {
+ entry->rt->error_handler(entry, err);
+ }
+ rspamd_http_entry_free(entry);
+ }
+ else {
+ /* Here we can write a reply to a client */
+ if (entry->rt->error_handler != NULL) {
+ entry->rt->error_handler(entry, err);
+ }
+ msg = rspamd_http_new_message(HTTP_RESPONSE);
+ msg->date = time(NULL);
+ msg->code = err->code;
+ rspamd_http_message_set_body(msg, err->message, strlen(err->message));
+ rspamd_http_connection_reset(entry->conn);
+ rspamd_http_connection_write_message(entry->conn,
+ msg,
+ NULL,
+ "text/plain",
+ entry,
+ entry->rt->timeout);
+ entry->is_reply = TRUE;
+ }
+}
+
+static const gchar *
+rspamd_http_router_detect_ct(const gchar *path)
+{
+ const gchar *dot;
+ guint i;
+
+ dot = strrchr(path, '.');
+ if (dot == NULL) {
+ return http_file_types[HTTP_MAGIC_PLAIN].ct;
+ }
+ dot++;
+
+ for (i = 0; i < G_N_ELEMENTS(http_file_types); i++) {
+ if (strcmp(http_file_types[i].ext, dot) == 0) {
+ return http_file_types[i].ct;
+ }
+ }
+
+ return http_file_types[HTTP_MAGIC_PLAIN].ct;
+}
+
+static gboolean
+rspamd_http_router_is_subdir(const gchar *parent, const gchar *sub)
+{
+ if (parent == NULL || sub == NULL || *parent == '\0') {
+ return FALSE;
+ }
+
+ while (*parent != '\0') {
+ if (*sub != *parent) {
+ return FALSE;
+ }
+ parent++;
+ sub++;
+ }
+
+ parent--;
+ if (*parent == G_DIR_SEPARATOR) {
+ return TRUE;
+ }
+
+ return (*sub == G_DIR_SEPARATOR || *sub == '\0');
+}
+
+static gboolean
+rspamd_http_router_try_file(struct rspamd_http_connection_entry *entry,
+ rspamd_ftok_t *lookup, gboolean expand_path)
+{
+ struct stat st;
+ gint fd;
+ gchar filebuf[PATH_MAX], realbuf[PATH_MAX], *dir;
+ struct rspamd_http_message *reply_msg;
+
+ rspamd_snprintf(filebuf, sizeof(filebuf), "%s%c%T",
+ entry->rt->default_fs_path, G_DIR_SEPARATOR, lookup);
+
+ if (realpath(filebuf, realbuf) == NULL ||
+ lstat(realbuf, &st) == -1) {
+ return FALSE;
+ }
+
+ if (S_ISDIR(st.st_mode) && expand_path) {
+ /* Try to append 'index.html' to the url */
+ rspamd_fstring_t *nlookup;
+ rspamd_ftok_t tok;
+ gboolean ret;
+
+ nlookup = rspamd_fstring_sized_new(lookup->len + sizeof("index.html"));
+ rspamd_printf_fstring(&nlookup, "%T%c%s", lookup, G_DIR_SEPARATOR,
+ "index.html");
+ tok.begin = nlookup->str;
+ tok.len = nlookup->len;
+ ret = rspamd_http_router_try_file(entry, &tok, FALSE);
+ rspamd_fstring_free(nlookup);
+
+ return ret;
+ }
+ else if (!S_ISREG(st.st_mode)) {
+ return FALSE;
+ }
+
+ /* We also need to ensure that file is inside the defined dir */
+ rspamd_strlcpy(filebuf, realbuf, sizeof(filebuf));
+ dir = dirname(filebuf);
+
+ if (dir == NULL ||
+ !rspamd_http_router_is_subdir(entry->rt->default_fs_path,
+ dir)) {
+ return FALSE;
+ }
+
+ fd = open(realbuf, O_RDONLY);
+ if (fd == -1) {
+ return FALSE;
+ }
+
+ reply_msg = rspamd_http_new_message(HTTP_RESPONSE);
+ reply_msg->date = time(NULL);
+ reply_msg->code = 200;
+ rspamd_http_router_insert_headers(entry->rt, reply_msg);
+
+ if (!rspamd_http_message_set_body_from_fd(reply_msg, fd)) {
+ rspamd_http_message_free(reply_msg);
+ close(fd);
+ return FALSE;
+ }
+
+ close(fd);
+
+ rspamd_http_connection_reset(entry->conn);
+
+ msg_debug("requested file %s", realbuf);
+ rspamd_http_connection_write_message(entry->conn, reply_msg, NULL,
+ rspamd_http_router_detect_ct(realbuf), entry,
+ entry->rt->timeout);
+
+ return TRUE;
+}
+
+static void
+rspamd_http_router_send_error(GError *err,
+ struct rspamd_http_connection_entry *entry)
+{
+ struct rspamd_http_message *err_msg;
+
+ err_msg = rspamd_http_new_message(HTTP_RESPONSE);
+ err_msg->date = time(NULL);
+ err_msg->code = err->code;
+ rspamd_http_message_set_body(err_msg, err->message,
+ strlen(err->message));
+ entry->is_reply = TRUE;
+ err_msg->status = rspamd_fstring_new_init(err->message, strlen(err->message));
+ rspamd_http_router_insert_headers(entry->rt, err_msg);
+ rspamd_http_connection_reset(entry->conn);
+ rspamd_http_connection_write_message(entry->conn,
+ err_msg,
+ NULL,
+ "text/plain",
+ entry,
+ entry->rt->timeout);
+}
+
+
+static int
+rspamd_http_router_finish_handler(struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg)
+{
+ struct rspamd_http_connection_entry *entry = conn->ud;
+ rspamd_http_router_handler_t handler = NULL;
+ gpointer found;
+
+ GError *err;
+ rspamd_ftok_t lookup;
+ const rspamd_ftok_t *encoding;
+ struct http_parser_url u;
+ guint i;
+ rspamd_regexp_t *re;
+ struct rspamd_http_connection_router *router;
+ gchar *pathbuf = NULL;
+
+ G_STATIC_ASSERT(sizeof(rspamd_http_router_handler_t) ==
+ sizeof(gpointer));
+
+ memset(&lookup, 0, sizeof(lookup));
+ router = entry->rt;
+
+ if (entry->is_reply) {
+ /* Request is finished, it is safe to free a connection */
+ rspamd_http_entry_free(entry);
+ }
+ else {
+ if (G_UNLIKELY(msg->method != HTTP_GET && msg->method != HTTP_POST)) {
+ if (router->unknown_method_handler) {
+ return router->unknown_method_handler(entry, msg);
+ }
+ else {
+ err = g_error_new(HTTP_ERROR, 500,
+ "Invalid method");
+ if (entry->rt->error_handler != NULL) {
+ entry->rt->error_handler(entry, err);
+ }
+
+ rspamd_http_router_send_error(err, entry);
+ g_error_free(err);
+
+ return 0;
+ }
+ }
+
+ /* Search for path */
+ if (msg->url != NULL && msg->url->len != 0) {
+
+ http_parser_parse_url(msg->url->str, msg->url->len, TRUE, &u);
+
+ if (u.field_set & (1 << UF_PATH)) {
+ gsize unnorm_len;
+
+ pathbuf = g_malloc(u.field_data[UF_PATH].len);
+ memcpy(pathbuf, msg->url->str + u.field_data[UF_PATH].off,
+ u.field_data[UF_PATH].len);
+ lookup.begin = pathbuf;
+ lookup.len = u.field_data[UF_PATH].len;
+
+ rspamd_normalize_path_inplace(pathbuf,
+ lookup.len,
+ &unnorm_len);
+ lookup.len = unnorm_len;
+ }
+ else {
+ lookup.begin = msg->url->str;
+ lookup.len = msg->url->len;
+ }
+
+ found = g_hash_table_lookup(entry->rt->paths, &lookup);
+ memcpy(&handler, &found, sizeof(found));
+ msg_debug("requested known path: %T", &lookup);
+ }
+ else {
+ err = g_error_new(HTTP_ERROR, 404,
+ "Empty path requested");
+ if (entry->rt->error_handler != NULL) {
+ entry->rt->error_handler(entry, err);
+ }
+
+ rspamd_http_router_send_error(err, entry);
+ g_error_free(err);
+
+ return 0;
+ }
+
+ entry->is_reply = TRUE;
+
+ encoding = rspamd_http_message_find_header(msg, "Accept-Encoding");
+
+ if (encoding && rspamd_substring_search(encoding->begin, encoding->len,
+ "gzip", 4) != -1) {
+ entry->support_gzip = TRUE;
+ }
+
+ if (handler != NULL) {
+ if (pathbuf) {
+ g_free(pathbuf);
+ }
+
+ return handler(entry, msg);
+ }
+ else {
+ /* Try regexps */
+ for (i = 0; i < router->regexps->len; i++) {
+ re = g_ptr_array_index(router->regexps, i);
+ if (rspamd_regexp_match(re, lookup.begin, lookup.len,
+ TRUE)) {
+ found = rspamd_regexp_get_ud(re);
+ memcpy(&handler, &found, sizeof(found));
+
+ if (pathbuf) {
+ g_free(pathbuf);
+ }
+
+ return handler(entry, msg);
+ }
+ }
+
+ /* Now try plain file */
+ if (entry->rt->default_fs_path == NULL || lookup.len == 0 ||
+ !rspamd_http_router_try_file(entry, &lookup, TRUE)) {
+
+ err = g_error_new(HTTP_ERROR, 404,
+ "Not found");
+ if (entry->rt->error_handler != NULL) {
+ entry->rt->error_handler(entry, err);
+ }
+
+ msg_info("path: %T not found", &lookup);
+ rspamd_http_router_send_error(err, entry);
+ g_error_free(err);
+ }
+ }
+ }
+
+ if (pathbuf) {
+ g_free(pathbuf);
+ }
+
+ return 0;
+}
+
+struct rspamd_http_connection_router *
+rspamd_http_router_new(rspamd_http_router_error_handler_t eh,
+ rspamd_http_router_finish_handler_t fh,
+ ev_tstamp timeout,
+ const char *default_fs_path,
+ struct rspamd_http_context *ctx)
+{
+ struct rspamd_http_connection_router *nrouter;
+ struct stat st;
+
+ nrouter = g_malloc0(sizeof(struct rspamd_http_connection_router));
+ nrouter->paths = g_hash_table_new_full(rspamd_ftok_icase_hash,
+ rspamd_ftok_icase_equal, rspamd_fstring_mapped_ftok_free, NULL);
+ nrouter->regexps = g_ptr_array_new();
+ nrouter->conns = NULL;
+ nrouter->error_handler = eh;
+ nrouter->finish_handler = fh;
+ nrouter->response_headers = g_hash_table_new_full(rspamd_strcase_hash,
+ rspamd_strcase_equal, g_free, g_free);
+ nrouter->event_loop = ctx->event_loop;
+ nrouter->timeout = timeout;
+ nrouter->default_fs_path = NULL;
+
+ if (default_fs_path != NULL) {
+ if (stat(default_fs_path, &st) == -1) {
+ msg_err("cannot stat %s", default_fs_path);
+ }
+ else {
+ if (!S_ISDIR(st.st_mode)) {
+ msg_err("path %s is not a directory", default_fs_path);
+ }
+ else {
+ nrouter->default_fs_path = realpath(default_fs_path, NULL);
+ }
+ }
+ }
+
+ nrouter->ctx = ctx;
+
+ return nrouter;
+}
+
+void rspamd_http_router_set_key(struct rspamd_http_connection_router *router,
+ struct rspamd_cryptobox_keypair *key)
+{
+ g_assert(key != NULL);
+
+ router->key = rspamd_keypair_ref(key);
+}
+
+void rspamd_http_router_add_path(struct rspamd_http_connection_router *router,
+ const gchar *path, rspamd_http_router_handler_t handler)
+{
+ gpointer ptr;
+ rspamd_ftok_t *key;
+ rspamd_fstring_t *storage;
+ G_STATIC_ASSERT(sizeof(rspamd_http_router_handler_t) ==
+ sizeof(gpointer));
+
+ if (path != NULL && handler != NULL && router != NULL) {
+ memcpy(&ptr, &handler, sizeof(ptr));
+ storage = rspamd_fstring_new_init(path, strlen(path));
+ key = g_malloc0(sizeof(*key));
+ key->begin = storage->str;
+ key->len = storage->len;
+ g_hash_table_insert(router->paths, key, ptr);
+ }
+}
+
+void rspamd_http_router_set_unknown_handler(struct rspamd_http_connection_router *router,
+ rspamd_http_router_handler_t handler)
+{
+ if (router != NULL) {
+ router->unknown_method_handler = handler;
+ }
+}
+
+void rspamd_http_router_add_header(struct rspamd_http_connection_router *router,
+ const gchar *name, const gchar *value)
+{
+ if (name != NULL && value != NULL && router != NULL) {
+ g_hash_table_replace(router->response_headers, g_strdup(name),
+ g_strdup(value));
+ }
+}
+
+void rspamd_http_router_insert_headers(struct rspamd_http_connection_router *router,
+ struct rspamd_http_message *msg)
+{
+ GHashTableIter it;
+ gpointer k, v;
+
+ if (router && msg) {
+ g_hash_table_iter_init(&it, router->response_headers);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ rspamd_http_message_add_header(msg, k, v);
+ }
+ }
+}
+
+void rspamd_http_router_add_regexp(struct rspamd_http_connection_router *router,
+ struct rspamd_regexp_s *re, rspamd_http_router_handler_t handler)
+{
+ gpointer ptr;
+ G_STATIC_ASSERT(sizeof(rspamd_http_router_handler_t) ==
+ sizeof(gpointer));
+
+ if (re != NULL && handler != NULL && router != NULL) {
+ memcpy(&ptr, &handler, sizeof(ptr));
+ rspamd_regexp_set_ud(re, ptr);
+ g_ptr_array_add(router->regexps, rspamd_regexp_ref(re));
+ }
+}
+
+void rspamd_http_router_handle_socket(struct rspamd_http_connection_router *router,
+ gint fd, gpointer ud)
+{
+ struct rspamd_http_connection_entry *conn;
+
+ conn = g_malloc0(sizeof(struct rspamd_http_connection_entry));
+ conn->rt = router;
+ conn->ud = ud;
+ conn->is_reply = FALSE;
+
+ conn->conn = rspamd_http_connection_new_server(router->ctx,
+ fd,
+ NULL,
+ rspamd_http_router_error_handler,
+ rspamd_http_router_finish_handler,
+ 0);
+
+ if (router->key) {
+ rspamd_http_connection_set_key(conn->conn, router->key);
+ }
+
+ rspamd_http_connection_read_message(conn->conn, conn, router->timeout);
+ DL_PREPEND(router->conns, conn);
+}
+
+void rspamd_http_router_free(struct rspamd_http_connection_router *router)
+{
+ struct rspamd_http_connection_entry *conn, *tmp;
+ rspamd_regexp_t *re;
+ guint i;
+
+ if (router) {
+ DL_FOREACH_SAFE(router->conns, conn, tmp)
+ {
+ rspamd_http_entry_free(conn);
+ }
+
+ if (router->key) {
+ rspamd_keypair_unref(router->key);
+ }
+
+ if (router->default_fs_path != NULL) {
+ g_free(router->default_fs_path);
+ }
+
+ for (i = 0; i < router->regexps->len; i++) {
+ re = g_ptr_array_index(router->regexps, i);
+ rspamd_regexp_unref(re);
+ }
+
+ g_ptr_array_free(router->regexps, TRUE);
+ g_hash_table_unref(router->paths);
+ g_hash_table_unref(router->response_headers);
+ g_free(router);
+ }
+}
diff --git a/src/libserver/http/http_router.h b/src/libserver/http/http_router.h
new file mode 100644
index 0000000..1bf70ed
--- /dev/null
+++ b/src/libserver/http/http_router.h
@@ -0,0 +1,149 @@
+/*-
+ * Copyright 2019 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_HTTP_ROUTER_H
+#define RSPAMD_HTTP_ROUTER_H
+
+#include "config.h"
+#include "http_connection.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_http_connection_router;
+struct rspamd_http_connection_entry;
+
+typedef int (*rspamd_http_router_handler_t)(struct rspamd_http_connection_entry
+ *conn_ent,
+ struct rspamd_http_message *msg);
+
+typedef void (*rspamd_http_router_error_handler_t)(struct rspamd_http_connection_entry *conn_ent,
+ GError *err);
+
+typedef void (*rspamd_http_router_finish_handler_t)(struct rspamd_http_connection_entry *conn_ent);
+
+
+struct rspamd_http_connection_entry {
+ struct rspamd_http_connection_router *rt;
+ struct rspamd_http_connection *conn;
+ gpointer ud;
+ gboolean is_reply;
+ gboolean support_gzip;
+ struct rspamd_http_connection_entry *prev, *next;
+};
+
+struct rspamd_http_connection_router {
+ struct rspamd_http_connection_entry *conns;
+ GHashTable *paths;
+ GHashTable *response_headers;
+ GPtrArray *regexps;
+ ev_tstamp timeout;
+ struct ev_loop *event_loop;
+ struct rspamd_http_context *ctx;
+ gchar *default_fs_path;
+ rspamd_http_router_handler_t unknown_method_handler;
+ struct rspamd_cryptobox_keypair *key;
+ rspamd_http_router_error_handler_t error_handler;
+ rspamd_http_router_finish_handler_t finish_handler;
+};
+
+/**
+ * Create new http connection router and the associated HTTP connection
+ * @param eh error handler callback
+ * @param fh finish handler callback
+ * @param default_fs_path if not NULL try to serve static files from
+ * the specified directory
+ * @return
+ */
+struct rspamd_http_connection_router *rspamd_http_router_new(
+ rspamd_http_router_error_handler_t eh,
+ rspamd_http_router_finish_handler_t fh,
+ ev_tstamp timeout,
+ const char *default_fs_path,
+ struct rspamd_http_context *ctx);
+
+/**
+ * Set encryption key for the HTTP router
+ * @param router router structure
+ * @param key opaque key structure
+ */
+void rspamd_http_router_set_key(struct rspamd_http_connection_router *router,
+ struct rspamd_cryptobox_keypair *key);
+
+/**
+ * Add new path to the router
+ */
+void rspamd_http_router_add_path(struct rspamd_http_connection_router *router,
+ const gchar *path, rspamd_http_router_handler_t handler);
+
+/**
+ * Add custom header to append to router replies
+ * @param router
+ * @param name
+ * @param value
+ */
+void rspamd_http_router_add_header(struct rspamd_http_connection_router *router,
+ const gchar *name, const gchar *value);
+
+/**
+ * Sets method to handle unknown request methods
+ * @param router
+ * @param handler
+ */
+void rspamd_http_router_set_unknown_handler(struct rspamd_http_connection_router *router,
+ rspamd_http_router_handler_t handler);
+
+/**
+ * Inserts router headers to the outbound message
+ * @param router
+ * @param msg
+ */
+void rspamd_http_router_insert_headers(struct rspamd_http_connection_router *router,
+ struct rspamd_http_message *msg);
+
+struct rspamd_regexp_s;
+
+/**
+ * Adds new pattern to router, regexp object is refcounted by this function
+ * @param router
+ * @param re
+ * @param handler
+ */
+void rspamd_http_router_add_regexp(struct rspamd_http_connection_router *router,
+ struct rspamd_regexp_s *re, rspamd_http_router_handler_t handler);
+
+/**
+ * Handle new accepted socket
+ * @param router router object
+ * @param fd server socket
+ * @param ud opaque userdata
+ */
+void rspamd_http_router_handle_socket(
+ struct rspamd_http_connection_router *router,
+ gint fd,
+ gpointer ud);
+
+/**
+ * Free router and all connections associated
+ * @param router
+ */
+void rspamd_http_router_free(struct rspamd_http_connection_router *router);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/http/http_util.c b/src/libserver/http/http_util.c
new file mode 100644
index 0000000..d5c4a57
--- /dev/null
+++ b/src/libserver/http/http_util.c
@@ -0,0 +1,295 @@
+/*-
+ * Copyright 2019 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 "libserver/http/http_util.h"
+#include "libutil/printf.h"
+#include "libutil/util.h"
+
+static const gchar *http_week[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
+static const gchar *http_month[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
+
+/*
+ * Obtained from nginx
+ * Copyright (C) Igor Sysoev
+ */
+static guint mday[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+
+time_t
+rspamd_http_parse_date(const gchar *header, gsize len)
+{
+ const gchar *p, *end;
+ gint month;
+ guint day, year, hour, min, sec;
+ guint64 time;
+ enum {
+ no = 0,
+ rfc822, /* Tue, 10 Nov 2002 23:50:13 */
+ rfc850, /* Tuesday, 10-Dec-02 23:50:13 */
+ isoc /* Tue Dec 10 23:50:13 2002 */
+ } fmt;
+
+ fmt = 0;
+ if (len > 0) {
+ end = header + len;
+ }
+ else {
+ end = header + strlen(header);
+ }
+
+ day = 32;
+ year = 2038;
+
+ for (p = header; p < end; p++) {
+ if (*p == ',') {
+ break;
+ }
+
+ if (*p == ' ') {
+ fmt = isoc;
+ break;
+ }
+ }
+
+ for (p++; p < end; p++)
+ if (*p != ' ') {
+ break;
+ }
+
+ if (end - p < 18) {
+ return (time_t) -1;
+ }
+
+ if (fmt != isoc) {
+ if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9') {
+ return (time_t) -1;
+ }
+
+ day = (*p - '0') * 10 + *(p + 1) - '0';
+ p += 2;
+
+ if (*p == ' ') {
+ if (end - p < 18) {
+ return (time_t) -1;
+ }
+ fmt = rfc822;
+ }
+ else if (*p == '-') {
+ fmt = rfc850;
+ }
+ else {
+ return (time_t) -1;
+ }
+
+ p++;
+ }
+
+ switch (*p) {
+
+ case 'J':
+ month = *(p + 1) == 'a' ? 0 : *(p + 2) == 'n' ? 5
+ : 6;
+ break;
+
+ case 'F':
+ month = 1;
+ break;
+
+ case 'M':
+ month = *(p + 2) == 'r' ? 2 : 4;
+ break;
+
+ case 'A':
+ month = *(p + 1) == 'p' ? 3 : 7;
+ break;
+
+ case 'S':
+ month = 8;
+ break;
+
+ case 'O':
+ month = 9;
+ break;
+
+ case 'N':
+ month = 10;
+ break;
+
+ case 'D':
+ month = 11;
+ break;
+
+ default:
+ return (time_t) -1;
+ }
+
+ p += 3;
+
+ if ((fmt == rfc822 && *p != ' ') || (fmt == rfc850 && *p != '-')) {
+ return (time_t) -1;
+ }
+
+ p++;
+
+ if (fmt == rfc822) {
+ if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9' || *(p + 2) < '0' || *(p + 2) > '9' || *(p + 3) < '0' || *(p + 3) > '9') {
+ return (time_t) -1;
+ }
+
+ year = (*p - '0') * 1000 + (*(p + 1) - '0') * 100 + (*(p + 2) - '0') * 10 + *(p + 3) - '0';
+ p += 4;
+ }
+ else if (fmt == rfc850) {
+ if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9') {
+ return (time_t) -1;
+ }
+
+ year = (*p - '0') * 10 + *(p + 1) - '0';
+ year += (year < 70) ? 2000 : 1900;
+ p += 2;
+ }
+
+ if (fmt == isoc) {
+ if (*p == ' ') {
+ p++;
+ }
+
+ if (*p < '0' || *p > '9') {
+ return (time_t) -1;
+ }
+
+ day = *p++ - '0';
+
+ if (*p != ' ') {
+ if (*p < '0' || *p > '9') {
+ return (time_t) -1;
+ }
+
+ day = day * 10 + *p++ - '0';
+ }
+
+ if (end - p < 14) {
+ return (time_t) -1;
+ }
+ }
+
+ if (*p++ != ' ') {
+ return (time_t) -1;
+ }
+
+ if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9') {
+ return (time_t) -1;
+ }
+
+ hour = (*p - '0') * 10 + *(p + 1) - '0';
+ p += 2;
+
+ if (*p++ != ':') {
+ return (time_t) -1;
+ }
+
+ if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9') {
+ return (time_t) -1;
+ }
+
+ min = (*p - '0') * 10 + *(p + 1) - '0';
+ p += 2;
+
+ if (*p++ != ':') {
+ return (time_t) -1;
+ }
+
+ if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9') {
+ return (time_t) -1;
+ }
+
+ sec = (*p - '0') * 10 + *(p + 1) - '0';
+
+ if (fmt == isoc) {
+ p += 2;
+
+ if (*p++ != ' ') {
+ return (time_t) -1;
+ }
+
+ if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9' || *(p + 2) < '0' || *(p + 2) > '9' || *(p + 3) < '0' || *(p + 3) > '9') {
+ return (time_t) -1;
+ }
+
+ year = (*p - '0') * 1000 + (*(p + 1) - '0') * 100 + (*(p + 2) - '0') * 10 + *(p + 3) - '0';
+ }
+
+ if (hour > 23 || min > 59 || sec > 59) {
+ return (time_t) -1;
+ }
+
+ if (day == 29 && month == 1) {
+ if ((year & 3) || ((year % 100 == 0) && (year % 400) != 0)) {
+ return (time_t) -1;
+ }
+ }
+ else if (day > mday[month]) {
+ return (time_t) -1;
+ }
+
+ /*
+ * shift new year to March 1 and start months from 1 (not 0),
+ * it is needed for Gauss' formula
+ */
+
+ if (--month <= 0) {
+ month += 12;
+ year -= 1;
+ }
+
+ /* Gauss' formula for Gregorian days since March 1, 1 BC */
+
+ time = (guint64) (
+ /* days in years including leap years since March 1, 1 BC */
+
+ 365 * year + year / 4 - year / 100 + year / 400
+
+ /* days before the month */
+
+ + 367 * month / 12 - 30
+
+ /* days before the day */
+
+ + day - 1
+
+ /*
+ * 719527 days were between March 1, 1 BC and March 1, 1970,
+ * 31 and 28 days were in January and February 1970
+ */
+
+ - 719527 + 31 + 28) *
+ 86400 +
+ hour * 3600 + min * 60 + sec;
+
+ return (time_t) time;
+}
+
+glong rspamd_http_date_format(gchar *buf, gsize len, time_t time)
+{
+ struct tm tms;
+
+ rspamd_gmtime(time, &tms);
+
+ return rspamd_snprintf(buf, len, "%s, %02d %s %4d %02d:%02d:%02d GMT",
+ http_week[tms.tm_wday], tms.tm_mday,
+ http_month[tms.tm_mon], tms.tm_year + 1900,
+ tms.tm_hour, tms.tm_min, tms.tm_sec);
+} \ No newline at end of file
diff --git a/src/libserver/http/http_util.h b/src/libserver/http/http_util.h
new file mode 100644
index 0000000..ec57508
--- /dev/null
+++ b/src/libserver/http/http_util.h
@@ -0,0 +1,47 @@
+/*-
+ * Copyright 2019 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_HTTP_UTIL_H
+#define RSPAMD_HTTP_UTIL_H
+
+#include "config.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Parse HTTP date header and return it as time_t
+ * @param header HTTP date header
+ * @param len length of header
+ * @return time_t or (time_t)-1 in case of error
+ */
+time_t rspamd_http_parse_date(const gchar *header, gsize len);
+
+/**
+ * Prints HTTP date from `time` to `buf` using standard HTTP date format
+ * @param buf date buffer
+ * @param len length of buffer
+ * @param time time in unix seconds
+ * @return number of bytes written
+ */
+glong rspamd_http_date_format(gchar *buf, gsize len, time_t time);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/hyperscan_tools.cxx b/src/libserver/hyperscan_tools.cxx
new file mode 100644
index 0000000..7d1ecf3
--- /dev/null
+++ b/src/libserver/hyperscan_tools.cxx
@@ -0,0 +1,627 @@
+/*
+ * 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"
+
+#ifdef WITH_HYPERSCAN
+#include <string>
+#include <filesystem>
+#include "contrib/ankerl/unordered_dense.h"
+#include "contrib/ankerl/svector.h"
+#include "fmt/core.h"
+#include "libutil/cxx/file_util.hxx"
+#include "libutil/cxx/error.hxx"
+#include "hs.h"
+#include "logger.h"
+#include "worker_util.h"
+#include "hyperscan_tools.h"
+
+#include <glob.h> /* for glob */
+#include <unistd.h> /* for unlink */
+#include <optional>
+#include <cstdlib> /* for std::getenv */
+#include "unix-std.h"
+#include "rspamd_control.h"
+
+#define HYPERSCAN_LOG_TAG "hsxxxx"
+
+// Hyperscan does not provide any API to check validity of it's databases
+// However, it is required for us to perform migrations properly without
+// failing at `hs_alloc_scratch` phase or even `hs_scan` which is **way too late**
+// Hence, we have to check hyperscan internal guts to prevent that situation...
+
+#ifdef HS_MAJOR
+#ifndef HS_VERSION_32BIT
+#define HS_VERSION_32BIT ((HS_MAJOR << 24) | (HS_MINOR << 16) | (HS_PATCH << 8) | 0)
+#endif
+#endif// defined(HS_MAJOR)
+
+#if !defined(HS_DB_VERSION) && defined(HS_VERSION_32BIT)
+#define HS_DB_VERSION HS_VERSION_32BIT
+#endif
+
+#ifndef HS_DB_MAGIC
+#define HS_DB_MAGIC (0xdbdbdbdbU)
+#endif
+
+#define msg_info_hyperscan(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ "hyperscan", HYPERSCAN_LOG_TAG, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_hyperscan_lambda(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ "hyperscan", HYPERSCAN_LOG_TAG, \
+ log_func, \
+ __VA_ARGS__)
+#define msg_err_hyperscan(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "hyperscan", HYPERSCAN_LOG_TAG, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_hyperscan(...) rspamd_conditional_debug_fast(nullptr, nullptr, \
+ rspamd_hyperscan_log_id, "hyperscan", HYPERSCAN_LOG_TAG, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_hyperscan_lambda(...) rspamd_conditional_debug_fast(nullptr, nullptr, \
+ rspamd_hyperscan_log_id, "hyperscan", HYPERSCAN_LOG_TAG, \
+ log_func, \
+ __VA_ARGS__)
+
+INIT_LOG_MODULE_PUBLIC(hyperscan)
+
+namespace rspamd::util {
+
+/*
+ * A singleton class that is responsible for deletion of the outdated hyperscan files
+ * One issue is that it must know about HS files in all workers, which is a problem
+ * TODO: we need to export hyperscan caches from all workers to a single place where
+ * we can clean them up (probably, to the main process)
+ */
+class hs_known_files_cache {
+private:
+ // These fields are filled when we add new known cache files
+ ankerl::svector<std::string, 4> cache_dirs;
+ ankerl::svector<std::string, 8> cache_extensions;
+ ankerl::unordered_dense::set<std::string> known_cached_files;
+ bool loaded = false;
+
+private:
+ hs_known_files_cache() = default;
+
+ virtual ~hs_known_files_cache()
+ {
+ // Cleanup cache dir
+ cleanup_maybe();
+ }
+
+public:
+ hs_known_files_cache(const hs_known_files_cache &) = delete;
+ hs_known_files_cache(hs_known_files_cache &&) = delete;
+
+ static auto get() -> hs_known_files_cache &
+ {
+ static hs_known_files_cache *singleton = nullptr;
+
+ if (singleton == nullptr) {
+ singleton = new hs_known_files_cache;
+ }
+
+ return *singleton;
+ }
+
+ void add_cached_file(const raii_file &file)
+ {
+ auto fpath = std::filesystem::path{file.get_name()};
+ std::error_code ec;
+
+ fpath = std::filesystem::canonical(fpath, ec);
+
+ if (ec && ec.value() != 0) {
+ msg_err_hyperscan("invalid path: \"%s\", error message: %s", fpath.c_str(), ec.message().c_str());
+ return;
+ }
+
+ auto dir = fpath.parent_path();
+ auto ext = fpath.extension();
+
+ if (std::find_if(cache_dirs.begin(), cache_dirs.end(),
+ [&](const auto &item) { return item == dir; }) == std::end(cache_dirs)) {
+ cache_dirs.emplace_back(std::string{dir});
+ }
+ if (std::find_if(cache_extensions.begin(), cache_extensions.end(),
+ [&](const auto &item) { return item == ext; }) == std::end(cache_extensions)) {
+ cache_extensions.emplace_back(std::string{ext});
+ }
+
+ auto is_known = known_cached_files.insert(fpath.string());
+ msg_debug_hyperscan("added %s hyperscan file: %s",
+ is_known.second ? "new" : "already known",
+ fpath.c_str());
+ }
+
+ void add_cached_file(const char *fname)
+ {
+ auto fpath = std::filesystem::path{fname};
+ std::error_code ec;
+
+ fpath = std::filesystem::canonical(fpath, ec);
+
+ if (ec && ec.value() != 0) {
+ msg_err_hyperscan("invalid path: \"%s\", error message: %s", fname, ec.message().c_str());
+ return;
+ }
+
+ auto dir = fpath.parent_path();
+ auto ext = fpath.extension();
+
+ if (std::find_if(cache_dirs.begin(), cache_dirs.end(),
+ [&](const auto &item) { return item == dir; }) == std::end(cache_dirs)) {
+ cache_dirs.emplace_back(dir.string());
+ }
+ if (std::find_if(cache_extensions.begin(), cache_extensions.end(),
+ [&](const auto &item) { return item == ext; }) == std::end(cache_extensions)) {
+ cache_extensions.emplace_back(ext.string());
+ }
+
+ auto is_known = known_cached_files.insert(fpath.string());
+ msg_debug_hyperscan("added %s hyperscan file: %s",
+ is_known.second ? "new" : "already known",
+ fpath.c_str());
+ }
+
+ void delete_cached_file(const char *fname)
+ {
+ auto fpath = std::filesystem::path{fname};
+ std::error_code ec;
+
+ fpath = std::filesystem::canonical(fpath, ec);
+
+ if (ec && ec.value() != 0) {
+ msg_err_hyperscan("invalid path to remove: \"%s\", error message: %s",
+ fname, ec.message().c_str());
+ return;
+ }
+
+ if (fpath.empty()) {
+ msg_err_hyperscan("attempt to remove an empty hyperscan file!");
+ return;
+ }
+
+ if (unlink(fpath.c_str()) == -1) {
+ msg_err_hyperscan("cannot remove hyperscan file %s: %s",
+ fpath.c_str(), strerror(errno));
+ }
+ else {
+ msg_debug_hyperscan("removed hyperscan file %s", fpath.c_str());
+ }
+
+ known_cached_files.erase(fpath.string());
+ }
+
+ auto cleanup_maybe() -> void
+ {
+ auto env_cleanup_disable = std::getenv("RSPAMD_NO_CLEANUP");
+ /* We clean dir merely if we are running from the main process */
+ if (rspamd_current_worker == nullptr && env_cleanup_disable == nullptr && loaded) {
+ const auto *log_func = RSPAMD_LOG_FUNC;
+ auto cleanup_dir = [&](std::string_view dir) -> void {
+ for (const auto &ext: cache_extensions) {
+ glob_t globbuf;
+
+ auto glob_pattern = fmt::format("{}{}*{}",
+ dir, G_DIR_SEPARATOR_S, ext);
+ msg_debug_hyperscan_lambda("perform glob for pattern: %s",
+ glob_pattern.c_str());
+ memset(&globbuf, 0, sizeof(globbuf));
+
+ if (glob(glob_pattern.c_str(), 0, nullptr, &globbuf) == 0) {
+ for (auto i = 0; i < globbuf.gl_pathc; i++) {
+ auto path = std::string{globbuf.gl_pathv[i]};
+ std::size_t nsz;
+ struct stat st;
+
+ rspamd_normalize_path_inplace(path.data(), path.size(), &nsz);
+ path.resize(nsz);
+
+ if (stat(path.c_str(), &st) == -1) {
+ msg_debug_hyperscan_lambda("cannot stat file %s: %s",
+ path.c_str(), strerror(errno));
+ continue;
+ }
+
+ if (S_ISREG(st.st_mode)) {
+ if (!known_cached_files.contains(path)) {
+ msg_info_hyperscan_lambda("remove stale hyperscan file %s", path.c_str());
+ unlink(path.c_str());
+ }
+ else {
+ msg_debug_hyperscan_lambda("found known hyperscan file %s, size: %Hz",
+ path.c_str(), st.st_size);
+ }
+ }
+ }
+ }
+
+ globfree(&globbuf);
+ }
+ };
+
+ for (const auto &dir: cache_dirs) {
+ msg_info_hyperscan("cleaning up directory %s", dir.c_str());
+ cleanup_dir(dir);
+ }
+
+ cache_dirs.clear();
+ cache_extensions.clear();
+ known_cached_files.clear();
+ }
+ else if (rspamd_current_worker == nullptr && env_cleanup_disable != nullptr) {
+ msg_info_hyperscan("disable hyperscan cleanup: env variable RSPAMD_NO_CLEANUP is set");
+ }
+ else if (!loaded) {
+ msg_info_hyperscan("disable hyperscan cleanup: not loaded");
+ }
+ }
+
+ auto notice_loaded() -> void
+ {
+ loaded = true;
+ }
+};
+
+
+/**
+ * This is a higher level representation of the cached hyperscan file
+ */
+struct hs_shared_database {
+ hs_database_t *db = nullptr; /**< internal database (might be in a shared memory) */
+ std::optional<raii_mmaped_file> maybe_map;
+ std::string cached_path;
+
+ ~hs_shared_database()
+ {
+ if (!maybe_map) {
+ hs_free_database(db);
+ }
+ // Otherwise, handled by maybe_map dtor
+ }
+
+ explicit hs_shared_database(raii_mmaped_file &&map, hs_database_t *db)
+ : db(db), maybe_map(std::move(map))
+ {
+ cached_path = maybe_map.value().get_file().get_name();
+ }
+ explicit hs_shared_database(hs_database_t *db, const char *fname)
+ : db(db), maybe_map(std::nullopt)
+ {
+ if (fname) {
+ cached_path = fname;
+ }
+ else {
+ /* Likely a test case */
+ cached_path = "";
+ }
+ }
+ hs_shared_database(const hs_shared_database &other) = delete;
+ hs_shared_database() = default;
+ hs_shared_database(hs_shared_database &&other) noexcept
+ {
+ *this = std::move(other);
+ }
+ hs_shared_database &operator=(hs_shared_database &&other) noexcept
+ {
+ std::swap(db, other.db);
+ std::swap(maybe_map, other.maybe_map);
+ return *this;
+ }
+};
+
+struct real_hs_db {
+ std::uint32_t magic;
+ std::uint32_t version;
+ std::uint32_t length;
+ std::uint64_t platform;
+ std::uint32_t crc32;
+};
+static auto
+hs_is_valid_database(void *raw, std::size_t len, std::string_view fname) -> tl::expected<bool, std::string>
+{
+ if (len < sizeof(real_hs_db)) {
+ return tl::make_unexpected(fmt::format("cannot load hyperscan database from {}: too short", fname));
+ }
+
+ static real_hs_db test;
+
+ memcpy(&test, raw, sizeof(test));
+
+ if (test.magic != HS_DB_MAGIC) {
+ return tl::make_unexpected(fmt::format("cannot load hyperscan database from {}: invalid magic: {} ({} expected)",
+ fname, test.magic, HS_DB_MAGIC));
+ }
+
+#ifdef HS_DB_VERSION
+ if (test.version != HS_DB_VERSION) {
+ return tl::make_unexpected(fmt::format("cannot load hyperscan database from {}: invalid version: {} ({} expected)",
+ fname, test.version, HS_DB_VERSION));
+ }
+#endif
+
+ return true;
+}
+
+static auto
+hs_shared_from_unserialized(hs_known_files_cache &hs_cache, raii_mmaped_file &&map) -> tl::expected<hs_shared_database, error>
+{
+ auto ptr = map.get_map();
+ auto db = (hs_database_t *) ptr;
+
+ auto is_valid = hs_is_valid_database(map.get_map(), map.get_size(), map.get_file().get_name());
+ if (!is_valid) {
+ return tl::make_unexpected(error{is_valid.error(), -1, error_category::IMPORTANT});
+ }
+
+ hs_cache.add_cached_file(map.get_file());
+ return tl::expected<hs_shared_database, error>{tl::in_place, std::move(map), db};
+}
+
+static auto
+hs_shared_from_serialized(hs_known_files_cache &hs_cache, raii_mmaped_file &&map, std::int64_t offset) -> tl::expected<hs_shared_database, error>
+{
+ hs_database_t *target = nullptr;
+
+ if (auto ret = hs_deserialize_database((const char *) map.get_map() + offset,
+ map.get_size() - offset, &target);
+ ret != HS_SUCCESS) {
+ return tl::make_unexpected(error{"cannot deserialize database", ret});
+ }
+
+ hs_cache.add_cached_file(map.get_file());
+ return tl::expected<hs_shared_database, error>{tl::in_place, target, map.get_file().get_name().data()};
+}
+
+auto load_cached_hs_file(const char *fname, std::int64_t offset = 0) -> tl::expected<hs_shared_database, error>
+{
+ auto &hs_cache = hs_known_files_cache::get();
+ const auto *log_func = RSPAMD_LOG_FUNC;
+
+ return raii_mmaped_file::mmap_shared(fname, O_RDONLY, PROT_READ, 0)
+ .and_then([&]<class T>(T &&cached_serialized) -> tl::expected<hs_shared_database, error> {
+ if (cached_serialized.get_size() <= offset) {
+ return tl::make_unexpected(error{"Invalid offset", EINVAL, error_category::CRITICAL});
+ }
+#if defined(HS_MAJOR) && defined(HS_MINOR) && HS_MAJOR >= 5 && HS_MINOR >= 4
+ auto unserialized_fname = fmt::format("{}.unser", fname);
+ auto unserialized_file = raii_locked_file::create(unserialized_fname.c_str(), O_CREAT | O_RDWR | O_EXCL,
+ 00644)
+ .and_then([&](auto &&new_file_locked) -> tl::expected<raii_file, error> {
+ auto tmpfile_pattern = fmt::format("{}{}hsmp-XXXXXXXXXXXXXXXXXX",
+ cached_serialized.get_file().get_dir(), G_DIR_SEPARATOR);
+ auto tmpfile = raii_locked_file::mkstemp(tmpfile_pattern.data(), O_CREAT | O_RDWR | O_EXCL,
+ 00644);
+
+ if (!tmpfile) {
+ return tl::make_unexpected(tmpfile.error());
+ }
+ else {
+ auto &tmpfile_checked = tmpfile.value();
+ // Store owned string
+ auto tmpfile_name = std::string{tmpfile_checked.get_name()};
+ std::size_t unserialized_size;
+
+ if (auto ret = hs_serialized_database_size(((const char *) cached_serialized.get_map()) + offset,
+ cached_serialized.get_size() - offset, &unserialized_size);
+ ret != HS_SUCCESS) {
+ return tl::make_unexpected(error{
+ fmt::format("cannot get unserialized database size: {}", ret),
+ EINVAL,
+ error_category::IMPORTANT});
+ }
+
+ msg_debug_hyperscan_lambda("multipattern: create new database in %s; %Hz size",
+ tmpfile_name.c_str(), unserialized_size);
+ void *buf;
+#ifdef HAVE_GETPAGESIZE
+ auto page_size = getpagesize();
+#else
+ auto page_size = sysconf(_SC_PAGESIZE);
+#endif
+ if (page_size == -1) {
+ page_size = 4096;
+ }
+ auto errcode = posix_memalign(&buf, page_size, unserialized_size);
+ if (errcode != 0 || buf == nullptr) {
+ return tl::make_unexpected(error{"Cannot allocate memory",
+ errno, error_category::CRITICAL});
+ }
+
+ if (auto ret = hs_deserialize_database_at(((const char *) cached_serialized.get_map()) + offset,
+ cached_serialized.get_size() - offset, (hs_database_t *) buf);
+ ret != HS_SUCCESS) {
+ return tl::make_unexpected(error{
+ fmt::format("cannot deserialize hyperscan database: {}", ret), ret});
+ }
+ else {
+ if (write(tmpfile_checked.get_fd(), buf, unserialized_size) == -1) {
+ free(buf);
+ return tl::make_unexpected(error{fmt::format("cannot write to {}: {}",
+ tmpfile_name, ::strerror(errno)),
+ errno, error_category::CRITICAL});
+ }
+ else {
+ free(buf);
+ /*
+ * Unlink target file before renaming to avoid
+ * race condition.
+ * So what we have is that `new_file_locked`
+ * will have flock on that file, so it will be
+ * replaced after unlink safely, and also unlocked.
+ */
+ (void) unlink(unserialized_fname.c_str());
+ if (rename(tmpfile_name.c_str(),
+ unserialized_fname.c_str()) == -1) {
+ if (errno != EEXIST) {
+ msg_info_hyperscan_lambda("cannot rename %s -> %s: %s",
+ tmpfile_name.c_str(),
+ unserialized_fname.c_str(),
+ strerror(errno));
+ }
+ }
+ else {
+ /* Unlock file but mark it as immortal first to avoid deletion */
+ tmpfile_checked.make_immortal();
+ (void) tmpfile_checked.unlock();
+ }
+ }
+ }
+ /* Reopen in RO mode */
+ return raii_file::open(unserialized_fname.c_str(), O_RDONLY);
+ };
+ })
+ .or_else([&](auto unused) -> tl::expected<raii_file, error> {
+ // Cannot create file, so try to open it in RO mode
+ return raii_file::open(unserialized_fname.c_str(), O_RDONLY);
+ });
+
+ tl::expected<hs_shared_database, error> ret;
+
+ if (unserialized_file.has_value()) {
+
+ auto &unserialized_checked = unserialized_file.value();
+
+ if (unserialized_checked.get_size() == 0) {
+ /*
+ * This is a case when we have a file that is currently
+ * being created by another process.
+ * We cannot use it!
+ */
+ ret = hs_shared_from_serialized(hs_cache, std::forward<T>(cached_serialized), offset);
+ }
+ else {
+ ret = raii_mmaped_file::mmap_shared(std::move(unserialized_checked), PROT_READ)
+ .and_then([&]<class U>(U &&mmapped_unserialized) -> auto {
+ return hs_shared_from_unserialized(hs_cache, std::forward<U>(mmapped_unserialized));
+ });
+ }
+ }
+ else {
+ ret = hs_shared_from_serialized(hs_cache, std::forward<T>(cached_serialized), offset);
+ }
+#else // defined(HS_MAJOR) && defined(HS_MINOR) && HS_MAJOR >= 5 && HS_MINOR >= 4
+ auto ret = hs_shared_from_serialized(hs_cache, std::forward<T>(cached_serialized), offset);
+#endif// defined(HS_MAJOR) && defined(HS_MINOR) && HS_MAJOR >= 5 && HS_MINOR >= 4 \
+ // Add serialized file to cache merely if we have successfully loaded the actual db
+ if (ret.has_value()) {
+ hs_cache.add_cached_file(cached_serialized.get_file());
+ }
+ return ret;
+ });
+}
+}// namespace rspamd::util
+
+/* C API */
+
+#define CXX_DB_FROM_C(obj) (reinterpret_cast<rspamd::util::hs_shared_database *>(obj))
+#define C_DB_FROM_CXX(obj) (reinterpret_cast<rspamd_hyperscan_t *>(obj))
+
+rspamd_hyperscan_t *
+rspamd_hyperscan_maybe_load(const char *filename, goffset offset)
+{
+ auto maybe_db = rspamd::util::load_cached_hs_file(filename, offset);
+
+ if (maybe_db.has_value()) {
+ auto *ndb = new rspamd::util::hs_shared_database;
+ *ndb = std::move(maybe_db.value());
+ return C_DB_FROM_CXX(ndb);
+ }
+ else {
+ auto error = maybe_db.error();
+
+ switch (error.category) {
+ case rspamd::util::error_category::CRITICAL:
+ msg_err_hyperscan("critical error when trying to load cached hyperscan: %s",
+ error.error_message.data());
+ break;
+ case rspamd::util::error_category::IMPORTANT:
+ msg_info_hyperscan("error when trying to load cached hyperscan: %s",
+ error.error_message.data());
+ break;
+ default:
+ msg_debug_hyperscan("error when trying to load cached hyperscan: %s",
+ error.error_message.data());
+ break;
+ }
+ }
+
+ return nullptr;
+}
+
+hs_database_t *
+rspamd_hyperscan_get_database(rspamd_hyperscan_t *db)
+{
+ auto *real_db = CXX_DB_FROM_C(db);
+ return real_db->db;
+}
+
+rspamd_hyperscan_t *
+rspamd_hyperscan_from_raw_db(hs_database_t *db, const char *fname)
+{
+ auto *ndb = new rspamd::util::hs_shared_database{db, fname};
+
+ return C_DB_FROM_CXX(ndb);
+}
+
+void rspamd_hyperscan_free(rspamd_hyperscan_t *db, bool invalid)
+{
+ auto *real_db = CXX_DB_FROM_C(db);
+
+ if (invalid && !real_db->cached_path.empty()) {
+ rspamd::util::hs_known_files_cache::get().delete_cached_file(real_db->cached_path.c_str());
+ }
+ delete real_db;
+}
+
+void rspamd_hyperscan_notice_known(const char *fname)
+{
+ rspamd::util::hs_known_files_cache::get().add_cached_file(fname);
+
+ if (rspamd_current_worker != nullptr) {
+ /* Also notify main process */
+ struct rspamd_srv_command notice_cmd;
+
+ if (strlen(fname) >= sizeof(notice_cmd.cmd.hyperscan_cache_file.path)) {
+ msg_err("internal error: length of the filename %d ('%s') is larger than control buffer path: %d",
+ (int) strlen(fname), fname, (int) sizeof(notice_cmd.cmd.hyperscan_cache_file.path));
+ }
+ else {
+ notice_cmd.type = RSPAMD_SRV_NOTICE_HYPERSCAN_CACHE;
+ rspamd_strlcpy(notice_cmd.cmd.hyperscan_cache_file.path, fname, sizeof(notice_cmd.cmd.hyperscan_cache_file.path));
+ rspamd_srv_send_command(rspamd_current_worker,
+ rspamd_current_worker->srv->event_loop, &notice_cmd, -1,
+ nullptr,
+ nullptr);
+ }
+ }
+}
+
+void rspamd_hyperscan_cleanup_maybe(void)
+{
+ rspamd::util::hs_known_files_cache::get().cleanup_maybe();
+}
+
+void rspamd_hyperscan_notice_loaded(void)
+{
+ rspamd::util::hs_known_files_cache::get().notice_loaded();
+}
+
+#endif// WITH_HYPERSCAN \ No newline at end of file
diff --git a/src/libserver/hyperscan_tools.h b/src/libserver/hyperscan_tools.h
new file mode 100644
index 0000000..624b7b0
--- /dev/null
+++ b/src/libserver/hyperscan_tools.h
@@ -0,0 +1,77 @@
+/*
+ * 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"
+
+#ifndef RSPAMD_HYPERSCAN_TOOLS_H
+#define RSPAMD_HYPERSCAN_TOOLS_H
+
+#ifdef WITH_HYPERSCAN
+
+#include "hs.h"
+
+G_BEGIN_DECLS
+
+/**
+ * Opaque structure that represents hyperscan (maybe shared/cached database)
+ */
+typedef struct rspamd_hyperscan_s rspamd_hyperscan_t;
+
+/**
+ * Maybe load or mmap shared a hyperscan from a file
+ * @param filename
+ * @return cached database if available
+ */
+rspamd_hyperscan_t *rspamd_hyperscan_maybe_load(const char *filename, goffset offset);
+
+/**
+ * Creates a wrapper for a raw hs db. Ownership is transferred to the enclosing object returned
+ * @param filename
+ * @return
+ */
+rspamd_hyperscan_t *rspamd_hyperscan_from_raw_db(hs_database_t *db, const char *fname);
+/**
+ * Get the internal database
+ * @param db
+ * @return
+ */
+hs_database_t *rspamd_hyperscan_get_database(rspamd_hyperscan_t *db);
+/**
+ * Free the database
+ * @param db
+ */
+void rspamd_hyperscan_free(rspamd_hyperscan_t *db, bool invalid);
+
+/**
+ * Notice a known hyperscan file (e.g. externally serialized)
+ * @param fname
+ */
+void rspamd_hyperscan_notice_known(const char *fname);
+
+/**
+ * Notice that hyperscan files are all loaded (e.g. in the main process), so we can cleanup old files on termination
+ */
+void rspamd_hyperscan_notice_loaded(void);
+
+/**
+ * Cleans up old files. This method should be called on config free (in the main process)
+ */
+void rspamd_hyperscan_cleanup_maybe(void);
+
+G_END_DECLS
+
+#endif
+
+#endif
diff --git a/src/libserver/logger.h b/src/libserver/logger.h
new file mode 100644
index 0000000..8d4e313
--- /dev/null
+++ b/src/libserver/logger.h
@@ -0,0 +1,403 @@
+#ifndef RSPAMD_LOGGER_H
+#define RSPAMD_LOGGER_H
+
+#include "config.h"
+#include "radix.h"
+#include "util.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef G_LOG_LEVEL_USER_SHIFT
+#define G_LOG_LEVEL_USER_SHIFT 8
+#endif
+
+#define RSPAMD_LOG_ID_LEN 6
+
+struct rspamd_config;
+
+enum rspamd_log_flags {
+ RSPAMD_LOG_FORCED = (1 << G_LOG_LEVEL_USER_SHIFT),
+ RSPAMD_LOG_ENCRYPTED = (1 << (G_LOG_LEVEL_USER_SHIFT + 1)),
+ RSPAMD_LOG_LEVEL_MASK = ~(RSPAMD_LOG_FORCED | RSPAMD_LOG_ENCRYPTED)
+};
+
+typedef struct rspamd_logger_s rspamd_logger_t;
+typedef bool (*rspamd_log_func_t)(const gchar *module, const gchar *id,
+ const gchar *function,
+ gint level_flags,
+ const gchar *message,
+ gsize mlen,
+ rspamd_logger_t *logger,
+ gpointer arg);
+typedef void *(*rspamd_log_init_func)(rspamd_logger_t *logger,
+ struct rspamd_config *cfg,
+ uid_t uid, gid_t gid,
+ GError **err);
+typedef bool (*rspamd_log_on_fork_func)(rspamd_logger_t *logger,
+ struct rspamd_config *cfg,
+ gpointer arg,
+ GError **err);
+typedef void *(*rspamd_log_reload_func)(rspamd_logger_t *logger,
+ struct rspamd_config *cfg,
+ gpointer arg,
+ uid_t uid, gid_t gid,
+ GError **err);
+typedef void (*rspamd_log_dtor_func)(rspamd_logger_t *logger,
+ gpointer arg);
+
+struct rspamd_logger_funcs {
+ rspamd_log_init_func init;
+ rspamd_log_reload_func reload;
+ rspamd_log_dtor_func dtor;
+ rspamd_log_func_t log;
+ rspamd_log_on_fork_func on_fork;
+ gpointer specific;
+};
+
+#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64) || defined(_M_X64)
+#define RSPAMD_LOGBUF_SIZE 8192
+#else
+/* Use a smaller buffer */
+#define RSPAMD_LOGBUF_SIZE 2048
+#endif
+
+/**
+ * Opens a new (initial) logger with console type
+ * This logger is also used as an emergency logger
+ * @return new rspamd logger object
+ */
+rspamd_logger_t *rspamd_log_open_emergency(rspamd_mempool_t *pool, gint flags);
+
+/**
+ * Open specific (configured logging)
+ * @param pool
+ * @param config
+ * @param uid
+ * @param gid
+ * @return
+ */
+rspamd_logger_t *rspamd_log_open_specific(rspamd_mempool_t *pool,
+ struct rspamd_config *config,
+ const gchar *ptype,
+ uid_t uid, gid_t gid);
+
+/**
+ * Set log level (from GLogLevelFlags)
+ * @param logger
+ * @param level
+ */
+void rspamd_log_set_log_level(rspamd_logger_t *logger, gint level);
+gint rspamd_log_get_log_level(rspamd_logger_t *logger);
+const gchar *rspamd_get_log_severity_string(gint level_flags);
+/**
+ * Set log flags (from enum rspamd_log_flags)
+ * @param logger
+ * @param flags
+ */
+void rspamd_log_set_log_flags(rspamd_logger_t *logger, gint flags);
+
+/**
+ * Close log file or destroy other structures
+ */
+void rspamd_log_close(rspamd_logger_t *logger);
+
+
+rspamd_logger_t *rspamd_log_default_logger(void);
+rspamd_logger_t *rspamd_log_emergency_logger(void);
+
+/**
+ * Close and open log again for privileged processes
+ */
+bool rspamd_log_reopen(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ uid_t uid, gid_t gid);
+
+/**
+ * Set log pid
+ */
+void rspamd_log_on_fork(GQuark ptype, struct rspamd_config *cfg,
+ rspamd_logger_t *logger);
+
+/**
+ * Log function that is compatible for glib messages
+ */
+void rspamd_glib_log_function(const gchar *log_domain,
+ GLogLevelFlags log_level,
+ const gchar *message,
+ gpointer arg);
+
+/**
+ * Log function for printing glib assertions
+ */
+void rspamd_glib_printerr_function(const gchar *message);
+
+/**
+ * Function with variable number of arguments support
+ */
+bool rspamd_common_log_function(rspamd_logger_t *logger,
+ gint level_flags,
+ const gchar *module, const gchar *id,
+ const gchar *function, const gchar *fmt, ...);
+
+bool rspamd_common_logv(rspamd_logger_t *logger, gint level_flags,
+ const gchar *module, const gchar *id, const gchar *function,
+ const gchar *fmt, va_list args);
+
+/**
+ * Add new logging module, returns module ID
+ * @param mod
+ * @return
+ */
+gint rspamd_logger_add_debug_module(const gchar *mod);
+
+/*
+ * Macro to use for faster debug modules
+ */
+#define INIT_LOG_MODULE(mname) \
+ static gint rspamd_##mname##_log_id = -1; \
+ RSPAMD_CONSTRUCTOR(rspamd_##mname##_log_init) \
+ { \
+ rspamd_##mname##_log_id = rspamd_logger_add_debug_module(#mname); \
+ }
+
+
+#define INIT_LOG_MODULE_PUBLIC(mname) \
+ gint rspamd_##mname##_log_id = -1; \
+ RSPAMD_CONSTRUCTOR(rspamd_##mname##_log_init) \
+ { \
+ rspamd_##mname##_log_id = rspamd_logger_add_debug_module(#mname); \
+ }
+
+#define EXTERN_LOG_MODULE_DEF(mname) \
+ extern gint rspamd_##mname##_log_id
+
+void rspamd_logger_configure_modules(GHashTable *mods_enabled);
+
+/**
+ * Conditional debug function
+ */
+bool rspamd_conditional_debug(rspamd_logger_t *logger,
+ rspamd_inet_addr_t *addr, const gchar *module, const gchar *id,
+ const gchar *function, const gchar *fmt, ...);
+
+bool rspamd_conditional_debug_fast(rspamd_logger_t *logger,
+ rspamd_inet_addr_t *addr,
+ gint mod_id,
+ const gchar *module, const gchar *id,
+ const gchar *function, const gchar *fmt, ...);
+bool rspamd_conditional_debug_fast_num_id(rspamd_logger_t *logger,
+ rspamd_inet_addr_t *addr,
+ gint mod_id,
+ const gchar *module, guint64 id,
+ const gchar *function, const gchar *fmt, ...);
+gboolean rspamd_logger_need_log(rspamd_logger_t *rspamd_log,
+ GLogLevelFlags log_level,
+ gint module_id);
+
+/**
+ * Function with variable number of arguments support that uses static default logger
+ */
+bool rspamd_default_log_function(gint level_flags,
+ const gchar *module, const gchar *id,
+ const gchar *function,
+ const gchar *fmt,
+ ...);
+
+/**
+ * Varargs version of default log function
+ * @param log_level
+ * @param function
+ * @param fmt
+ * @param args
+ */
+bool rspamd_default_logv(gint level_flags,
+ const gchar *module, const gchar *id,
+ const gchar *function,
+ const gchar *fmt,
+ va_list args);
+
+/**
+ * Temporary turn on debug
+ */
+void rspamd_log_debug(rspamd_logger_t *logger);
+
+/**
+ * Turn off debug
+ */
+void rspamd_log_nodebug(rspamd_logger_t *logger);
+
+/**
+ * Return array of counters (4 numbers):
+ * 0 - errors
+ * 1 - warnings
+ * 2 - info messages
+ * 3 - debug messages
+ */
+const guint64 *rspamd_log_counters(rspamd_logger_t *logger);
+
+/**
+ * Returns errors ring buffer as ucl array
+ * @param logger
+ * @return
+ */
+ucl_object_t *rspamd_log_errorbuf_export(const rspamd_logger_t *logger);
+
+/**
+ * Sets new logger functions and initialise logging if needed
+ * @param logger
+ * @param nfuncs
+ * @return static pointer to the old functions (so this function is not reentrant)
+ */
+struct rspamd_logger_funcs *rspamd_logger_set_log_function(rspamd_logger_t *logger,
+ struct rspamd_logger_funcs *nfuncs);
+
+/* Typical functions */
+
+extern guint rspamd_task_log_id;
+#ifdef __cplusplus
+#define RSPAMD_LOG_FUNC __func__
+#else
+#define RSPAMD_LOG_FUNC G_STRFUNC
+#endif
+
+/* Logging in postfix style */
+#define msg_err(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ NULL, NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ NULL, NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ NULL, NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_notice(...) rspamd_default_log_function(G_LOG_LEVEL_MESSAGE, \
+ NULL, NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug(...) rspamd_default_log_function(G_LOG_LEVEL_DEBUG, \
+ NULL, NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+#define debug_task(...) rspamd_conditional_debug_fast(NULL, \
+ task->from_addr, \
+ rspamd_task_log_id, "task", task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+/* Use the following macros if you have `task` in the function */
+#define msg_err_task(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ task->task_pool->tag.tagname, task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_err_task_lambda(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ task->task_pool->tag.tagname, task->task_pool->tag.uid, \
+ log_func, \
+ __VA_ARGS__)
+#define msg_warn_task(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ task->task_pool->tag.tagname, task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_notice_task(...) rspamd_default_log_function(G_LOG_LEVEL_MESSAGE, \
+ task->task_pool->tag.tagname, task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_task(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ task->task_pool->tag.tagname, task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_task_lambda(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ task->task_pool->tag.tagname, task->task_pool->tag.uid, \
+ log_func, \
+ __VA_ARGS__)
+#define msg_debug_task(...) rspamd_conditional_debug_fast(NULL, task->from_addr, \
+ rspamd_task_log_id, "task", task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_task_lambda(...) rspamd_conditional_debug_fast(NULL, task->from_addr, \
+ rspamd_task_log_id, "task", task->task_pool->tag.uid, \
+ log_func, \
+ __VA_ARGS__)
+#define msg_err_task_encrypted(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL | RSPAMD_LOG_ENCRYPTED, \
+ task->task_pool->tag.tagname, task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_task_encrypted(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING | RSPAMD_LOG_ENCRYPTED, \
+ task->task_pool->tag.tagname, task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_notice_task_encrypted(...) rspamd_default_log_function(G_LOG_LEVEL_MESSAGE | RSPAMD_LOG_ENCRYPTED, \
+ task->task_pool->tag.tagname, task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_task_encrypted(...) rspamd_default_log_function(G_LOG_LEVEL_INFO | RSPAMD_LOG_ENCRYPTED, \
+ task->task_pool->tag.tagname, task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+/* Check for NULL pointer first */
+#define msg_err_task_check(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ task ? task->task_pool->tag.tagname : NULL, task ? task->task_pool->tag.uid : NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_task_check(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ task ? task->task_pool->tag.tagname : NULL, task ? task->task_pool->tag.uid : NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_task_check(...) rspamd_default_log_function(G_LOG_LEVEL_MESSAGE, \
+ task ? task->task_pool->tag.tagname : NULL, task ? task->task_pool->tag.uid : NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_notice_task_check(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ task ? task->task_pool->tag.tagname : NULL, task ? task->task_pool->tag.uid : NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_task_check(...) rspamd_conditional_debug_fast(NULL, \
+ task ? task->from_addr : NULL, \
+ rspamd_task_log_id, "task", task ? task->task_pool->tag.uid : NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+/* Use the following macros if you have `pool` in the function */
+#define msg_err_pool(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ pool->tag.tagname, pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_pool(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ pool->tag.tagname, pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_pool(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ pool->tag.tagname, pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_pool(...) rspamd_conditional_debug(NULL, NULL, \
+ pool->tag.tagname, pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+/* Check for NULL pointer first */
+#define msg_err_pool_check(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ pool ? pool->tag.tagname : NULL, pool ? pool->tag.uid : NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_pool_check(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ pool ? pool->tag.tagname : NULL, pool ? pool->tag.uid : NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_pool_check(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ pool ? pool->tag.tagname : NULL, pool ? pool->tag.uid : NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_pool_check(...) rspamd_conditional_debug(NULL, NULL, \
+ pool ? pool->tag.tagname : NULL, pool ? pool->tag.uid : NULL, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/logger/logger.c b/src/libserver/logger/logger.c
new file mode 100644
index 0000000..2dae632
--- /dev/null
+++ b/src/libserver/logger/logger.c
@@ -0,0 +1,1319 @@
+/*
+ * 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 "logger.h"
+#include "rspamd.h"
+#include "libserver/maps/map.h"
+#include "libserver/maps/map_helpers.h"
+#include "ottery.h"
+#include "unix-std.h"
+#include "logger_private.h"
+
+
+static rspamd_logger_t *default_logger = NULL;
+static rspamd_logger_t *emergency_logger = NULL;
+static struct rspamd_log_modules *log_modules = NULL;
+
+static const gchar lf_chr = '\n';
+
+guint rspamd_task_log_id = (guint) -1;
+RSPAMD_CONSTRUCTOR(rspamd_task_log_init)
+{
+ rspamd_task_log_id = rspamd_logger_add_debug_module("task");
+}
+
+rspamd_logger_t *
+rspamd_log_default_logger(void)
+{
+ return default_logger;
+}
+
+rspamd_logger_t *
+rspamd_log_emergency_logger(void)
+{
+ return emergency_logger;
+}
+
+void rspamd_log_set_log_level(rspamd_logger_t *logger, gint level)
+{
+ if (logger == NULL) {
+ logger = default_logger;
+ }
+
+ logger->log_level = level;
+}
+
+gint rspamd_log_get_log_level(rspamd_logger_t *logger)
+{
+ if (logger == NULL) {
+ logger = default_logger;
+ }
+
+ return logger->log_level;
+}
+
+void rspamd_log_set_log_flags(rspamd_logger_t *logger, gint flags)
+{
+ g_assert(logger != NULL);
+
+ logger->flags = flags;
+}
+
+void rspamd_log_close(rspamd_logger_t *logger)
+{
+ g_assert(logger != NULL);
+
+ if (logger->closed) {
+ return;
+ }
+
+ logger->closed = TRUE;
+
+ if (logger->debug_ip) {
+ rspamd_map_helper_destroy_radix(logger->debug_ip);
+ }
+
+ if (logger->pk) {
+ rspamd_pubkey_unref(logger->pk);
+ }
+
+ if (logger->keypair) {
+ rspamd_keypair_unref(logger->keypair);
+ }
+
+ logger->ops.dtor(logger, logger->ops.specific);
+
+ /* TODO: Do we really need that ? */
+ if (logger == default_logger) {
+ default_logger = NULL;
+ }
+
+ if (logger == emergency_logger) {
+ emergency_logger = NULL;
+ }
+
+ if (!logger->pool) {
+ g_free(logger);
+ }
+}
+
+bool rspamd_log_reopen(rspamd_logger_t *rspamd_log, struct rspamd_config *cfg,
+ uid_t uid, gid_t gid)
+{
+ void *nspec;
+ GError *err = NULL;
+
+ g_assert(rspamd_log != NULL);
+
+ nspec = rspamd_log->ops.reload(rspamd_log, cfg, rspamd_log->ops.specific,
+ uid, gid, &err);
+
+ if (nspec != NULL) {
+ rspamd_log->ops.specific = nspec;
+ }
+ else {
+ }
+
+ return nspec != NULL;
+}
+
+static void
+rspamd_emergency_logger_dtor(gpointer d)
+{
+ rspamd_logger_t *logger = (rspamd_logger_t *) d;
+
+ rspamd_log_close(logger);
+}
+
+rspamd_logger_t *
+rspamd_log_open_emergency(rspamd_mempool_t *pool, gint flags)
+{
+ rspamd_logger_t *logger;
+ GError *err = NULL;
+
+ g_assert(default_logger == NULL);
+ g_assert(emergency_logger == NULL);
+
+ if (pool) {
+ logger = rspamd_mempool_alloc0(pool, sizeof(rspamd_logger_t));
+ logger->mtx = rspamd_mempool_get_mutex(pool);
+ }
+ else {
+ logger = g_malloc0(sizeof(rspamd_logger_t));
+ }
+
+ logger->flags = flags;
+ logger->pool = pool;
+ logger->process_type = "main";
+ logger->pid = getpid();
+
+ const struct rspamd_logger_funcs *funcs = &console_log_funcs;
+ memcpy(&logger->ops, funcs, sizeof(*funcs));
+
+ logger->ops.specific = logger->ops.init(logger, NULL, -1, -1, &err);
+
+ if (logger->ops.specific == NULL) {
+ rspamd_fprintf(stderr, "fatal error: cannot init console logging: %e\n",
+ err);
+ g_error_free(err);
+
+ exit(EXIT_FAILURE);
+ }
+
+ default_logger = logger;
+ emergency_logger = logger;
+
+ rspamd_mempool_add_destructor(pool, rspamd_emergency_logger_dtor,
+ emergency_logger);
+
+ return logger;
+}
+
+rspamd_logger_t *
+rspamd_log_open_specific(rspamd_mempool_t *pool,
+ struct rspamd_config *cfg,
+ const gchar *ptype,
+ uid_t uid, gid_t gid)
+{
+ rspamd_logger_t *logger;
+ GError *err = NULL;
+
+ if (pool) {
+ logger = rspamd_mempool_alloc0(pool, sizeof(rspamd_logger_t));
+ logger->mtx = rspamd_mempool_get_mutex(pool);
+ }
+ else {
+ logger = g_malloc0(sizeof(rspamd_logger_t));
+ }
+
+ logger->pool = pool;
+
+ if (cfg) {
+ if (cfg->log_error_elts > 0 && pool) {
+ logger->errlog = rspamd_mempool_alloc0_shared(pool,
+ sizeof(*logger->errlog));
+ logger->errlog->pool = pool;
+ logger->errlog->max_elts = cfg->log_error_elts;
+ logger->errlog->elt_len = cfg->log_error_elt_maxlen;
+ logger->errlog->elts = rspamd_mempool_alloc0_shared(pool,
+ sizeof(struct rspamd_logger_error_elt) * cfg->log_error_elts +
+ cfg->log_error_elt_maxlen * cfg->log_error_elts);
+ }
+
+ logger->log_level = cfg->log_level;
+ logger->flags = cfg->log_flags;
+
+ if (!(logger->flags & RSPAMD_LOG_FLAG_ENFORCED)) {
+ logger->log_level = cfg->log_level;
+ }
+ }
+
+ const struct rspamd_logger_funcs *funcs = NULL;
+
+ if (cfg) {
+ switch (cfg->log_type) {
+ case RSPAMD_LOG_CONSOLE:
+ funcs = &console_log_funcs;
+ break;
+ case RSPAMD_LOG_SYSLOG:
+ funcs = &syslog_log_funcs;
+ break;
+ case RSPAMD_LOG_FILE:
+ funcs = &file_log_funcs;
+ break;
+ }
+ }
+ else {
+ funcs = &console_log_funcs;
+ }
+
+ g_assert(funcs != NULL);
+ memcpy(&logger->ops, funcs, sizeof(*funcs));
+
+ logger->ops.specific = logger->ops.init(logger, cfg, uid, gid, &err);
+
+ if (emergency_logger && logger->ops.specific == NULL) {
+ rspamd_common_log_function(emergency_logger, G_LOG_LEVEL_CRITICAL,
+ "logger", NULL, G_STRFUNC,
+ "cannot open specific logger: %e", err);
+ g_error_free(err);
+
+ return NULL;
+ }
+
+ logger->pid = getpid();
+ logger->process_type = ptype;
+ logger->enabled = TRUE;
+
+ /* Set up conditional logging */
+ if (cfg) {
+ if (cfg->debug_ip_map != NULL) {
+ /* Try to add it as map first of all */
+ if (logger->debug_ip) {
+ rspamd_map_helper_destroy_radix(logger->debug_ip);
+ }
+
+ logger->debug_ip = NULL;
+ rspamd_config_radix_from_ucl(cfg,
+ cfg->debug_ip_map,
+ "IP addresses for which debug logs are enabled",
+ &logger->debug_ip,
+ NULL,
+ NULL, "debug ip");
+ }
+
+ if (cfg->log_encryption_key) {
+ logger->pk = rspamd_pubkey_ref(cfg->log_encryption_key);
+ logger->keypair = rspamd_keypair_new(RSPAMD_KEYPAIR_KEX,
+ RSPAMD_CRYPTOBOX_MODE_25519);
+ rspamd_pubkey_calculate_nm(logger->pk, logger->keypair);
+ }
+ }
+
+ default_logger = logger;
+
+ return logger;
+}
+
+
+/**
+ * Used after fork() for updating structure params
+ */
+void rspamd_log_on_fork(GQuark ptype, struct rspamd_config *cfg,
+ rspamd_logger_t *logger)
+{
+ logger->pid = getpid();
+ logger->process_type = g_quark_to_string(ptype);
+
+ if (logger->ops.on_fork) {
+ GError *err = NULL;
+
+ bool ret = logger->ops.on_fork(logger, cfg, logger->ops.specific, &err);
+
+ if (!ret && emergency_logger) {
+ rspamd_common_log_function(emergency_logger, G_LOG_LEVEL_CRITICAL,
+ "logger", NULL, G_STRFUNC,
+ "cannot update logging on fork: %e", err);
+ g_error_free(err);
+ }
+ }
+}
+
+inline gboolean
+rspamd_logger_need_log(rspamd_logger_t *rspamd_log, GLogLevelFlags log_level,
+ gint module_id)
+{
+ g_assert(rspamd_log != NULL);
+
+ if ((log_level & RSPAMD_LOG_FORCED) ||
+ (log_level & (RSPAMD_LOG_LEVEL_MASK & G_LOG_LEVEL_MASK)) <= rspamd_log->log_level) {
+ return TRUE;
+ }
+
+ if (module_id != -1 && isset(log_modules->bitset, module_id)) {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gchar *
+rspamd_log_encrypt_message(const gchar *begin, const gchar *end, gsize *enc_len,
+ rspamd_logger_t *rspamd_log)
+{
+ guchar *out;
+ gchar *b64;
+ guchar *p, *nonce, *mac;
+ const guchar *comp;
+ guint len, inlen;
+
+ g_assert(end > begin);
+ /* base64 (pubkey | nonce | message) */
+ inlen = rspamd_cryptobox_nonce_bytes(RSPAMD_CRYPTOBOX_MODE_25519) +
+ rspamd_cryptobox_pk_bytes(RSPAMD_CRYPTOBOX_MODE_25519) +
+ rspamd_cryptobox_mac_bytes(RSPAMD_CRYPTOBOX_MODE_25519) +
+ (end - begin);
+ out = g_malloc(inlen);
+
+ p = out;
+ comp = rspamd_pubkey_get_pk(rspamd_log->pk, &len);
+ memcpy(p, comp, len);
+ p += len;
+ ottery_rand_bytes(p, rspamd_cryptobox_nonce_bytes(RSPAMD_CRYPTOBOX_MODE_25519));
+ nonce = p;
+ p += rspamd_cryptobox_nonce_bytes(RSPAMD_CRYPTOBOX_MODE_25519);
+ mac = p;
+ p += rspamd_cryptobox_mac_bytes(RSPAMD_CRYPTOBOX_MODE_25519);
+ memcpy(p, begin, end - begin);
+ comp = rspamd_pubkey_get_nm(rspamd_log->pk, rspamd_log->keypair);
+ g_assert(comp != NULL);
+ rspamd_cryptobox_encrypt_nm_inplace(p, end - begin, nonce, comp, mac,
+ RSPAMD_CRYPTOBOX_MODE_25519);
+ b64 = rspamd_encode_base64(out, inlen, 0, enc_len);
+ g_free(out);
+
+ return b64;
+}
+
+static void
+rspamd_log_write_ringbuffer(rspamd_logger_t *rspamd_log,
+ const gchar *module, const gchar *id,
+ const gchar *data, glong len)
+{
+ guint32 row_num;
+ struct rspamd_logger_error_log *elog;
+ struct rspamd_logger_error_elt *elt;
+
+ if (!rspamd_log->errlog) {
+ return;
+ }
+
+ elog = rspamd_log->errlog;
+
+ g_atomic_int_compare_and_exchange(&elog->cur_row, elog->max_elts, 0);
+#if ((GLIB_MAJOR_VERSION == 2) && (GLIB_MINOR_VERSION > 30))
+ row_num = g_atomic_int_add(&elog->cur_row, 1);
+#else
+ row_num = g_atomic_int_exchange_and_add(&elog->cur_row, 1);
+#endif
+
+ if (row_num < elog->max_elts) {
+ elt = (struct rspamd_logger_error_elt *) (((guchar *) elog->elts) +
+ (sizeof(*elt) + elog->elt_len) * row_num);
+ g_atomic_int_set(&elt->completed, 0);
+ }
+ else {
+ /* Race condition */
+ elog->cur_row = 0;
+ return;
+ }
+
+ elt->pid = rspamd_log->pid;
+ elt->ptype = g_quark_from_string(rspamd_log->process_type);
+ elt->ts = rspamd_get_calendar_ticks();
+
+ if (id) {
+ rspamd_strlcpy(elt->id, id, sizeof(elt->id));
+ }
+ else {
+ rspamd_strlcpy(elt->id, "", sizeof(elt->id));
+ }
+
+ if (module) {
+ rspamd_strlcpy(elt->module, module, sizeof(elt->module));
+ }
+ else {
+ rspamd_strlcpy(elt->module, "", sizeof(elt->module));
+ }
+
+ rspamd_strlcpy(elt->message, data, MIN(len + 1, elog->elt_len));
+ g_atomic_int_set(&elt->completed, 1);
+}
+
+bool rspamd_common_logv(rspamd_logger_t *rspamd_log, gint level_flags,
+ const gchar *module, const gchar *id, const gchar *function,
+ const gchar *fmt, va_list args)
+{
+ gchar *end;
+ gint level = level_flags & (RSPAMD_LOG_LEVEL_MASK & G_LOG_LEVEL_MASK), mod_id;
+ bool ret = false;
+ gchar logbuf[RSPAMD_LOGBUF_SIZE], *log_line;
+ gsize nescaped;
+
+ if (G_UNLIKELY(rspamd_log == NULL)) {
+ rspamd_log = default_logger;
+ }
+
+ log_line = logbuf;
+
+ if (G_UNLIKELY(rspamd_log == NULL)) {
+ /* Just fprintf message to stderr */
+ if (level >= G_LOG_LEVEL_INFO) {
+ end = rspamd_vsnprintf(logbuf, sizeof(logbuf), fmt, args);
+ rspamd_fprintf(stderr, "%*s\n", (gint) (end - log_line),
+ log_line);
+ }
+ }
+ else {
+ if (level == G_LOG_LEVEL_DEBUG) {
+ mod_id = rspamd_logger_add_debug_module(module);
+ }
+ else {
+ mod_id = -1;
+ }
+
+ if (rspamd_logger_need_log(rspamd_log, level_flags, mod_id)) {
+ end = rspamd_vsnprintf(logbuf, sizeof(logbuf), fmt, args);
+
+ if (!(rspamd_log->flags & RSPAMD_LOG_FLAG_RSPAMADM)) {
+ if ((nescaped = rspamd_log_line_need_escape(logbuf, end - logbuf)) != 0) {
+ gsize unescaped_len = end - logbuf;
+ gchar *logbuf_escaped = g_alloca(unescaped_len + nescaped * 4);
+ log_line = logbuf_escaped;
+
+ end = rspamd_log_line_hex_escape(logbuf, unescaped_len,
+ logbuf_escaped, unescaped_len + nescaped * 4);
+ }
+ }
+
+ if ((level_flags & RSPAMD_LOG_ENCRYPTED) && rspamd_log->pk) {
+ gchar *encrypted;
+ gsize enc_len;
+
+ encrypted = rspamd_log_encrypt_message(log_line, end, &enc_len,
+ rspamd_log);
+ ret = rspamd_log->ops.log(module, id,
+ function,
+ level_flags,
+ encrypted,
+ enc_len,
+ rspamd_log,
+ rspamd_log->ops.specific);
+ g_free(encrypted);
+ }
+ else {
+ ret = rspamd_log->ops.log(module, id,
+ function,
+ level_flags,
+ log_line,
+ end - log_line,
+ rspamd_log,
+ rspamd_log->ops.specific);
+ }
+
+ switch (level) {
+ case G_LOG_LEVEL_CRITICAL:
+ rspamd_log->log_cnt[0]++;
+ rspamd_log_write_ringbuffer(rspamd_log, module, id, log_line,
+ end - log_line);
+ break;
+ case G_LOG_LEVEL_WARNING:
+ rspamd_log->log_cnt[1]++;
+ break;
+ case G_LOG_LEVEL_INFO:
+ rspamd_log->log_cnt[2]++;
+ break;
+ case G_LOG_LEVEL_DEBUG:
+ rspamd_log->log_cnt[3]++;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ return ret;
+}
+
+/**
+ * This log functions select real logger and write message if level is less or equal to configured log level
+ */
+bool rspamd_common_log_function(rspamd_logger_t *rspamd_log,
+ gint level_flags,
+ const gchar *module, const gchar *id,
+ const gchar *function,
+ const gchar *fmt,
+ ...)
+{
+ va_list vp;
+
+ va_start(vp, fmt);
+ bool ret = rspamd_common_logv(rspamd_log, level_flags, module, id, function, fmt, vp);
+ va_end(vp);
+
+ return ret;
+}
+
+bool rspamd_default_logv(gint level_flags, const gchar *module, const gchar *id,
+ const gchar *function,
+ const gchar *fmt, va_list args)
+{
+ return rspamd_common_logv(NULL, level_flags, module, id, function, fmt, args);
+}
+
+bool rspamd_default_log_function(gint level_flags,
+ const gchar *module, const gchar *id,
+ const gchar *function, const gchar *fmt, ...)
+{
+
+ va_list vp;
+
+ va_start(vp, fmt);
+ bool ret = rspamd_default_logv(level_flags, module, id, function, fmt, vp);
+ va_end(vp);
+
+ return ret;
+}
+
+
+/**
+ * Main file interface for logging
+ */
+/**
+ * Write log line depending on ip
+ */
+bool rspamd_conditional_debug(rspamd_logger_t *rspamd_log,
+ rspamd_inet_addr_t *addr, const gchar *module, const gchar *id,
+ const gchar *function, const gchar *fmt, ...)
+{
+ static gchar logbuf[LOGBUF_LEN];
+ va_list vp;
+ gchar *end;
+ gint mod_id;
+
+ if (rspamd_log == NULL) {
+ rspamd_log = default_logger;
+ }
+
+ mod_id = rspamd_logger_add_debug_module(module);
+
+ if (rspamd_logger_need_log(rspamd_log, G_LOG_LEVEL_DEBUG, mod_id) ||
+ rspamd_log->is_debug) {
+ if (rspamd_log->debug_ip && addr != NULL) {
+ if (rspamd_match_radix_map_addr(rspamd_log->debug_ip,
+ addr) == NULL) {
+ return false;
+ }
+ }
+
+ va_start(vp, fmt);
+ end = rspamd_vsnprintf(logbuf, sizeof(logbuf), fmt, vp);
+ *end = '\0';
+ va_end(vp);
+ return rspamd_log->ops.log(module, id,
+ function,
+ G_LOG_LEVEL_DEBUG | RSPAMD_LOG_FORCED,
+ logbuf,
+ end - logbuf,
+ rspamd_log,
+ rspamd_log->ops.specific);
+ }
+
+ return false;
+}
+
+bool rspamd_conditional_debug_fast(rspamd_logger_t *rspamd_log,
+ rspamd_inet_addr_t *addr,
+ gint mod_id, const gchar *module, const gchar *id,
+ const gchar *function, const gchar *fmt, ...)
+{
+ static gchar logbuf[LOGBUF_LEN];
+ va_list vp;
+ gchar *end;
+
+ if (rspamd_log == NULL) {
+ rspamd_log = default_logger;
+ }
+
+ if (rspamd_logger_need_log(rspamd_log, G_LOG_LEVEL_DEBUG, mod_id) ||
+ rspamd_log->is_debug) {
+ if (rspamd_log->debug_ip && addr != NULL) {
+ if (rspamd_match_radix_map_addr(rspamd_log->debug_ip, addr) == NULL) {
+ return false;
+ }
+ }
+
+ va_start(vp, fmt);
+ end = rspamd_vsnprintf(logbuf, sizeof(logbuf), fmt, vp);
+ *end = '\0';
+ va_end(vp);
+ return rspamd_log->ops.log(module, id,
+ function,
+ G_LOG_LEVEL_DEBUG | RSPAMD_LOG_FORCED,
+ logbuf,
+ end - logbuf,
+ rspamd_log,
+ rspamd_log->ops.specific);
+ }
+
+ return false;
+}
+
+bool rspamd_conditional_debug_fast_num_id(rspamd_logger_t *rspamd_log,
+ rspamd_inet_addr_t *addr,
+ gint mod_id, const gchar *module, guint64 id,
+ const gchar *function, const gchar *fmt, ...)
+{
+ static gchar logbuf[LOGBUF_LEN], idbuf[64];
+ va_list vp;
+ gchar *end;
+
+ if (rspamd_log == NULL) {
+ rspamd_log = default_logger;
+ }
+
+ if (rspamd_logger_need_log(rspamd_log, G_LOG_LEVEL_DEBUG, mod_id) ||
+ rspamd_log->is_debug) {
+ if (rspamd_log->debug_ip && addr != NULL) {
+ if (rspamd_match_radix_map_addr(rspamd_log->debug_ip, addr) == NULL) {
+ return false;
+ }
+ }
+
+ rspamd_snprintf(idbuf, sizeof(idbuf), "%XuL", id);
+ va_start(vp, fmt);
+ end = rspamd_vsnprintf(logbuf, sizeof(logbuf), fmt, vp);
+ *end = '\0';
+ va_end(vp);
+ return rspamd_log->ops.log(module, idbuf,
+ function,
+ G_LOG_LEVEL_DEBUG | RSPAMD_LOG_FORCED,
+ logbuf,
+ end - logbuf,
+ rspamd_log,
+ rspamd_log->ops.specific);
+ }
+
+ return false;
+}
+
+/**
+ * Wrapper for glib logger
+ */
+void rspamd_glib_log_function(const gchar *log_domain,
+ GLogLevelFlags log_level,
+ const gchar *message,
+ gpointer arg)
+{
+ rspamd_logger_t *rspamd_log = (rspamd_logger_t *) arg;
+
+ if (rspamd_log->enabled &&
+ rspamd_logger_need_log(rspamd_log, log_level, -1)) {
+ rspamd_log->ops.log("glib", NULL,
+ NULL,
+ log_level,
+ message,
+ strlen(message),
+ rspamd_log,
+ rspamd_log->ops.specific);
+ }
+}
+
+void rspamd_glib_printerr_function(const gchar *message)
+{
+ rspamd_common_log_function(NULL, G_LOG_LEVEL_CRITICAL, "glib",
+ NULL, G_STRFUNC,
+ "%s", message);
+}
+
+/**
+ * Temporary turn on debugging
+ */
+void rspamd_log_debug(rspamd_logger_t *rspamd_log)
+{
+ rspamd_log->is_debug = TRUE;
+}
+
+/**
+ * Turn off temporary debugging
+ */
+void rspamd_log_nodebug(rspamd_logger_t *rspamd_log)
+{
+ rspamd_log->is_debug = FALSE;
+}
+
+const guint64 *
+rspamd_log_counters(rspamd_logger_t *logger)
+{
+ if (logger) {
+ return logger->log_cnt;
+ }
+
+ return NULL;
+}
+
+static gint
+rspamd_log_errlog_cmp(const ucl_object_t **o1, const ucl_object_t **o2)
+{
+ const ucl_object_t *ts1, *ts2;
+
+ ts1 = ucl_object_lookup(*o1, "ts");
+ ts2 = ucl_object_lookup(*o2, "ts");
+
+ if (ts1 && ts2) {
+ gdouble t1 = ucl_object_todouble(ts1), t2 = ucl_object_todouble(ts2);
+
+ if (t1 > t2) {
+ return -1;
+ }
+ else if (t2 > t1) {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+ucl_object_t *
+rspamd_log_errorbuf_export(const rspamd_logger_t *logger)
+{
+ struct rspamd_logger_error_elt *cpy, *cur;
+ ucl_object_t *top = ucl_object_typed_new(UCL_ARRAY);
+ guint i;
+
+ if (logger->errlog == NULL) {
+ return top;
+ }
+
+ cpy = g_malloc0_n(logger->errlog->max_elts,
+ sizeof(*cpy) + logger->errlog->elt_len);
+ memcpy(cpy, logger->errlog->elts, logger->errlog->max_elts * (sizeof(*cpy) + logger->errlog->elt_len));
+
+ for (i = 0; i < logger->errlog->max_elts; i++) {
+ cur = (struct rspamd_logger_error_elt *) ((guchar *) cpy +
+ i * ((sizeof(*cpy) + logger->errlog->elt_len)));
+ if (cur->completed) {
+ ucl_object_t *obj = ucl_object_typed_new(UCL_OBJECT);
+
+ ucl_object_insert_key(obj, ucl_object_fromdouble(cur->ts),
+ "ts", 0, false);
+ ucl_object_insert_key(obj, ucl_object_fromint(cur->pid),
+ "pid", 0, false);
+ ucl_object_insert_key(obj,
+ ucl_object_fromstring(g_quark_to_string(cur->ptype)),
+ "type", 0, false);
+ ucl_object_insert_key(obj, ucl_object_fromstring(cur->id),
+ "id", 0, false);
+ ucl_object_insert_key(obj, ucl_object_fromstring(cur->module),
+ "module", 0, false);
+ ucl_object_insert_key(obj, ucl_object_fromstring(cur->message),
+ "message", 0, false);
+
+ ucl_array_append(top, obj);
+ }
+ }
+
+ ucl_object_array_sort(top, rspamd_log_errlog_cmp);
+ g_free(cpy);
+
+ return top;
+}
+
+static guint
+rspamd_logger_allocate_mod_bit(void)
+{
+ if (log_modules->bitset_allocated * NBBY > log_modules->bitset_len + 1) {
+ log_modules->bitset_len++;
+ return log_modules->bitset_len - 1;
+ }
+ else {
+ /* Need to expand */
+ log_modules->bitset_allocated *= 2;
+ log_modules->bitset = g_realloc(log_modules->bitset,
+ log_modules->bitset_allocated);
+
+ return rspamd_logger_allocate_mod_bit();
+ }
+}
+
+RSPAMD_DESTRUCTOR(rspamd_debug_modules_dtor)
+{
+ if (log_modules) {
+ g_hash_table_unref(log_modules->modules);
+ g_free(log_modules->bitset);
+ g_free(log_modules);
+ }
+}
+
+gint rspamd_logger_add_debug_module(const gchar *mname)
+{
+ struct rspamd_log_module *m;
+
+ if (mname == NULL) {
+ return -1;
+ }
+
+ if (log_modules == NULL) {
+ /*
+ * This is usually called from constructors, so we call init check
+ * each time to avoid dependency issues between ctors calls
+ */
+ log_modules = g_malloc0(sizeof(*log_modules));
+ log_modules->modules = g_hash_table_new_full(rspamd_strcase_hash,
+ rspamd_strcase_equal, g_free, g_free);
+ log_modules->bitset_allocated = 16;
+ log_modules->bitset_len = 0;
+ log_modules->bitset = g_malloc0(log_modules->bitset_allocated);
+ }
+
+ if ((m = g_hash_table_lookup(log_modules->modules, mname)) == NULL) {
+ m = g_malloc0(sizeof(*m));
+ m->mname = g_strdup(mname);
+ m->id = rspamd_logger_allocate_mod_bit();
+ clrbit(log_modules->bitset, m->id);
+ g_hash_table_insert(log_modules->modules, m->mname, m);
+ }
+
+ return m->id;
+}
+
+void rspamd_logger_configure_modules(GHashTable *mods_enabled)
+{
+ GHashTableIter it;
+ gpointer k, v;
+ guint id;
+
+ /* Clear all in bitset_allocated -> this are bytes not bits */
+ memset(log_modules->bitset, 0, log_modules->bitset_allocated);
+ /* On first iteration, we go through all modules enabled and add missing ones */
+ g_hash_table_iter_init(&it, mods_enabled);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ rspamd_logger_add_debug_module((const gchar *) k);
+ }
+
+ g_hash_table_iter_init(&it, mods_enabled);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ id = rspamd_logger_add_debug_module((const gchar *) k);
+
+ if (isclr(log_modules->bitset, id)) {
+ msg_info("enable debugging for module %s (%d)", (const gchar *) k,
+ id);
+ setbit(log_modules->bitset, id);
+ }
+ }
+}
+
+struct rspamd_logger_funcs *
+rspamd_logger_set_log_function(rspamd_logger_t *logger,
+ struct rspamd_logger_funcs *nfuncs)
+{
+ /* TODO: write this */
+
+ return NULL;
+}
+
+
+gchar *
+rspamd_log_line_hex_escape(const guchar *src, gsize srclen,
+ gchar *dst, gsize dstlen)
+{
+ static const gchar hexdigests[16] = "0123456789ABCDEF";
+ gchar *d = dst;
+
+ static guint32 escape[] = {
+ 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */
+
+ /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */
+ 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0100 */
+
+ /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */
+ 0x00000000, /* 0001 0000 0000 0000 0000 0000 0000 0000 */
+
+ /* ~}| {zyx wvut srqp onml kjih gfed cba` */
+ 0x80000000, /* 1000 0000 0000 0000 0000 0000 0000 0000 */
+
+ /* Allow all 8bit characters (assuming they are valid utf8) */
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ };
+
+ while (srclen && dstlen) {
+ if (escape[*src >> 5] & (1U << (*src & 0x1f))) {
+ if (dstlen >= 4) {
+ *d++ = '\\';
+ *d++ = 'x';
+ *d++ = hexdigests[*src >> 4];
+ *d++ = hexdigests[*src & 0xf];
+ src++;
+ dstlen -= 4;
+ }
+ else {
+ /* Overflow */
+ break;
+ }
+ }
+ else {
+ *d++ = *src++;
+ dstlen--;
+ }
+
+ srclen--;
+ }
+
+ return d;
+}
+
+gsize rspamd_log_line_need_escape(const guchar *src, gsize srclen)
+{
+ static guint32 escape[] = {
+ 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */
+
+ /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */
+ 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0100 */
+
+ /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */
+ 0x00000000, /* 0001 0000 0000 0000 0000 0000 0000 0000 */
+
+ /* ~}| {zyx wvut srqp onml kjih gfed cba` */
+ 0x80000000, /* 1000 0000 0000 0000 0000 0000 0000 0000 */
+
+ /* Allow all 8bit characters (assuming they are valid utf8) */
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ };
+ gsize n = 0;
+
+ while (srclen) {
+ if (escape[*src >> 5] & (1U << (*src & 0x1f))) {
+ n++;
+ }
+
+ src++;
+ srclen--;
+ }
+
+ return n;
+}
+
+const gchar *
+rspamd_get_log_severity_string(gint level_flags)
+{
+ unsigned int bitnum;
+ static const char *level_strs[G_LOG_LEVEL_USER_SHIFT] = {
+ "", /* G_LOG_FLAG_RECURSION */
+ "", /* G_LOG_FLAG_FATAL */
+ "crit",
+ "error",
+ "warn",
+ "notice",
+ "info",
+ "debug"};
+ level_flags &= ((1u << G_LOG_LEVEL_USER_SHIFT) - 1u) & ~(G_LOG_FLAG_RECURSION | G_LOG_FLAG_FATAL);
+#ifdef __GNUC__
+ /* We assume gcc >= 3 and clang >= 5 anyway */
+ bitnum = __builtin_ffs(level_flags) - 1;
+#else
+ bitnum = ffs(level_flags) - 1;
+#endif
+ return level_strs[bitnum];
+}
+
+static inline void
+log_time(gdouble now, rspamd_logger_t *rspamd_log, gchar *timebuf,
+ size_t len)
+{
+ time_t sec = (time_t) now;
+ gsize r;
+ struct tm tms;
+
+ rspamd_localtime(sec, &tms);
+ r = strftime(timebuf, len, "%F %H:%M:%S", &tms);
+
+ if (rspamd_log->flags & RSPAMD_LOG_FLAG_USEC) {
+ gchar usec_buf[16];
+
+ rspamd_snprintf(usec_buf, sizeof(usec_buf), "%.5f",
+ now - (gdouble) sec);
+ rspamd_snprintf(timebuf + r, len - r,
+ "%s", usec_buf + 1);
+ }
+}
+
+void rspamd_log_fill_iov(struct rspamd_logger_iov_ctx *iov_ctx,
+ double ts,
+ const gchar *module,
+ const gchar *id,
+ const gchar *function,
+ gint level_flags,
+ const gchar *message,
+ gsize mlen,
+ rspamd_logger_t *logger)
+{
+ bool log_color = (logger->flags & RSPAMD_LOG_FLAG_COLOR);
+ bool log_severity = (logger->flags & RSPAMD_LOG_FLAG_SEVERITY);
+ bool log_rspamadm = (logger->flags & RSPAMD_LOG_FLAG_RSPAMADM);
+ bool log_systemd = (logger->flags & RSPAMD_LOG_FLAG_SYSTEMD);
+ bool log_json = (logger->flags & RSPAMD_LOG_FLAG_JSON);
+
+ if (log_json) {
+ /* Some sanity to avoid too many branches */
+ log_color = false;
+ log_severity = true;
+ log_systemd = false;
+ }
+
+ glong r;
+ static gchar timebuf[64], modulebuf[64];
+ static gchar tmpbuf[256];
+
+ if (!log_json && !log_systemd) {
+ log_time(ts, logger, timebuf, sizeof(timebuf));
+ }
+
+ if (G_UNLIKELY(log_json)) {
+ /* Perform JSON logging */
+ guint slen = id ? strlen(id) : strlen("(NULL)");
+ slen = MIN(RSPAMD_LOG_ID_LEN, slen);
+ r = rspamd_snprintf(tmpbuf, sizeof(tmpbuf), "{\"ts\": %f, "
+ "\"pid\": %P, "
+ "\"severity\": \"%s\", "
+ "\"worker_type\": \"%s\", "
+ "\"id\": \"%*.s\", "
+ "\"module\": \"%s\", "
+ "\"function\": \"%s\", "
+ "\"message\": \"",
+ ts,
+ logger->pid,
+ rspamd_get_log_severity_string(level_flags),
+ logger->process_type,
+ slen, id,
+ module,
+ function);
+ iov_ctx->iov[0].iov_base = tmpbuf;
+ iov_ctx->iov[0].iov_len = r;
+ /* TODO: is it possible to have other 'bad' symbols here? */
+ if (rspamd_memcspn(message, "\"\\\r\n\b\t\v", mlen) == mlen) {
+ iov_ctx->iov[1].iov_base = (void *) message;
+ iov_ctx->iov[1].iov_len = mlen;
+ }
+ else {
+ /* We need to do JSON escaping of the quotes */
+ const char *p, *end = message + mlen;
+ long escaped_len;
+
+ for (p = message, escaped_len = 0; p < end; p++, escaped_len++) {
+ switch (*p) {
+ case '\v':
+ case '\0':
+ escaped_len += 5;
+ break;
+ case '\\':
+ case '"':
+ case '\n':
+ case '\r':
+ case '\b':
+ case '\t':
+ escaped_len++;
+ break;
+ default:
+ break;
+ }
+ }
+
+
+ struct rspamd_logger_iov_thrash_stack *thrash_stack_elt = g_malloc(
+ sizeof(struct rspamd_logger_iov_thrash_stack) +
+ escaped_len);
+
+ char *dst = ((char *) thrash_stack_elt) + sizeof(struct rspamd_logger_iov_thrash_stack);
+ char *d;
+
+ thrash_stack_elt->prev = iov_ctx->thrash_stack;
+ iov_ctx->thrash_stack = thrash_stack_elt;
+
+ for (p = message, d = dst; p < end; p++, d++) {
+ switch (*p) {
+ case '\n':
+ *d++ = '\\';
+ *d = 'n';
+ break;
+ case '\r':
+ *d++ = '\\';
+ *d = 'r';
+ break;
+ case '\b':
+ *d++ = '\\';
+ *d = 'b';
+ break;
+ case '\t':
+ *d++ = '\\';
+ *d = 't';
+ break;
+ case '\f':
+ *d++ = '\\';
+ *d = 'f';
+ break;
+ case '\0':
+ *d++ = '\\';
+ *d++ = 'u';
+ *d++ = '0';
+ *d++ = '0';
+ *d++ = '0';
+ *d = '0';
+ break;
+ case '\v':
+ *d++ = '\\';
+ *d++ = 'u';
+ *d++ = '0';
+ *d++ = '0';
+ *d++ = '0';
+ *d = 'B';
+ break;
+ case '\\':
+ *d++ = '\\';
+ *d = '\\';
+ break;
+ case '"':
+ *d++ = '\\';
+ *d = '"';
+ break;
+ default:
+ *d = *p;
+ break;
+ }
+ }
+
+ iov_ctx->iov[1].iov_base = dst;
+ iov_ctx->iov[1].iov_len = d - dst;
+ }
+ iov_ctx->iov[2].iov_base = (void *) "\"}\n";
+ iov_ctx->iov[2].iov_len = sizeof("\"}\n") - 1;
+
+ iov_ctx->niov = 3;
+ }
+ else if (G_LIKELY(!log_rspamadm)) {
+ if (!log_systemd) {
+ if (log_color) {
+ if (level_flags & (G_LOG_LEVEL_INFO | G_LOG_LEVEL_MESSAGE)) {
+ /* White */
+ r = rspamd_snprintf(tmpbuf, sizeof(tmpbuf), "\033[0;37m");
+ }
+ else if (level_flags & G_LOG_LEVEL_WARNING) {
+ /* Magenta */
+ r = rspamd_snprintf(tmpbuf, sizeof(tmpbuf), "\033[0;32m");
+ }
+ else if (level_flags & G_LOG_LEVEL_CRITICAL) {
+ /* Red */
+ r = rspamd_snprintf(tmpbuf, sizeof(tmpbuf), "\033[1;31m");
+ }
+ else {
+ r = 0;
+ }
+ }
+ else {
+ r = 0;
+ }
+
+ if (log_severity) {
+ r += rspamd_snprintf(tmpbuf + r,
+ sizeof(tmpbuf) - r,
+ "%s [%s] #%P(%s) ",
+ timebuf,
+ rspamd_get_log_severity_string(level_flags),
+ logger->pid,
+ logger->process_type);
+ }
+ else {
+ r += rspamd_snprintf(tmpbuf + r,
+ sizeof(tmpbuf) - r,
+ "%s #%P(%s) ",
+ timebuf,
+ logger->pid,
+ logger->process_type);
+ }
+ }
+ else {
+ r = 0;
+ r += rspamd_snprintf(tmpbuf + r,
+ sizeof(tmpbuf) - r,
+ "(%s) ",
+ logger->process_type);
+ }
+
+ glong mremain, mr;
+ char *m;
+
+ modulebuf[0] = '\0';
+ mremain = sizeof(modulebuf);
+ m = modulebuf;
+
+ if (id != NULL) {
+ guint slen = strlen(id);
+ slen = MIN(RSPAMD_LOG_ID_LEN, slen);
+ mr = rspamd_snprintf(m, mremain, "<%*.s>; ", slen,
+ id);
+ m += mr;
+ mremain -= mr;
+ }
+ if (module != NULL) {
+ mr = rspamd_snprintf(m, mremain, "%s; ", module);
+ m += mr;
+ mremain -= mr;
+ }
+ if (function != NULL) {
+ mr = rspamd_snprintf(m, mremain, "%s: ", function);
+ m += mr;
+ mremain -= mr;
+ }
+ else {
+ mr = rspamd_snprintf(m, mremain, ": ");
+ m += mr;
+ mremain -= mr;
+ }
+
+ /* Ensure that we have a space at the end */
+ if (m > modulebuf && *(m - 1) != ' ') {
+ *(m - 1) = ' ';
+ }
+
+ /* Construct IOV for log line */
+ iov_ctx->iov[0].iov_base = tmpbuf;
+ iov_ctx->iov[0].iov_len = r;
+ iov_ctx->iov[1].iov_base = modulebuf;
+ iov_ctx->iov[1].iov_len = m - modulebuf;
+ iov_ctx->iov[2].iov_base = (void *) message;
+ iov_ctx->iov[2].iov_len = mlen;
+ iov_ctx->iov[3].iov_base = (void *) &lf_chr;
+ iov_ctx->iov[3].iov_len = 1;
+
+ iov_ctx->niov = 4;
+
+ if (log_color) {
+ iov_ctx->iov[4].iov_base = "\033[0m";
+ iov_ctx->iov[4].iov_len = sizeof("\033[0m") - 1;
+
+ iov_ctx->niov = 5;
+ }
+ }
+ else {
+ /* Rspamadm case */
+ int niov = 0;
+
+ if (logger->log_level == G_LOG_LEVEL_DEBUG) {
+ iov_ctx->iov[niov].iov_base = (void *) timebuf;
+ iov_ctx->iov[niov++].iov_len = strlen(timebuf);
+ iov_ctx->iov[niov].iov_base = (void *) " ";
+ iov_ctx->iov[niov++].iov_len = 1;
+ }
+
+ iov_ctx->iov[niov].iov_base = (void *) message;
+ iov_ctx->iov[niov++].iov_len = mlen;
+ iov_ctx->iov[niov].iov_base = (void *) &lf_chr;
+ iov_ctx->iov[niov++].iov_len = 1;
+
+ iov_ctx->niov = niov;
+ }
+
+ // this is kind of "after-the-fact" check, but it's mostly for debugging-only
+ g_assert(iov_ctx->niov <= G_N_ELEMENTS(iov_ctx->iov));
+}
+
+void rspamd_log_iov_free(struct rspamd_logger_iov_ctx *iov_ctx)
+{
+ struct rspamd_logger_iov_thrash_stack *st = iov_ctx->thrash_stack;
+
+ while (st) {
+ struct rspamd_logger_iov_thrash_stack *nst = st->prev;
+ g_free(st);
+ st = nst;
+ }
+} \ No newline at end of file
diff --git a/src/libserver/logger/logger_console.c b/src/libserver/logger/logger_console.c
new file mode 100644
index 0000000..7f3c770
--- /dev/null
+++ b/src/libserver/logger/logger_console.c
@@ -0,0 +1,211 @@
+/*
+ * 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 "logger.h"
+#include "libserver/cfg_file.h"
+#include "libcryptobox/cryptobox.h"
+#include "unix-std.h"
+
+#include "logger_private.h"
+
+#define CONSOLE_LOG_QUARK g_quark_from_static_string("console_logger")
+
+static const gchar lf_chr = '\n';
+struct rspamd_console_logger_priv {
+ gint fd;
+ gint crit_fd;
+};
+
+/* Copy & paste :( */
+static inline void
+log_time(gdouble now, rspamd_logger_t *rspamd_log, gchar *timebuf,
+ size_t len)
+{
+ time_t sec = (time_t) now;
+ gsize r;
+ struct tm tms;
+
+ rspamd_localtime(sec, &tms);
+ r = strftime(timebuf, len, "%F %H:%M:%S", &tms);
+
+ if (rspamd_log->flags & RSPAMD_LOG_FLAG_USEC) {
+ gchar usec_buf[16];
+
+ rspamd_snprintf(usec_buf, sizeof(usec_buf), "%.5f",
+ now - (gdouble) sec);
+ rspamd_snprintf(timebuf + r, len - r,
+ "%s", usec_buf + 1);
+ }
+}
+
+void *
+rspamd_log_console_init(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ uid_t uid, gid_t gid, GError **err)
+{
+ struct rspamd_console_logger_priv *priv;
+
+ priv = g_malloc0(sizeof(*priv));
+
+ if (logger->flags & RSPAMD_LOG_FLAG_RSPAMADM) {
+ priv->fd = dup(STDOUT_FILENO);
+ priv->crit_fd = dup(STDERR_FILENO);
+ }
+ else {
+ priv->fd = dup(STDERR_FILENO);
+ priv->crit_fd = priv->fd;
+ }
+
+ if (priv->fd == -1) {
+ g_set_error(err, CONSOLE_LOG_QUARK, errno,
+ "open_log: cannot dup console fd: %s\n",
+ strerror(errno));
+ rspamd_log_console_dtor(logger, priv);
+
+ return NULL;
+ }
+
+ if (!isatty(priv->fd)) {
+ if (logger->flags & RSPAMD_LOG_FLAG_COLOR) {
+ /* Disable colors for not a tty */
+ logger->flags &= ~RSPAMD_LOG_FLAG_COLOR;
+ }
+ }
+
+ return priv;
+}
+
+void *
+rspamd_log_console_reload(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ gpointer arg, uid_t uid, gid_t gid, GError **err)
+{
+ struct rspamd_console_logger_priv *npriv;
+
+ npriv = rspamd_log_console_init(logger, cfg, uid, gid, err);
+
+ if (npriv) {
+ /* Close old */
+ rspamd_log_console_dtor(logger, arg);
+ }
+
+ return npriv;
+}
+
+void rspamd_log_console_dtor(rspamd_logger_t *logger, gpointer arg)
+{
+ struct rspamd_console_logger_priv *priv = (struct rspamd_console_logger_priv *) arg;
+
+ if (priv->fd != -1) {
+ if (priv->fd != priv->crit_fd) {
+ /* Two different FD case */
+ if (close(priv->crit_fd) == -1) {
+ rspamd_fprintf(stderr, "cannot close log crit_fd %d: %s\n",
+ priv->crit_fd, strerror(errno));
+ }
+ }
+
+ if (close(priv->fd) == -1) {
+ rspamd_fprintf(stderr, "cannot close log fd %d: %s\n",
+ priv->fd, strerror(errno));
+ }
+
+ /* Avoid the next if to be executed as crit_fd is equal to fd */
+ priv->crit_fd = -1;
+ }
+
+ if (priv->crit_fd != -1) {
+ if (close(priv->crit_fd) == -1) {
+ rspamd_fprintf(stderr, "cannot close log crit_fd %d: %s\n",
+ priv->crit_fd, strerror(errno));
+ }
+ }
+
+ g_free(priv);
+}
+
+bool rspamd_log_console_log(const gchar *module, const gchar *id,
+ const gchar *function,
+ gint level_flags,
+ const gchar *message,
+ gsize mlen,
+ rspamd_logger_t *rspamd_log,
+ gpointer arg)
+{
+ struct rspamd_console_logger_priv *priv = (struct rspamd_console_logger_priv *) arg;
+ gint fd, r;
+ double now;
+
+ if (level_flags & G_LOG_LEVEL_CRITICAL) {
+ fd = priv->crit_fd;
+ }
+ else {
+ /* Use stderr if we are in rspamadm mode and severity is more than WARNING */
+ if ((rspamd_log->flags & RSPAMD_LOG_FLAG_RSPAMADM) && (level_flags & G_LOG_LEVEL_WARNING)) {
+ fd = priv->crit_fd;
+ }
+ else {
+ fd = priv->fd;
+ }
+ }
+
+#ifndef DISABLE_PTHREAD_MUTEX
+ if (rspamd_log->mtx) {
+ rspamd_mempool_lock_mutex(rspamd_log->mtx);
+ }
+ else {
+ rspamd_file_lock(fd, FALSE);
+ }
+#else
+ rspamd_file_lock(fd, FALSE);
+#endif
+
+ now = rspamd_get_calendar_ticks();
+
+ struct rspamd_logger_iov_ctx iov_ctx;
+ memset(&iov_ctx, 0, sizeof(iov_ctx));
+ rspamd_log_fill_iov(&iov_ctx, now, module, id,
+ function, level_flags, message,
+ mlen, rspamd_log);
+
+again:
+ r = writev(fd, iov_ctx.iov, iov_ctx.niov);
+
+ if (r == -1) {
+ if (errno == EAGAIN || errno == EINTR) {
+ goto again;
+ }
+
+ if (rspamd_log->mtx) {
+ rspamd_mempool_unlock_mutex(rspamd_log->mtx);
+ }
+ else {
+ rspamd_file_unlock(fd, FALSE);
+ }
+
+ rspamd_log_iov_free(&iov_ctx);
+ return false;
+ }
+
+ if (rspamd_log->mtx) {
+ rspamd_mempool_unlock_mutex(rspamd_log->mtx);
+ }
+ else {
+ rspamd_file_unlock(fd, FALSE);
+ }
+
+ rspamd_log_iov_free(&iov_ctx);
+ return true;
+} \ No newline at end of file
diff --git a/src/libserver/logger/logger_file.c b/src/libserver/logger/logger_file.c
new file mode 100644
index 0000000..20b04b8
--- /dev/null
+++ b/src/libserver/logger/logger_file.c
@@ -0,0 +1,510 @@
+/*
+ * 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 "logger.h"
+#include "libserver/cfg_file.h"
+#include "libcryptobox/cryptobox.h"
+#include "unix-std.h"
+
+#include "logger_private.h"
+
+#define FILE_LOG_QUARK g_quark_from_static_string("file_logger")
+
+struct rspamd_file_logger_priv {
+ gint fd;
+ struct {
+ guint32 size;
+ guint32 used;
+ u_char *buf;
+ } io_buf;
+ gboolean throttling;
+ gchar *log_file;
+ gboolean is_buffered;
+ gboolean log_severity;
+ time_t throttling_time;
+ guint32 repeats;
+ guint64 last_line_cksum;
+ gchar *saved_message;
+ gsize saved_mlen;
+ gchar *saved_function;
+ gchar *saved_module;
+ gchar *saved_id;
+ guint saved_loglevel;
+};
+
+/**
+ * Calculate checksum for log line (used for repeating logic)
+ */
+static inline guint64
+rspamd_log_calculate_cksum(const gchar *message, size_t mlen)
+{
+ return rspamd_cryptobox_fast_hash(message, mlen, rspamd_hash_seed());
+}
+
+/*
+ * Write a line to log file (unbuffered)
+ */
+static bool
+direct_write_log_line(rspamd_logger_t *rspamd_log,
+ struct rspamd_file_logger_priv *priv,
+ void *data,
+ gsize count,
+ gboolean is_iov,
+ gint level_flags)
+{
+ struct iovec *iov;
+ const gchar *line;
+ glong r;
+ gint fd;
+ gboolean locked = FALSE;
+
+ iov = (struct iovec *) data;
+ fd = priv->fd;
+
+ if (!rspamd_log->no_lock) {
+ gsize tlen;
+
+ if (is_iov) {
+ tlen = 0;
+
+ for (guint i = 0; i < count; i++) {
+ tlen += iov[i].iov_len;
+ }
+ }
+ else {
+ tlen = count;
+ }
+
+ if (tlen > PIPE_BUF) {
+ locked = TRUE;
+
+#ifndef DISABLE_PTHREAD_MUTEX
+ if (rspamd_log->mtx) {
+ rspamd_mempool_lock_mutex(rspamd_log->mtx);
+ }
+ else {
+ rspamd_file_lock(fd, FALSE);
+ }
+#else
+ rspamd_file_lock(fd, FALSE);
+#endif
+ }
+ }
+
+ if (is_iov) {
+ r = writev(fd, iov, count);
+ }
+ else {
+ line = (const gchar *) data;
+ r = write(fd, line, count);
+ }
+
+ if (locked) {
+#ifndef DISABLE_PTHREAD_MUTEX
+ if (rspamd_log->mtx) {
+ rspamd_mempool_unlock_mutex(rspamd_log->mtx);
+ }
+ else {
+ rspamd_file_unlock(fd, FALSE);
+ }
+#else
+ rspamd_file_unlock(fd, FALSE);
+#endif
+ }
+
+ if (r == -1) {
+ /* We cannot write message to file, so we need to detect error and make decision */
+ if (errno == EINTR) {
+ /* Try again */
+ return direct_write_log_line(rspamd_log, priv, data, count, is_iov, level_flags);
+ }
+
+ if (errno == EFAULT || errno == EINVAL || errno == EFBIG ||
+ errno == ENOSPC) {
+ /* Rare case */
+ priv->throttling = TRUE;
+ priv->throttling_time = time(NULL);
+ }
+ else if (errno == EPIPE || errno == EBADF) {
+ /* We write to some pipe and it disappears, disable logging or we has opened bad file descriptor */
+ rspamd_log->enabled = FALSE;
+ }
+
+ return false;
+ }
+ else if (priv->throttling) {
+ priv->throttling = FALSE;
+ }
+
+ return true;
+}
+
+/**
+ * Fill buffer with message (limits must be checked BEFORE this call)
+ */
+static void
+fill_buffer(rspamd_logger_t *rspamd_log,
+ struct rspamd_file_logger_priv *priv,
+ const struct iovec *iov, gint iovcnt)
+{
+ gint i;
+
+ for (i = 0; i < iovcnt; i++) {
+ memcpy(priv->io_buf.buf + priv->io_buf.used,
+ iov[i].iov_base,
+ iov[i].iov_len);
+ priv->io_buf.used += iov[i].iov_len;
+ }
+}
+
+static void
+rspamd_log_flush(rspamd_logger_t *rspamd_log, struct rspamd_file_logger_priv *priv)
+{
+ if (priv->is_buffered) {
+ direct_write_log_line(rspamd_log,
+ priv,
+ priv->io_buf.buf,
+ priv->io_buf.used,
+ FALSE,
+ rspamd_log->log_level);
+ priv->io_buf.used = 0;
+ }
+}
+
+/*
+ * Write message to buffer or to file (using direct_write_log_line function)
+ */
+static bool
+file_log_helper(rspamd_logger_t *rspamd_log,
+ struct rspamd_file_logger_priv *priv,
+ const struct iovec *iov,
+ guint iovcnt,
+ gint level_flags)
+{
+ size_t len = 0;
+ guint i;
+
+ if (!priv->is_buffered) {
+ /* Write string directly */
+ return direct_write_log_line(rspamd_log, priv, (void *) iov, iovcnt,
+ TRUE, level_flags);
+ }
+ else {
+ /* Calculate total length */
+ for (i = 0; i < iovcnt; i++) {
+ len += iov[i].iov_len;
+ }
+ /* Fill buffer */
+ if (priv->io_buf.size < len) {
+ /* Buffer is too small to hold this string, so write it directly */
+ rspamd_log_flush(rspamd_log, priv);
+ return direct_write_log_line(rspamd_log, priv, (void *) iov, iovcnt,
+ TRUE, level_flags);
+ }
+ else if (priv->io_buf.used + len >= priv->io_buf.size) {
+ /* Buffer is full, try to write it directly */
+ rspamd_log_flush(rspamd_log, priv);
+ fill_buffer(rspamd_log, priv, iov, iovcnt);
+ }
+ else {
+ /* Copy incoming string to buffer */
+ fill_buffer(rspamd_log, priv, iov, iovcnt);
+ }
+ }
+
+ return true;
+}
+
+static void
+rspamd_log_reset_repeated(rspamd_logger_t *rspamd_log,
+ struct rspamd_file_logger_priv *priv)
+{
+ gchar tmpbuf[256];
+ gssize r;
+
+ if (priv->repeats > REPEATS_MIN) {
+ r = rspamd_snprintf(tmpbuf,
+ sizeof(tmpbuf),
+ "Last message repeated %ud times",
+ priv->repeats - REPEATS_MIN);
+ priv->repeats = 0;
+
+ if (priv->saved_message) {
+ rspamd_log_file_log(priv->saved_module,
+ priv->saved_id,
+ priv->saved_function,
+ priv->saved_loglevel | RSPAMD_LOG_FORCED,
+ priv->saved_message,
+ priv->saved_mlen,
+ rspamd_log,
+ priv);
+
+ g_free(priv->saved_message);
+ g_free(priv->saved_function);
+ g_free(priv->saved_module);
+ g_free(priv->saved_id);
+ priv->saved_message = NULL;
+ priv->saved_function = NULL;
+ priv->saved_module = NULL;
+ priv->saved_id = NULL;
+ }
+
+ /* It is safe to use temporary buffer here as it is not static */
+ rspamd_log_file_log(NULL, NULL,
+ G_STRFUNC,
+ priv->saved_loglevel | RSPAMD_LOG_FORCED,
+ tmpbuf,
+ r,
+ rspamd_log,
+ priv);
+ rspamd_log_flush(rspamd_log, priv);
+ }
+}
+
+static gint
+rspamd_try_open_log_fd(rspamd_logger_t *rspamd_log,
+ struct rspamd_file_logger_priv *priv,
+ uid_t uid, gid_t gid,
+ GError **err)
+{
+ gint fd;
+
+ fd = open(priv->log_file,
+ O_CREAT | O_WRONLY | O_APPEND,
+ S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH);
+ if (fd == -1) {
+ g_set_error(err, FILE_LOG_QUARK, errno,
+ "open_log: cannot open desired log file: %s, %s\n",
+ priv->log_file, strerror(errno));
+ return -1;
+ }
+
+ if (uid != -1 || gid != -1) {
+ if (fchown(fd, uid, gid) == -1) {
+ g_set_error(err, FILE_LOG_QUARK, errno,
+ "open_log: cannot chown desired log file: %s, %s\n",
+ priv->log_file, strerror(errno));
+ close(fd);
+
+ return -1;
+ }
+ }
+
+ return fd;
+}
+
+void *
+rspamd_log_file_init(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ uid_t uid, gid_t gid, GError **err)
+{
+ struct rspamd_file_logger_priv *priv;
+
+ if (!cfg || !cfg->cfg_name) {
+ g_set_error(err, FILE_LOG_QUARK, EINVAL,
+ "no log file specified");
+ return NULL;
+ }
+
+ priv = g_malloc0(sizeof(*priv));
+
+ if (cfg->log_buffered) {
+ if (cfg->log_buf_size != 0) {
+ priv->io_buf.size = cfg->log_buf_size;
+ }
+ else {
+ priv->io_buf.size = LOGBUF_LEN;
+ }
+ priv->is_buffered = TRUE;
+ priv->io_buf.buf = g_malloc(priv->io_buf.size);
+ }
+
+ if (cfg->log_file) {
+ priv->log_file = g_strdup(cfg->log_file);
+ }
+
+ priv->log_severity = (logger->flags & RSPAMD_LOG_FLAG_SEVERITY);
+ priv->fd = rspamd_try_open_log_fd(logger, priv, uid, gid, err);
+
+ if (priv->fd == -1) {
+ rspamd_log_file_dtor(logger, priv);
+
+ return NULL;
+ }
+
+ return priv;
+}
+
+void rspamd_log_file_dtor(rspamd_logger_t *logger, gpointer arg)
+{
+ struct rspamd_file_logger_priv *priv = (struct rspamd_file_logger_priv *) arg;
+
+ rspamd_log_reset_repeated(logger, priv);
+ rspamd_log_flush(logger, priv);
+
+ if (priv->fd != -1) {
+ if (close(priv->fd) == -1) {
+ rspamd_fprintf(stderr, "cannot close log fd %d: %s; log file = %s\n",
+ priv->fd, strerror(errno), priv->log_file);
+ }
+ }
+
+ g_free(priv->log_file);
+ g_free(priv);
+}
+
+bool rspamd_log_file_log(const gchar *module, const gchar *id,
+ const gchar *function,
+ gint level_flags,
+ const gchar *message,
+ gsize mlen,
+ rspamd_logger_t *rspamd_log,
+ gpointer arg)
+{
+ struct rspamd_file_logger_priv *priv = (struct rspamd_file_logger_priv *) arg;
+ gdouble now;
+ guint64 cksum;
+ gboolean got_time = FALSE;
+
+
+ if (!(level_flags & RSPAMD_LOG_FORCED) && !rspamd_log->enabled) {
+ return false;
+ }
+
+ /* Check throttling due to write errors */
+ if (!(level_flags & RSPAMD_LOG_FORCED) && priv->throttling) {
+ now = rspamd_get_calendar_ticks();
+
+ if (priv->throttling_time != now) {
+ priv->throttling_time = now;
+ got_time = TRUE;
+ }
+ else {
+ /* Do not try to write to file too often while throttling */
+ return false;
+ }
+ }
+
+ /* Check repeats */
+ cksum = rspamd_log_calculate_cksum(message, mlen);
+
+ if (cksum == priv->last_line_cksum) {
+ priv->repeats++;
+
+ if (priv->repeats > REPEATS_MIN && priv->repeats <
+ REPEATS_MAX) {
+ /* Do not log anything but save message for future */
+ if (priv->saved_message == NULL) {
+ priv->saved_function = g_strdup(function);
+ priv->saved_mlen = mlen;
+ priv->saved_message = g_malloc(mlen);
+ memcpy(priv->saved_message, message, mlen);
+
+ if (module) {
+ priv->saved_module = g_strdup(module);
+ }
+
+ if (id) {
+ priv->saved_id = g_strdup(id);
+ }
+
+ priv->saved_loglevel = level_flags;
+ }
+
+ return true;
+ }
+ else if (priv->repeats > REPEATS_MAX) {
+ rspamd_log_reset_repeated(rspamd_log, priv);
+
+ bool ret = rspamd_log_file_log(module, id,
+ function,
+ level_flags,
+ message,
+ mlen,
+ rspamd_log,
+ priv);
+
+ /* Probably we have more repeats in future */
+ priv->repeats = REPEATS_MIN + 1;
+
+ return ret;
+ }
+ }
+ else {
+ /* Reset counter if new message differs from saved message */
+ priv->last_line_cksum = cksum;
+
+ if (priv->repeats > REPEATS_MIN) {
+ rspamd_log_reset_repeated(rspamd_log, priv);
+ return rspamd_log_file_log(module, id,
+ function,
+ level_flags,
+ message,
+ mlen,
+ rspamd_log,
+ arg);
+ }
+ else {
+ priv->repeats = 0;
+ }
+ }
+ if (!got_time) {
+ now = rspamd_get_calendar_ticks();
+ }
+
+ struct rspamd_logger_iov_ctx iov_ctx;
+ memset(&iov_ctx, 0, sizeof(iov_ctx));
+ rspamd_log_fill_iov(&iov_ctx, now, module, id, function, level_flags, message,
+ mlen, rspamd_log);
+
+ bool ret = file_log_helper(rspamd_log, priv, iov_ctx.iov, iov_ctx.niov, level_flags);
+ rspamd_log_iov_free(&iov_ctx);
+
+ return ret;
+}
+
+void *
+rspamd_log_file_reload(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ gpointer arg, uid_t uid, gid_t gid, GError **err)
+{
+ struct rspamd_file_logger_priv *npriv;
+
+ if (!cfg->cfg_name) {
+ g_set_error(err, FILE_LOG_QUARK, EINVAL,
+ "no log file specified");
+ return NULL;
+ }
+
+ npriv = rspamd_log_file_init(logger, cfg, uid, gid, err);
+
+ if (npriv) {
+ /* Close old */
+ rspamd_log_file_dtor(logger, arg);
+ }
+
+ return npriv;
+}
+
+bool rspamd_log_file_on_fork(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ gpointer arg, GError **err)
+{
+ struct rspamd_file_logger_priv *priv = (struct rspamd_file_logger_priv *) arg;
+
+ rspamd_log_reset_repeated(logger, priv);
+ rspamd_log_flush(logger, priv);
+
+ return true;
+} \ No newline at end of file
diff --git a/src/libserver/logger/logger_private.h b/src/libserver/logger/logger_private.h
new file mode 100644
index 0000000..234a207
--- /dev/null
+++ b/src/libserver/logger/logger_private.h
@@ -0,0 +1,218 @@
+/*
+ * 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.
+ */
+#ifndef RSPAMD_LOGGER_PRIVATE_H
+#define RSPAMD_LOGGER_PRIVATE_H
+
+#include "logger.h"
+
+/* How much message should be repeated before it is count to be repeated one */
+#define REPEATS_MIN 3
+#define REPEATS_MAX 300
+#define LOGBUF_LEN 8192
+
+struct rspamd_log_module {
+ gchar *mname;
+ guint id;
+};
+
+struct rspamd_log_modules {
+ guchar *bitset;
+ guint bitset_len; /* Number of BITS used in bitset */
+ guint bitset_allocated; /* Size of bitset allocated in BYTES */
+ GHashTable *modules;
+};
+
+struct rspamd_logger_error_elt {
+ gint completed;
+ GQuark ptype;
+ pid_t pid;
+ gdouble ts;
+ gchar id[RSPAMD_LOG_ID_LEN + 1];
+ gchar module[9];
+ gchar message[];
+};
+
+struct rspamd_logger_error_log {
+ struct rspamd_logger_error_elt *elts;
+ rspamd_mempool_t *pool;
+ guint32 max_elts;
+ guint32 elt_len;
+ /* Avoid false cache sharing */
+ guchar __padding[64 - sizeof(gpointer) * 2 - sizeof(guint64)];
+ guint cur_row;
+};
+
+/**
+ * Static structure that store logging parameters
+ * It is NOT shared between processes and is created by main process
+ */
+struct rspamd_logger_s {
+ struct rspamd_logger_funcs ops;
+ gint log_level;
+
+ struct rspamd_logger_error_log *errlog;
+ struct rspamd_cryptobox_pubkey *pk;
+ struct rspamd_cryptobox_keypair *keypair;
+
+ guint flags;
+ gboolean closed;
+ gboolean enabled;
+ gboolean is_debug;
+ gboolean no_lock;
+
+ pid_t pid;
+ const gchar *process_type;
+ struct rspamd_radix_map_helper *debug_ip;
+ rspamd_mempool_mutex_t *mtx;
+ rspamd_mempool_t *pool;
+ guint64 log_cnt[4];
+};
+
+/*
+ * Common logging prototypes
+ */
+
+/*
+ * File logging
+ */
+void *rspamd_log_file_init(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ uid_t uid, gid_t gid, GError **err);
+void *rspamd_log_file_reload(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ gpointer arg, uid_t uid, gid_t gid, GError **err);
+void rspamd_log_file_dtor(rspamd_logger_t *logger, gpointer arg);
+bool rspamd_log_file_log(const gchar *module, const gchar *id,
+ const gchar *function,
+ gint level_flags,
+ const gchar *message,
+ gsize mlen,
+ rspamd_logger_t *rspamd_log,
+ gpointer arg);
+bool rspamd_log_file_on_fork(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ gpointer arg, GError **err);
+
+struct rspamd_logger_iov_thrash_stack {
+ struct rspamd_logger_iov_thrash_stack *prev;
+ char data[0];
+};
+#define RSPAMD_LOGGER_MAX_IOV 8
+struct rspamd_logger_iov_ctx {
+ struct iovec iov[RSPAMD_LOGGER_MAX_IOV];
+ int niov;
+ struct rspamd_logger_iov_thrash_stack *thrash_stack;
+};
+/**
+ * Fills IOV of logger (usable for file/console logging)
+ * Warning: this function is NOT reentrant, do not call it twice from a single moment of execution
+ * @param iov filled by this function
+ * @param module
+ * @param id
+ * @param function
+ * @param level_flags
+ * @param message
+ * @param mlen
+ * @param rspamd_log
+ * @return number of iov elements being filled
+ */
+void rspamd_log_fill_iov(struct rspamd_logger_iov_ctx *iov_ctx,
+ double ts,
+ const gchar *module, const gchar *id,
+ const gchar *function,
+ gint level_flags,
+ const gchar *message,
+ gsize mlen,
+ rspamd_logger_t *rspamd_log);
+
+/**
+ * Frees IOV context
+ * @param iov_ctx
+ */
+void rspamd_log_iov_free(struct rspamd_logger_iov_ctx *iov_ctx);
+/**
+ * Escape log line by replacing unprintable characters to hex escapes like \xNN
+ * @param src
+ * @param srclen
+ * @param dst
+ * @param dstlen
+ * @return end of the escaped buffer
+ */
+gchar *rspamd_log_line_hex_escape(const guchar *src, gsize srclen,
+ gchar *dst, gsize dstlen);
+/**
+ * Returns number of characters to be escaped, e.g. a caller can allocate a new buffer
+ * the desired number of characters
+ * @param src
+ * @param srclen
+ * @return number of characters to be escaped
+ */
+gsize rspamd_log_line_need_escape(const guchar *src, gsize srclen);
+
+static const struct rspamd_logger_funcs file_log_funcs = {
+ .init = rspamd_log_file_init,
+ .dtor = rspamd_log_file_dtor,
+ .reload = rspamd_log_file_reload,
+ .log = rspamd_log_file_log,
+ .on_fork = rspamd_log_file_on_fork,
+};
+
+/*
+ * Syslog logging
+ */
+void *rspamd_log_syslog_init(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ uid_t uid, gid_t gid, GError **err);
+void *rspamd_log_syslog_reload(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ gpointer arg, uid_t uid, gid_t gid, GError **err);
+void rspamd_log_syslog_dtor(rspamd_logger_t *logger, gpointer arg);
+bool rspamd_log_syslog_log(const gchar *module, const gchar *id,
+ const gchar *function,
+ gint level_flags,
+ const gchar *message,
+ gsize mlen,
+ rspamd_logger_t *rspamd_log,
+ gpointer arg);
+
+static const struct rspamd_logger_funcs syslog_log_funcs = {
+ .init = rspamd_log_syslog_init,
+ .dtor = rspamd_log_syslog_dtor,
+ .reload = rspamd_log_syslog_reload,
+ .log = rspamd_log_syslog_log,
+ .on_fork = NULL,
+};
+
+/*
+ * Console logging
+ */
+void *rspamd_log_console_init(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ uid_t uid, gid_t gid, GError **err);
+void *rspamd_log_console_reload(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ gpointer arg, uid_t uid, gid_t gid, GError **err);
+void rspamd_log_console_dtor(rspamd_logger_t *logger, gpointer arg);
+bool rspamd_log_console_log(const gchar *module, const gchar *id,
+ const gchar *function,
+ gint level_flags,
+ const gchar *message,
+ gsize mlen,
+ rspamd_logger_t *rspamd_log,
+ gpointer arg);
+
+static const struct rspamd_logger_funcs console_log_funcs = {
+ .init = rspamd_log_console_init,
+ .dtor = rspamd_log_console_dtor,
+ .reload = rspamd_log_console_reload,
+ .log = rspamd_log_console_log,
+ .on_fork = NULL,
+};
+
+#endif
diff --git a/src/libserver/logger/logger_syslog.c b/src/libserver/logger/logger_syslog.c
new file mode 100644
index 0000000..3c4f7f7
--- /dev/null
+++ b/src/libserver/logger/logger_syslog.c
@@ -0,0 +1,143 @@
+/*-
+ * Copyright 2020 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 "logger.h"
+#include "libserver/cfg_file.h"
+#include "logger_private.h"
+
+#define SYSLOG_LOG_QUARK g_quark_from_static_string("syslog_logger")
+
+struct rspamd_syslog_logger_priv {
+ gint log_facility;
+};
+
+#ifdef HAVE_SYSLOG_H
+#include <syslog.h>
+
+void *
+rspamd_log_syslog_init(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ uid_t uid, gid_t gid, GError **err)
+{
+ struct rspamd_syslog_logger_priv *priv;
+
+ if (!cfg) {
+ g_set_error(err, SYSLOG_LOG_QUARK, EINVAL,
+ "no log config specified");
+ return NULL;
+ }
+
+ priv = g_malloc0(sizeof(*priv));
+
+ priv->log_facility = cfg->log_facility;
+ openlog("rspamd", LOG_NDELAY | LOG_PID, priv->log_facility);
+
+ return priv;
+}
+
+void rspamd_log_syslog_dtor(rspamd_logger_t *logger, gpointer arg)
+{
+ struct rspamd_syslog_logger_priv *priv = (struct rspamd_syslog_logger_priv *) arg;
+
+ closelog();
+ g_free(priv);
+}
+bool rspamd_log_syslog_log(const gchar *module, const gchar *id,
+ const gchar *function,
+ gint level_flags,
+ const gchar *message,
+ gsize mlen,
+ rspamd_logger_t *rspamd_log,
+ gpointer arg)
+{
+ static const struct {
+ GLogLevelFlags glib_level;
+ gint syslog_level;
+ } levels_match[] = {
+ {G_LOG_LEVEL_DEBUG, LOG_DEBUG},
+ {G_LOG_LEVEL_INFO, LOG_INFO},
+ {G_LOG_LEVEL_WARNING, LOG_WARNING},
+ {G_LOG_LEVEL_CRITICAL, LOG_ERR}};
+ unsigned i;
+ gint syslog_level;
+
+ if (!(level_flags & RSPAMD_LOG_FORCED) && !rspamd_log->enabled) {
+ return false;
+ }
+
+ /* Detect level */
+ syslog_level = LOG_DEBUG;
+
+ for (i = 0; i < G_N_ELEMENTS(levels_match); i++) {
+ if (level_flags & levels_match[i].glib_level) {
+ syslog_level = levels_match[i].syslog_level;
+ break;
+ }
+ }
+
+ syslog(syslog_level, "<%.*s>; %s; %s: %.*s",
+ RSPAMD_LOG_ID_LEN, id != NULL ? id : "",
+ module != NULL ? module : "",
+ function != NULL ? function : "",
+ (gint) mlen, message);
+
+ return true;
+}
+
+#else
+
+void *
+rspamd_log_syslog_init(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ uid_t uid, gid_t gid, GError **err)
+{
+ g_set_error(err, SYSLOG_LOG_QUARK, EINVAL, "syslog support is not compiled in");
+
+ return NULL;
+}
+
+bool rspamd_log_syslog_log(const gchar *module, const gchar *id,
+ const gchar *function,
+ gint level_flags,
+ const gchar *message,
+ gsize mlen,
+ rspamd_logger_t *rspamd_log,
+ gpointer arg)
+{
+ return false;
+}
+
+void rspamd_log_syslog_dtor(rspamd_logger_t *logger, gpointer arg)
+{
+ /* Left blank intentionally */
+}
+
+#endif
+
+void *
+rspamd_log_syslog_reload(rspamd_logger_t *logger, struct rspamd_config *cfg,
+ gpointer arg, uid_t uid, gid_t gid, GError **err)
+{
+ struct rspamd_syslog_logger_priv *npriv;
+
+ npriv = rspamd_log_syslog_init(logger, cfg, uid, gid, err);
+
+ if (npriv) {
+ /* Close old */
+ rspamd_log_syslog_dtor(logger, arg);
+ }
+
+ return npriv;
+}
diff --git a/src/libserver/maps/map.c b/src/libserver/maps/map.c
new file mode 100644
index 0000000..7f6a48f
--- /dev/null
+++ b/src/libserver/maps/map.c
@@ -0,0 +1,3195 @@
+/*
+ * 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.
+ */
+/*
+ * Implementation of map files handling
+ */
+
+#include "config.h"
+#include "map.h"
+#include "map_private.h"
+#include "libserver/http/http_connection.h"
+#include "libserver/http/http_private.h"
+#include "rspamd.h"
+#include "contrib/libev/ev.h"
+#include "contrib/uthash/utlist.h"
+
+#ifdef SYS_ZSTD
+#include "zstd.h"
+#else
+#include "contrib/zstd/zstd.h"
+#endif
+
+#undef MAP_DEBUG_REFS
+#ifdef MAP_DEBUG_REFS
+#define MAP_RETAIN(x, t) \
+ do { \
+ msg_err(G_GNUC_PRETTY_FUNCTION ": " t ": retain ref %p, refcount: %d -> %d", (x), (x)->ref.refcount, (x)->ref.refcount + 1); \
+ REF_RETAIN(x); \
+ } while (0)
+
+#define MAP_RELEASE(x, t) \
+ do { \
+ msg_err(G_GNUC_PRETTY_FUNCTION ": " t ": release ref %p, refcount: %d -> %d", (x), (x)->ref.refcount, (x)->ref.refcount - 1); \
+ REF_RELEASE(x); \
+ } while (0)
+#else
+#define MAP_RETAIN(x, t) REF_RETAIN(x)
+#define MAP_RELEASE(x, t) REF_RELEASE(x)
+#endif
+
+enum rspamd_map_periodic_opts {
+ RSPAMD_MAP_SCHEDULE_NORMAL = 0,
+ RSPAMD_MAP_SCHEDULE_ERROR = (1u << 0u),
+ RSPAMD_MAP_SCHEDULE_LOCKED = (1u << 1u),
+ RSPAMD_MAP_SCHEDULE_INIT = (1u << 2u),
+};
+
+static void free_http_cbdata_common(struct http_callback_data *cbd,
+ gboolean plan_new);
+static void free_http_cbdata_dtor(gpointer p);
+static void free_http_cbdata(struct http_callback_data *cbd);
+static void rspamd_map_process_periodic(struct map_periodic_cbdata *cbd);
+static void rspamd_map_schedule_periodic(struct rspamd_map *map, int how);
+static gboolean read_map_file_chunks(struct rspamd_map *map,
+ struct map_cb_data *cbdata,
+ const gchar *fname,
+ gsize len,
+ goffset off);
+static gboolean rspamd_map_save_http_cached_file(struct rspamd_map *map,
+ struct rspamd_map_backend *bk,
+ struct http_map_data *htdata,
+ const guchar *data,
+ gsize len);
+static gboolean rspamd_map_update_http_cached_file(struct rspamd_map *map,
+ struct rspamd_map_backend *bk,
+ struct http_map_data *htdata);
+
+guint rspamd_map_log_id = (guint) -1;
+RSPAMD_CONSTRUCTOR(rspamd_map_log_init)
+{
+ rspamd_map_log_id = rspamd_logger_add_debug_module("map");
+}
+
+/**
+ * Write HTTP request
+ */
+static void
+write_http_request(struct http_callback_data *cbd)
+{
+ gchar datebuf[128];
+ struct rspamd_http_message *msg;
+
+ msg = rspamd_http_new_message(HTTP_REQUEST);
+ if (cbd->check) {
+ msg->method = HTTP_HEAD;
+ }
+
+ msg->url = rspamd_fstring_append(msg->url,
+ cbd->data->path, strlen(cbd->data->path));
+
+ if (cbd->check) {
+ if (cbd->data->last_modified != 0) {
+ rspamd_http_date_format(datebuf, sizeof(datebuf),
+ cbd->data->last_modified);
+ rspamd_http_message_add_header(msg, "If-Modified-Since",
+ datebuf);
+ }
+ if (cbd->data->etag) {
+ rspamd_http_message_add_header_len(msg, "If-None-Match",
+ cbd->data->etag->str, cbd->data->etag->len);
+ }
+ }
+
+ msg->url = rspamd_fstring_append(msg->url, cbd->data->rest,
+ strlen(cbd->data->rest));
+
+ if (cbd->data->userinfo) {
+ rspamd_http_message_add_header(msg, "Authorization",
+ cbd->data->userinfo);
+ }
+
+ MAP_RETAIN(cbd, "http_callback_data");
+ rspamd_http_connection_write_message(cbd->conn,
+ msg,
+ cbd->data->host,
+ NULL,
+ cbd,
+ cbd->timeout);
+}
+
+/**
+ * Callback for destroying HTTP callback data
+ */
+static void
+free_http_cbdata_common(struct http_callback_data *cbd, gboolean plan_new)
+{
+ struct map_periodic_cbdata *periodic = cbd->periodic;
+
+ if (cbd->shmem_data) {
+ rspamd_http_message_shmem_unref(cbd->shmem_data);
+ }
+
+ if (cbd->pk) {
+ rspamd_pubkey_unref(cbd->pk);
+ }
+
+ if (cbd->conn) {
+ rspamd_http_connection_unref(cbd->conn);
+ cbd->conn = NULL;
+ }
+
+ if (cbd->addrs) {
+ rspamd_inet_addr_t *addr;
+ guint i;
+
+ PTR_ARRAY_FOREACH(cbd->addrs, i, addr)
+ {
+ rspamd_inet_address_free(addr);
+ }
+
+ g_ptr_array_free(cbd->addrs, TRUE);
+ }
+
+
+ MAP_RELEASE(cbd->bk, "rspamd_map_backend");
+
+ if (periodic) {
+ /* Detached in case of HTTP error */
+ MAP_RELEASE(periodic, "periodic");
+ }
+
+ g_free(cbd);
+}
+
+static void
+free_http_cbdata(struct http_callback_data *cbd)
+{
+ cbd->map->tmp_dtor = NULL;
+ cbd->map->tmp_dtor_data = NULL;
+
+ free_http_cbdata_common(cbd, TRUE);
+}
+
+static void
+free_http_cbdata_dtor(gpointer p)
+{
+ struct http_callback_data *cbd = p;
+ struct rspamd_map *map;
+
+ map = cbd->map;
+ if (cbd->stage == http_map_http_conn) {
+ REF_RELEASE(cbd);
+ }
+ else {
+ /* We cannot terminate DNS requests sent */
+ cbd->stage = http_map_terminated;
+ }
+
+ msg_warn_map("%s: "
+ "connection with http server is terminated: worker is stopping",
+ map->name);
+}
+
+/*
+ * HTTP callbacks
+ */
+static void
+http_map_error(struct rspamd_http_connection *conn,
+ GError *err)
+{
+ struct http_callback_data *cbd = conn->ud;
+ struct rspamd_map *map;
+
+ map = cbd->map;
+
+ if (cbd->periodic) {
+ cbd->periodic->errored = TRUE;
+ msg_err_map("error reading %s(%s): "
+ "connection with http server terminated incorrectly: %e",
+ cbd->bk->uri,
+ cbd->addr ? rspamd_inet_address_to_string_pretty(cbd->addr) : "",
+ err);
+
+ rspamd_map_process_periodic(cbd->periodic);
+ }
+
+ MAP_RELEASE(cbd, "http_callback_data");
+}
+
+static void
+rspamd_map_cache_cb(struct ev_loop *loop, ev_timer *w, int revents)
+{
+ struct rspamd_http_map_cached_cbdata *cache_cbd = (struct rspamd_http_map_cached_cbdata *)
+ w->data;
+ struct rspamd_map *map;
+ struct http_map_data *data;
+
+ map = cache_cbd->map;
+ data = cache_cbd->data;
+
+ if (cache_cbd->gen != cache_cbd->data->gen) {
+ /* We have another update, so this cache element is obviously expired */
+ /*
+ * Important!: we do not set cache availability to zero here, as there
+ * might be fresh cache
+ */
+ msg_info_map("cached data is now expired (gen mismatch %L != %L) for %s; shm name=%s; refcount=%d",
+ cache_cbd->gen, cache_cbd->data->gen, map->name, cache_cbd->shm->shm_name,
+ cache_cbd->shm->ref.refcount);
+ MAP_RELEASE(cache_cbd->shm, "rspamd_http_map_cached_cbdata");
+ ev_timer_stop(loop, &cache_cbd->timeout);
+ g_free(cache_cbd);
+ }
+ else if (cache_cbd->data->last_checked >= cache_cbd->last_checked) {
+ /*
+ * We checked map but we have not found anything more recent,
+ * reschedule cache check
+ */
+ if (cache_cbd->map->poll_timeout >
+ rspamd_get_calendar_ticks() - cache_cbd->data->last_checked) {
+ w->repeat = cache_cbd->map->poll_timeout -
+ (rspamd_get_calendar_ticks() - cache_cbd->data->last_checked);
+ }
+ else {
+ w->repeat = cache_cbd->map->poll_timeout;
+ }
+
+ if (w->repeat < 0) {
+ msg_info_map("cached data for %s has skewed check time: %d last checked, "
+ "%d poll timeout, %.2f diff; shm name=%s; refcount=%d",
+ map->name, (int) cache_cbd->data->last_checked,
+ (int) cache_cbd->map->poll_timeout,
+ (rspamd_get_calendar_ticks() - cache_cbd->data->last_checked),
+ cache_cbd->shm->shm_name,
+ cache_cbd->shm->ref.refcount);
+ w->repeat = 0.0;
+ }
+
+ cache_cbd->last_checked = cache_cbd->data->last_checked;
+ msg_debug_map("cached data is up to date for %s", map->name);
+ ev_timer_again(loop, &cache_cbd->timeout);
+ }
+ else {
+ data->cur_cache_cbd = NULL;
+ g_atomic_int_set(&data->cache->available, 0);
+ msg_info_map("cached data is now expired for %s; shm name=%s; refcount=%d",
+ map->name,
+ cache_cbd->shm->shm_name,
+ cache_cbd->shm->ref.refcount);
+ MAP_RELEASE(cache_cbd->shm, "rspamd_http_map_cached_cbdata");
+ ev_timer_stop(loop, &cache_cbd->timeout);
+ g_free(cache_cbd);
+ }
+}
+
+static int
+http_map_finish(struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg)
+{
+ struct http_callback_data *cbd = conn->ud;
+ struct rspamd_map *map;
+ struct rspamd_map_backend *bk;
+ struct http_map_data *data;
+ struct rspamd_http_map_cached_cbdata *cache_cbd;
+ const rspamd_ftok_t *expires_hdr, *etag_hdr;
+ char next_check_date[128];
+ guchar *in = NULL;
+ gsize dlen = 0;
+
+ map = cbd->map;
+ bk = cbd->bk;
+ data = bk->data.hd;
+
+ if (msg->code == 200) {
+
+ if (cbd->check) {
+ msg_info_map("need to reread map from %s", cbd->bk->uri);
+ cbd->periodic->need_modify = TRUE;
+ /* Reset the whole chain */
+ cbd->periodic->cur_backend = 0;
+ /* Reset cache, old cached data will be cleaned on timeout */
+ g_atomic_int_set(&data->cache->available, 0);
+ data->cur_cache_cbd = NULL;
+
+ rspamd_map_process_periodic(cbd->periodic);
+ MAP_RELEASE(cbd, "http_callback_data");
+
+ return 0;
+ }
+
+ cbd->data->last_checked = msg->date;
+
+ if (msg->last_modified) {
+ cbd->data->last_modified = msg->last_modified;
+ }
+ else {
+ cbd->data->last_modified = msg->date;
+ }
+
+
+ /* Unsigned version - just open file */
+ cbd->shmem_data = rspamd_http_message_shmem_ref(msg);
+ cbd->data_len = msg->body_buf.len;
+
+ if (cbd->data_len == 0) {
+ msg_err_map("cannot read empty map");
+ goto err;
+ }
+
+ g_assert(cbd->shmem_data != NULL);
+
+ in = rspamd_shmem_xmap(cbd->shmem_data->shm_name, PROT_READ, &dlen);
+
+ if (in == NULL) {
+ msg_err_map("cannot read tempfile %s: %s",
+ cbd->shmem_data->shm_name,
+ strerror(errno));
+ goto err;
+ }
+
+ /* Check for expires */
+ double cached_timeout = map->poll_timeout * 2;
+
+ expires_hdr = rspamd_http_message_find_header(msg, "Expires");
+
+ if (expires_hdr) {
+ time_t hdate;
+
+ hdate = rspamd_http_parse_date(expires_hdr->begin, expires_hdr->len);
+
+ if (hdate != (time_t) -1 && hdate > msg->date) {
+ cached_timeout = map->next_check - msg->date +
+ map->poll_timeout * 2;
+
+ map->next_check = hdate;
+ }
+ else {
+ msg_info_map("invalid expires header: %T, ignore it", expires_hdr);
+ map->next_check = 0;
+ }
+ }
+
+ /* Check for etag */
+ etag_hdr = rspamd_http_message_find_header(msg, "ETag");
+
+ if (etag_hdr) {
+ if (cbd->data->etag) {
+ /* Remove old etag */
+ rspamd_fstring_free(cbd->data->etag);
+ }
+
+ cbd->data->etag = rspamd_fstring_new_init(etag_hdr->begin,
+ etag_hdr->len);
+ }
+ else {
+ if (cbd->data->etag) {
+ /* Remove and clear old etag */
+ rspamd_fstring_free(cbd->data->etag);
+ cbd->data->etag = NULL;
+ }
+ }
+
+ MAP_RETAIN(cbd->shmem_data, "shmem_data");
+ cbd->data->gen++;
+ /*
+ * We know that a map is in the locked state
+ */
+ g_atomic_int_set(&data->cache->available, 1);
+ /* Store cached data */
+ rspamd_strlcpy(data->cache->shmem_name, cbd->shmem_data->shm_name,
+ sizeof(data->cache->shmem_name));
+ data->cache->len = cbd->data_len;
+ data->cache->last_modified = cbd->data->last_modified;
+ cache_cbd = g_malloc0(sizeof(*cache_cbd));
+ cache_cbd->shm = cbd->shmem_data;
+ cache_cbd->event_loop = cbd->event_loop;
+ cache_cbd->map = map;
+ cache_cbd->data = cbd->data;
+ cache_cbd->last_checked = cbd->data->last_checked;
+ cache_cbd->gen = cbd->data->gen;
+ MAP_RETAIN(cache_cbd->shm, "shmem_data");
+ msg_info_map("stored map data in a shared memory cache: %s",
+ cache_cbd->shm->shm_name);
+
+ ev_timer_init(&cache_cbd->timeout, rspamd_map_cache_cb, cached_timeout,
+ 0.0);
+ ev_timer_start(cbd->event_loop, &cache_cbd->timeout);
+ cache_cbd->timeout.data = cache_cbd;
+ data->cur_cache_cbd = cache_cbd;
+
+ if (map->next_check) {
+ rspamd_http_date_format(next_check_date, sizeof(next_check_date),
+ map->next_check);
+ }
+ else {
+ rspamd_http_date_format(next_check_date, sizeof(next_check_date),
+ rspamd_get_calendar_ticks() + map->poll_timeout);
+ }
+
+
+ if (cbd->bk->is_compressed) {
+ ZSTD_DStream *zstream;
+ ZSTD_inBuffer zin;
+ ZSTD_outBuffer zout;
+ guchar *out;
+ gsize outlen, r;
+
+ zstream = ZSTD_createDStream();
+ ZSTD_initDStream(zstream);
+
+ zin.pos = 0;
+ zin.src = in;
+ zin.size = dlen;
+
+ if ((outlen = ZSTD_getDecompressedSize(zin.src, zin.size)) == 0) {
+ outlen = ZSTD_DStreamOutSize();
+ }
+
+ out = g_malloc(outlen);
+
+ zout.dst = out;
+ zout.pos = 0;
+ zout.size = outlen;
+
+ while (zin.pos < zin.size) {
+ r = ZSTD_decompressStream(zstream, &zout, &zin);
+
+ if (ZSTD_isError(r)) {
+ msg_err_map("%s(%s): cannot decompress data: %s",
+ cbd->bk->uri,
+ rspamd_inet_address_to_string_pretty(cbd->addr),
+ ZSTD_getErrorName(r));
+ ZSTD_freeDStream(zstream);
+ g_free(out);
+ MAP_RELEASE(cbd->shmem_data, "shmem_data");
+ goto err;
+ }
+
+ if (zout.pos == zout.size) {
+ /* We need to extend output buffer */
+ zout.size = zout.size * 2 + 1.0;
+ out = g_realloc(zout.dst, zout.size);
+ zout.dst = out;
+ }
+ }
+
+ ZSTD_freeDStream(zstream);
+ msg_info_map("%s(%s): read map data %z bytes compressed, "
+ "%z uncompressed, next check at %s",
+ cbd->bk->uri,
+ rspamd_inet_address_to_string_pretty(cbd->addr),
+ dlen, zout.pos, next_check_date);
+ map->read_callback(out, zout.pos, &cbd->periodic->cbdata, TRUE);
+ rspamd_map_save_http_cached_file(map, bk, cbd->data, out, zout.pos);
+ g_free(out);
+ }
+ else {
+ msg_info_map("%s(%s): read map data %z bytes, next check at %s",
+ cbd->bk->uri,
+ rspamd_inet_address_to_string_pretty(cbd->addr),
+ dlen, next_check_date);
+ rspamd_map_save_http_cached_file(map, bk, cbd->data, in, cbd->data_len);
+ map->read_callback(in, cbd->data_len, &cbd->periodic->cbdata, TRUE);
+ }
+
+ MAP_RELEASE(cbd->shmem_data, "shmem_data");
+
+ cbd->periodic->cur_backend++;
+ munmap(in, dlen);
+ rspamd_map_process_periodic(cbd->periodic);
+ }
+ else if (msg->code == 304 && cbd->check) {
+ cbd->data->last_checked = msg->date;
+
+ if (msg->last_modified) {
+ cbd->data->last_modified = msg->last_modified;
+ }
+ else {
+ cbd->data->last_modified = msg->date;
+ }
+
+ expires_hdr = rspamd_http_message_find_header(msg, "Expires");
+
+ if (expires_hdr) {
+ time_t hdate;
+
+ hdate = rspamd_http_parse_date(expires_hdr->begin, expires_hdr->len);
+ if (hdate != (time_t) -1 && hdate > msg->date) {
+ map->next_check = hdate;
+ }
+ else {
+ msg_info_map("invalid expires header: %T, ignore it", expires_hdr);
+ map->next_check = 0;
+ }
+ }
+
+ etag_hdr = rspamd_http_message_find_header(msg, "ETag");
+
+ if (etag_hdr) {
+ if (cbd->data->etag) {
+ /* Remove old etag */
+ rspamd_fstring_free(cbd->data->etag);
+ cbd->data->etag = rspamd_fstring_new_init(etag_hdr->begin,
+ etag_hdr->len);
+ }
+ }
+
+ if (map->next_check) {
+ rspamd_http_date_format(next_check_date, sizeof(next_check_date),
+ map->next_check);
+ msg_info_map("data is not modified for server %s, next check at %s "
+ "(http cache based: %T)",
+ cbd->data->host, next_check_date, expires_hdr);
+ }
+ else {
+ rspamd_http_date_format(next_check_date, sizeof(next_check_date),
+ rspamd_get_calendar_ticks() + map->poll_timeout);
+ msg_info_map("data is not modified for server %s, next check at %s "
+ "(timer based)",
+ cbd->data->host, next_check_date);
+ }
+
+ rspamd_map_update_http_cached_file(map, bk, cbd->data);
+ cbd->periodic->cur_backend++;
+ rspamd_map_process_periodic(cbd->periodic);
+ }
+ else {
+ msg_info_map("cannot load map %s from %s: HTTP error %d",
+ bk->uri, cbd->data->host, msg->code);
+ goto err;
+ }
+
+ MAP_RELEASE(cbd, "http_callback_data");
+ return 0;
+
+err:
+ cbd->periodic->errored = 1;
+ rspamd_map_process_periodic(cbd->periodic);
+ MAP_RELEASE(cbd, "http_callback_data");
+
+ return 0;
+}
+
+static gboolean
+read_map_file_chunks(struct rspamd_map *map, struct map_cb_data *cbdata,
+ const gchar *fname, gsize len, goffset off)
+{
+ gint fd;
+ gssize r, avail;
+ gsize buflen = 1024 * 1024;
+ gchar *pos, *bytes;
+
+ fd = rspamd_file_xopen(fname, O_RDONLY, 0, TRUE);
+
+ if (fd == -1) {
+ msg_err_map("can't open map for buffered reading %s: %s",
+ fname, strerror(errno));
+ return FALSE;
+ }
+
+ if (lseek(fd, off, SEEK_SET) == -1) {
+ msg_err_map("can't seek in map to pos %d for buffered reading %s: %s",
+ (gint) off, fname, strerror(errno));
+ close(fd);
+
+ return FALSE;
+ }
+
+ buflen = MIN(len, buflen);
+ bytes = g_malloc(buflen);
+ avail = buflen;
+ pos = bytes;
+
+ while ((r = read(fd, pos, avail)) > 0) {
+ gchar *end = bytes + (pos - bytes) + r;
+ msg_debug_map("%s: read map chunk, %z bytes", fname,
+ r);
+ pos = map->read_callback(bytes, end - bytes, cbdata, r == len);
+
+ if (pos && pos > bytes && pos < end) {
+ guint remain = end - pos;
+
+ memmove(bytes, pos, remain);
+ pos = bytes + remain;
+ /* Need to preserve the remain */
+ avail = ((gssize) buflen) - remain;
+
+ if (avail <= 0) {
+ /* Try realloc, too large element */
+ g_assert(buflen >= remain);
+ bytes = g_realloc(bytes, buflen * 2);
+
+ pos = bytes + remain; /* Adjust */
+ avail += buflen;
+ buflen *= 2;
+ }
+ }
+ else {
+ avail = buflen;
+ pos = bytes;
+ }
+
+ len -= r;
+ }
+
+ if (r == -1) {
+ msg_err_map("can't read from map %s: %s", fname, strerror(errno));
+ close(fd);
+ g_free(bytes);
+
+ return FALSE;
+ }
+
+ close(fd);
+ g_free(bytes);
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_map_check_sig_pk_mem(const guchar *sig,
+ gsize siglen,
+ struct rspamd_map *map,
+ const guchar *input,
+ gsize inlen,
+ struct rspamd_cryptobox_pubkey *pk)
+{
+ GString *b32_key;
+ gboolean ret = TRUE;
+
+ if (siglen != rspamd_cryptobox_signature_bytes(RSPAMD_CRYPTOBOX_MODE_25519)) {
+ msg_err_map("can't open signature for %s: invalid size: %z", map->name, siglen);
+
+ ret = FALSE;
+ }
+
+ if (ret && !rspamd_cryptobox_verify(sig, siglen, input, inlen,
+ rspamd_pubkey_get_pk(pk, NULL), RSPAMD_CRYPTOBOX_MODE_25519)) {
+ msg_err_map("can't verify signature for %s: incorrect signature", map->name);
+
+ ret = FALSE;
+ }
+
+ if (ret) {
+ b32_key = rspamd_pubkey_print(pk,
+ RSPAMD_KEYPAIR_BASE32 | RSPAMD_KEYPAIR_PUBKEY);
+ msg_info_map("verified signature for %s using trusted key %v",
+ map->name, b32_key);
+ g_string_free(b32_key, TRUE);
+ }
+
+ return ret;
+}
+
+static gboolean
+rspamd_map_check_file_sig(const char *fname,
+ struct rspamd_map *map,
+ struct rspamd_map_backend *bk,
+ const guchar *input,
+ gsize inlen)
+{
+ guchar *data;
+ struct rspamd_cryptobox_pubkey *pk = NULL;
+ GString *b32_key;
+ gboolean ret = TRUE;
+ gsize len = 0;
+ gchar fpath[PATH_MAX];
+
+ if (bk->trusted_pubkey == NULL) {
+ /* Try to load and check pubkey */
+ rspamd_snprintf(fpath, sizeof(fpath), "%s.pub", fname);
+ data = rspamd_file_xmap(fpath, PROT_READ, &len, TRUE);
+
+ if (data == NULL) {
+ msg_err_map("can't open pubkey %s: %s", fpath, strerror(errno));
+ return FALSE;
+ }
+
+ pk = rspamd_pubkey_from_base32(data, len, RSPAMD_KEYPAIR_SIGN,
+ RSPAMD_CRYPTOBOX_MODE_25519);
+ munmap(data, len);
+
+ if (pk == NULL) {
+ msg_err_map("can't load pubkey %s", fpath);
+ return FALSE;
+ }
+
+ /* We just check pk against the trusted db of keys */
+ b32_key = rspamd_pubkey_print(pk,
+ RSPAMD_KEYPAIR_BASE32 | RSPAMD_KEYPAIR_PUBKEY);
+ g_assert(b32_key != NULL);
+
+ if (g_hash_table_lookup(map->cfg->trusted_keys, b32_key->str) == NULL) {
+ msg_err_map("pubkey loaded from %s is untrusted: %v", fpath,
+ b32_key);
+ g_string_free(b32_key, TRUE);
+ rspamd_pubkey_unref(pk);
+
+ return FALSE;
+ }
+
+ g_string_free(b32_key, TRUE);
+ }
+ else {
+ pk = rspamd_pubkey_ref(bk->trusted_pubkey);
+ }
+
+ rspamd_snprintf(fpath, sizeof(fpath), "%s.sig", fname);
+ data = rspamd_shmem_xmap(fpath, PROT_READ, &len);
+
+ if (data == NULL) {
+ msg_err_map("can't open signature %s: %s", fpath, strerror(errno));
+ ret = FALSE;
+ }
+
+ if (ret) {
+ ret = rspamd_map_check_sig_pk_mem(data, len, map, input, inlen, pk);
+ munmap(data, len);
+ }
+
+ rspamd_pubkey_unref(pk);
+
+ return ret;
+}
+
+/**
+ * Callback for reading data from file
+ */
+static gboolean
+read_map_file(struct rspamd_map *map, struct file_map_data *data,
+ struct rspamd_map_backend *bk, struct map_periodic_cbdata *periodic)
+{
+ gchar *bytes;
+ gsize len;
+ struct stat st;
+
+ if (map->read_callback == NULL || map->fin_callback == NULL) {
+ msg_err_map("%s: bad callback for reading map file",
+ data->filename);
+ return FALSE;
+ }
+
+ if (stat(data->filename, &st) == -1) {
+ /* File does not exist, skipping */
+ if (errno != ENOENT) {
+ msg_err_map("%s: map file is unavailable for reading: %s",
+ data->filename, strerror(errno));
+
+ return FALSE;
+ }
+ else {
+ msg_info_map("%s: map file is not found; "
+ "it will be read automatically if created",
+ data->filename);
+ return TRUE;
+ }
+ }
+
+ ev_stat_stat(map->event_loop, &data->st_ev);
+ len = st.st_size;
+
+ if (bk->is_signed) {
+ bytes = rspamd_file_xmap(data->filename, PROT_READ, &len, TRUE);
+
+ if (bytes == NULL) {
+ msg_err_map("can't open map %s: %s", data->filename, strerror(errno));
+ return FALSE;
+ }
+
+ if (!rspamd_map_check_file_sig(data->filename, map, bk, bytes, len)) {
+ munmap(bytes, len);
+
+ return FALSE;
+ }
+
+ munmap(bytes, len);
+ }
+
+ if (len > 0) {
+ if (map->no_file_read) {
+ /* We just call read callback with backend name */
+ map->read_callback(data->filename, strlen(data->filename),
+ &periodic->cbdata, TRUE);
+ }
+ else {
+ if (bk->is_compressed) {
+ bytes = rspamd_file_xmap(data->filename, PROT_READ, &len, TRUE);
+
+ if (bytes == NULL) {
+ msg_err_map("can't open map %s: %s", data->filename, strerror(errno));
+ return FALSE;
+ }
+
+ ZSTD_DStream *zstream;
+ ZSTD_inBuffer zin;
+ ZSTD_outBuffer zout;
+ guchar *out;
+ gsize outlen, r;
+
+ zstream = ZSTD_createDStream();
+ ZSTD_initDStream(zstream);
+
+ zin.pos = 0;
+ zin.src = bytes;
+ zin.size = len;
+
+ if ((outlen = ZSTD_getDecompressedSize(zin.src, zin.size)) == 0) {
+ outlen = ZSTD_DStreamOutSize();
+ }
+
+ out = g_malloc(outlen);
+
+ zout.dst = out;
+ zout.pos = 0;
+ zout.size = outlen;
+
+ while (zin.pos < zin.size) {
+ r = ZSTD_decompressStream(zstream, &zout, &zin);
+
+ if (ZSTD_isError(r)) {
+ msg_err_map("%s: cannot decompress data: %s",
+ data->filename,
+ ZSTD_getErrorName(r));
+ ZSTD_freeDStream(zstream);
+ g_free(out);
+ munmap(bytes, len);
+ return FALSE;
+ }
+
+ if (zout.pos == zout.size) {
+ /* We need to extend output buffer */
+ zout.size = zout.size * 2 + 1;
+ out = g_realloc(zout.dst, zout.size);
+ zout.dst = out;
+ }
+ }
+
+ ZSTD_freeDStream(zstream);
+ msg_info_map("%s: read map data, %z bytes compressed, "
+ "%z uncompressed)",
+ data->filename,
+ len, zout.pos);
+ map->read_callback(out, zout.pos, &periodic->cbdata, TRUE);
+ g_free(out);
+
+ munmap(bytes, len);
+ }
+ else {
+ /* Perform buffered read: fail-safe */
+ if (!read_map_file_chunks(map, &periodic->cbdata, data->filename,
+ len, 0)) {
+ return FALSE;
+ }
+ }
+ }
+ }
+ else {
+ /* Empty map */
+ map->read_callback(NULL, 0, &periodic->cbdata, TRUE);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+read_map_static(struct rspamd_map *map, struct static_map_data *data,
+ struct rspamd_map_backend *bk, struct map_periodic_cbdata *periodic)
+{
+ guchar *bytes;
+ gsize len;
+
+ if (map->read_callback == NULL || map->fin_callback == NULL) {
+ msg_err_map("%s: bad callback for reading map file", map->name);
+ data->processed = TRUE;
+ return FALSE;
+ }
+
+ bytes = data->data;
+ len = data->len;
+
+ if (len > 0) {
+ if (bk->is_compressed) {
+ ZSTD_DStream *zstream;
+ ZSTD_inBuffer zin;
+ ZSTD_outBuffer zout;
+ guchar *out;
+ gsize outlen, r;
+
+ zstream = ZSTD_createDStream();
+ ZSTD_initDStream(zstream);
+
+ zin.pos = 0;
+ zin.src = bytes;
+ zin.size = len;
+
+ if ((outlen = ZSTD_getDecompressedSize(zin.src, zin.size)) == 0) {
+ outlen = ZSTD_DStreamOutSize();
+ }
+
+ out = g_malloc(outlen);
+
+ zout.dst = out;
+ zout.pos = 0;
+ zout.size = outlen;
+
+ while (zin.pos < zin.size) {
+ r = ZSTD_decompressStream(zstream, &zout, &zin);
+
+ if (ZSTD_isError(r)) {
+ msg_err_map("%s: cannot decompress data: %s",
+ map->name,
+ ZSTD_getErrorName(r));
+ ZSTD_freeDStream(zstream);
+ g_free(out);
+
+ return FALSE;
+ }
+
+ if (zout.pos == zout.size) {
+ /* We need to extend output buffer */
+ zout.size = zout.size * 2 + 1;
+ out = g_realloc(zout.dst, zout.size);
+ zout.dst = out;
+ }
+ }
+
+ ZSTD_freeDStream(zstream);
+ msg_info_map("%s: read map data, %z bytes compressed, "
+ "%z uncompressed)",
+ map->name,
+ len, zout.pos);
+ map->read_callback(out, zout.pos, &periodic->cbdata, TRUE);
+ g_free(out);
+ }
+ else {
+ msg_info_map("%s: read map data, %z bytes",
+ map->name, len);
+ map->read_callback(bytes, len, &periodic->cbdata, TRUE);
+ }
+ }
+ else {
+ map->read_callback(NULL, 0, &periodic->cbdata, TRUE);
+ }
+
+ data->processed = TRUE;
+
+ return TRUE;
+}
+
+static void
+rspamd_map_periodic_dtor(struct map_periodic_cbdata *periodic)
+{
+ struct rspamd_map *map;
+
+ map = periodic->map;
+ msg_debug_map("periodic dtor %p", periodic);
+
+ if (periodic->need_modify || periodic->cbdata.errored) {
+ /* Need to notify the real data structure */
+ periodic->map->fin_callback(&periodic->cbdata, periodic->map->user_data);
+
+ if (map->on_load_function) {
+ map->on_load_function(map, map->on_load_ud);
+ }
+ }
+ else {
+ /* Not modified */
+ }
+
+ if (periodic->locked) {
+ g_atomic_int_set(periodic->map->locked, 0);
+ msg_debug_map("unlocked map %s", periodic->map->name);
+
+ if (periodic->map->wrk->state == rspamd_worker_state_running) {
+ rspamd_map_schedule_periodic(periodic->map,
+ RSPAMD_SYMBOL_RESULT_NORMAL);
+ }
+ else {
+ msg_debug_map("stop scheduling periodics for %s; terminating state",
+ periodic->map->name);
+ }
+ }
+
+ g_free(periodic);
+}
+
+/* Called on timer execution */
+static void
+rspamd_map_periodic_callback(struct ev_loop *loop, ev_timer *w, int revents)
+{
+ struct map_periodic_cbdata *cbd = (struct map_periodic_cbdata *) w->data;
+
+ MAP_RETAIN(cbd, "periodic");
+ ev_timer_stop(loop, w);
+ rspamd_map_process_periodic(cbd);
+ MAP_RELEASE(cbd, "periodic");
+}
+
+static void
+rspamd_map_schedule_periodic(struct rspamd_map *map, int how)
+{
+ const gdouble error_mult = 20.0, lock_mult = 0.1;
+ static const gdouble min_timer_interval = 2.0;
+ const gchar *reason = "unknown reason";
+ gdouble jittered_sec;
+ gdouble timeout;
+ struct map_periodic_cbdata *cbd;
+
+ if (map->scheduled_check || (map->wrk &&
+ map->wrk->state != rspamd_worker_state_running)) {
+ /*
+ * Do not schedule check if some check is already scheduled or
+ * if worker is going to die
+ */
+ return;
+ }
+
+ if (!(how & RSPAMD_MAP_SCHEDULE_INIT) && map->static_only) {
+ /* No need to schedule anything for static maps */
+ return;
+ }
+
+ if (map->non_trivial && map->next_check != 0) {
+ timeout = map->next_check - rspamd_get_calendar_ticks();
+ map->next_check = 0;
+
+ if (timeout > 0 && timeout < map->poll_timeout) {
+ /* Early check case, jitter */
+ gdouble poll_timeout = map->poll_timeout;
+
+ if (how & RSPAMD_MAP_SCHEDULE_ERROR) {
+ poll_timeout = map->poll_timeout * error_mult;
+ reason = "early active non-trivial check (after error)";
+ }
+ else if (how & RSPAMD_MAP_SCHEDULE_LOCKED) {
+ poll_timeout = map->poll_timeout * lock_mult;
+ reason = "early active non-trivial check (after being locked)";
+ }
+ else {
+ reason = "early active non-trivial check";
+ }
+
+ jittered_sec = MIN(timeout, poll_timeout);
+ }
+ else if (timeout <= 0) {
+ /* Data is already expired, need to check */
+ if (how & RSPAMD_MAP_SCHEDULE_ERROR) {
+ /* In case of error we still need to increase delay */
+ jittered_sec = map->poll_timeout * error_mult;
+ reason = "expired non-trivial data (after error)";
+ }
+ else {
+ jittered_sec = 0.0;
+ reason = "expired non-trivial data";
+ }
+ }
+ else {
+ /* No need to check now, wait till next_check */
+ jittered_sec = timeout;
+ reason = "valid non-trivial data";
+ }
+ }
+ else {
+ /* No valid information when to check a map, plan a timer based check */
+ timeout = map->poll_timeout;
+
+ if (how & RSPAMD_MAP_SCHEDULE_INIT) {
+ if (map->active_http) {
+ /* Spill maps load to get better chances to hit ssl cache */
+ timeout = rspamd_time_jitter(0.0, 2.0);
+ }
+ else {
+ timeout = 0.0;
+ }
+
+ reason = "init scheduled check";
+ }
+ else {
+ if (how & RSPAMD_MAP_SCHEDULE_ERROR) {
+ timeout = map->poll_timeout * error_mult;
+ reason = "errored scheduled check";
+ }
+ else if (how & RSPAMD_MAP_SCHEDULE_LOCKED) {
+ timeout = map->poll_timeout * lock_mult;
+ reason = "locked scheduled check";
+ }
+ else {
+ reason = "normal scheduled check";
+ }
+ }
+
+ jittered_sec = rspamd_time_jitter(timeout, 0);
+ }
+
+ /* Now, we do some sanity checks for jittered seconds */
+ if (!(how & RSPAMD_MAP_SCHEDULE_INIT)) {
+ /* Never allow too low interval between timer checks, it is expensive */
+ if (jittered_sec < min_timer_interval) {
+ jittered_sec = rspamd_time_jitter(min_timer_interval, 0);
+ }
+
+ if (map->non_trivial) {
+ /*
+ * Even if we are reported that we need to reload cache often, we
+ * still want to be sane in terms of events...
+ */
+ if (jittered_sec < min_timer_interval * 2.0) {
+ if (map->nelts > 0) {
+ jittered_sec = min_timer_interval * 3.0;
+ }
+ }
+ }
+ }
+
+ cbd = g_malloc0(sizeof(*cbd));
+ cbd->cbdata.prev_data = *map->user_data;
+ cbd->cbdata.cur_data = NULL;
+ cbd->cbdata.map = map;
+ cbd->map = map;
+ map->scheduled_check = cbd;
+ REF_INIT_RETAIN(cbd, rspamd_map_periodic_dtor);
+
+ cbd->ev.data = cbd;
+ ev_timer_init(&cbd->ev, rspamd_map_periodic_callback, jittered_sec, 0.0);
+ ev_timer_start(map->event_loop, &cbd->ev);
+
+ msg_debug_map("schedule new periodic event %p in %.3f seconds for %s; reason: %s",
+ cbd, jittered_sec, map->name, reason);
+}
+
+static gint
+rspamd_map_af_to_weight(const rspamd_inet_addr_t *addr)
+{
+ int ret;
+
+ switch (rspamd_inet_address_get_af(addr)) {
+ case AF_UNIX:
+ ret = 2;
+ break;
+ case AF_INET:
+ ret = 1;
+ break;
+ default:
+ ret = 0;
+ break;
+ }
+
+ return ret;
+}
+
+static gint
+rspamd_map_dns_address_sort_func(gconstpointer a, gconstpointer b)
+{
+ const rspamd_inet_addr_t *ip1 = *(const rspamd_inet_addr_t **) a,
+ *ip2 = *(const rspamd_inet_addr_t **) b;
+ gint w1, w2;
+
+ w1 = rspamd_map_af_to_weight(ip1);
+ w2 = rspamd_map_af_to_weight(ip2);
+
+ /* Inverse order */
+ return w2 - w1;
+}
+
+static void
+rspamd_map_dns_callback(struct rdns_reply *reply, void *arg)
+{
+ struct http_callback_data *cbd = arg;
+ struct rdns_reply_entry *cur_rep;
+ struct rspamd_map *map;
+ guint flags = RSPAMD_HTTP_CLIENT_SIMPLE | RSPAMD_HTTP_CLIENT_SHARED;
+
+ map = cbd->map;
+
+ msg_debug_map("got dns reply with code %s on stage %d",
+ rdns_strerror(reply->code), cbd->stage);
+
+ if (cbd->stage == http_map_terminated) {
+ MAP_RELEASE(cbd, "http_callback_data");
+ return;
+ }
+
+ if (reply->code == RDNS_RC_NOERROR) {
+ DL_FOREACH(reply->entries, cur_rep)
+ {
+ rspamd_inet_addr_t *addr;
+ addr = rspamd_inet_address_from_rnds(cur_rep);
+
+ if (addr != NULL) {
+ rspamd_inet_address_set_port(addr, cbd->data->port);
+ g_ptr_array_add(cbd->addrs, (void *) addr);
+ }
+ }
+
+ if (cbd->stage == http_map_resolve_host2) {
+ /* We have still one request pending */
+ cbd->stage = http_map_resolve_host1;
+ }
+ else if (cbd->stage == http_map_resolve_host1) {
+ cbd->stage = http_map_http_conn;
+ }
+ }
+ else if (cbd->stage < http_map_http_conn) {
+ if (cbd->stage == http_map_resolve_host2) {
+ /* We have still one request pending */
+ cbd->stage = http_map_resolve_host1;
+ }
+ else if (cbd->addrs->len == 0) {
+ /* We could not resolve host, so cowardly fail here */
+ msg_err_map("cannot resolve %s: %s", cbd->data->host,
+ rdns_strerror(reply->code));
+ cbd->periodic->errored = 1;
+ rspamd_map_process_periodic(cbd->periodic);
+ }
+ else {
+ /* We have at least one address, so we can continue... */
+ cbd->stage = http_map_http_conn;
+ }
+ }
+
+ if (cbd->stage == http_map_http_conn && cbd->addrs->len > 0) {
+ rspamd_ptr_array_shuffle(cbd->addrs);
+ gint idx = 0;
+ /*
+ * For the existing addr we can just select any address as we have
+ * data available
+ */
+ if (cbd->map->nelts > 0 && rspamd_random_double_fast() > 0.5) {
+ /* Already shuffled, use whatever is the first */
+ cbd->addr = (rspamd_inet_addr_t *) g_ptr_array_index(cbd->addrs, idx);
+ }
+ else {
+ /* Always prefer IPv4 as IPv6 is almost all the time broken */
+ g_ptr_array_sort(cbd->addrs, rspamd_map_dns_address_sort_func);
+ cbd->addr = (rspamd_inet_addr_t *) g_ptr_array_index(cbd->addrs, idx);
+ }
+
+ retry:
+ msg_debug_map("try open http connection to %s",
+ rspamd_inet_address_to_string_pretty(cbd->addr));
+ if (cbd->bk->protocol == MAP_PROTO_HTTPS) {
+ flags |= RSPAMD_HTTP_CLIENT_SSL;
+ }
+ cbd->conn = rspamd_http_connection_new_client(NULL,
+ NULL,
+ http_map_error,
+ http_map_finish,
+ flags,
+ cbd->addr);
+
+ if (cbd->conn != NULL) {
+ write_http_request(cbd);
+ }
+ else {
+ if (idx < cbd->addrs->len - 1) {
+ /* We can retry */
+ idx++;
+ rspamd_inet_addr_t *prev_addr = cbd->addr;
+ cbd->addr = (rspamd_inet_addr_t *) g_ptr_array_index(cbd->addrs, idx);
+ msg_info_map("cannot connect to %s to get data for %s: %s, retry with %s (%d of %d)",
+ rspamd_inet_address_to_string_pretty(prev_addr),
+ cbd->bk->uri,
+ strerror(errno),
+ rspamd_inet_address_to_string_pretty(cbd->addr),
+ idx + 1, cbd->addrs->len);
+ goto retry;
+ }
+ else {
+ /* Nothing else left */
+ cbd->periodic->errored = TRUE;
+ msg_err_map("error reading %s(%s): "
+ "connection with http server terminated incorrectly: %s",
+ cbd->bk->uri,
+ cbd->addr ? rspamd_inet_address_to_string_pretty(cbd->addr) : "",
+ strerror(errno));
+
+ rspamd_map_process_periodic(cbd->periodic);
+ }
+ }
+ }
+
+ MAP_RELEASE(cbd, "http_callback_data");
+}
+
+static gboolean
+rspamd_map_read_cached(struct rspamd_map *map, struct rspamd_map_backend *bk,
+ struct map_periodic_cbdata *periodic, const gchar *host)
+{
+ gsize mmap_len, len;
+ gpointer in;
+ struct http_map_data *data;
+
+ data = bk->data.hd;
+
+ in = rspamd_shmem_xmap(data->cache->shmem_name, PROT_READ, &mmap_len);
+
+ if (in == NULL) {
+ msg_err("cannot map cache from %s: %s", data->cache->shmem_name,
+ strerror(errno));
+ return FALSE;
+ }
+
+ if (mmap_len < data->cache->len) {
+ msg_err("cannot map cache from %s: truncated length %z, %z expected",
+ data->cache->shmem_name,
+ mmap_len, data->cache->len);
+ munmap(in, mmap_len);
+
+ return FALSE;
+ }
+
+ /*
+ * Len is taken from the shmem file size that can be larger than the
+ * actual data length, as we use shared memory as a growing buffer for the
+ * HTTP input.
+ * Hence, we need to use len from the saved cache data, counting that it is
+ * at least not more than the cached file length (this is checked above).
+ */
+ len = data->cache->len;
+
+ if (bk->is_compressed) {
+ ZSTD_DStream *zstream;
+ ZSTD_inBuffer zin;
+ ZSTD_outBuffer zout;
+ guchar *out;
+ gsize outlen, r;
+
+ zstream = ZSTD_createDStream();
+ ZSTD_initDStream(zstream);
+
+ zin.pos = 0;
+ zin.src = in;
+ zin.size = len;
+
+ if ((outlen = ZSTD_getDecompressedSize(zin.src, zin.size)) == 0) {
+ outlen = ZSTD_DStreamOutSize();
+ }
+
+ out = g_malloc(outlen);
+
+ zout.dst = out;
+ zout.pos = 0;
+ zout.size = outlen;
+
+ while (zin.pos < zin.size) {
+ r = ZSTD_decompressStream(zstream, &zout, &zin);
+
+ if (ZSTD_isError(r)) {
+ msg_err_map("%s: cannot decompress data: %s",
+ bk->uri,
+ ZSTD_getErrorName(r));
+ ZSTD_freeDStream(zstream);
+ g_free(out);
+ munmap(in, mmap_len);
+ return FALSE;
+ }
+
+ if (zout.pos == zout.size) {
+ /* We need to extend output buffer */
+ zout.size = zout.size * 2 + 1;
+ out = g_realloc(zout.dst, zout.size);
+ zout.dst = out;
+ }
+ }
+
+ ZSTD_freeDStream(zstream);
+ msg_info_map("%s: read map data cached %z bytes compressed, "
+ "%z uncompressed",
+ bk->uri,
+ len, zout.pos);
+ map->read_callback(out, zout.pos, &periodic->cbdata, TRUE);
+ g_free(out);
+ }
+ else {
+ msg_info_map("%s: read map data cached %z bytes", bk->uri, len);
+ map->read_callback(in, len, &periodic->cbdata, TRUE);
+ }
+
+ munmap(in, mmap_len);
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_map_has_http_cached_file(struct rspamd_map *map,
+ struct rspamd_map_backend *bk)
+{
+ gchar path[PATH_MAX];
+ guchar digest[rspamd_cryptobox_HASHBYTES];
+ struct rspamd_config *cfg = map->cfg;
+ struct stat st;
+
+ if (cfg->maps_cache_dir == NULL || cfg->maps_cache_dir[0] == '\0') {
+ return FALSE;
+ }
+
+ rspamd_cryptobox_hash(digest, bk->uri, strlen(bk->uri), NULL, 0);
+ rspamd_snprintf(path, sizeof(path), "%s%c%*xs.map", cfg->maps_cache_dir,
+ G_DIR_SEPARATOR, 20, digest);
+
+ if (stat(path, &st) != -1 && st.st_size >
+ sizeof(struct rspamd_http_file_data)) {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+rspamd_map_save_http_cached_file(struct rspamd_map *map,
+ struct rspamd_map_backend *bk,
+ struct http_map_data *htdata,
+ const guchar *data,
+ gsize len)
+{
+ gchar path[PATH_MAX];
+ guchar digest[rspamd_cryptobox_HASHBYTES];
+ struct rspamd_config *cfg = map->cfg;
+ gint fd;
+ struct rspamd_http_file_data header;
+
+ if (cfg->maps_cache_dir == NULL || cfg->maps_cache_dir[0] == '\0') {
+ return FALSE;
+ }
+
+ rspamd_cryptobox_hash(digest, bk->uri, strlen(bk->uri), NULL, 0);
+ rspamd_snprintf(path, sizeof(path), "%s%c%*xs.map", cfg->maps_cache_dir,
+ G_DIR_SEPARATOR, 20, digest);
+
+ fd = rspamd_file_xopen(path, O_WRONLY | O_TRUNC | O_CREAT,
+ 00600, FALSE);
+
+ if (fd == -1) {
+ return FALSE;
+ }
+
+ if (!rspamd_file_lock(fd, FALSE)) {
+ msg_err_map("cannot lock file %s: %s", path, strerror(errno));
+ close(fd);
+
+ return FALSE;
+ }
+
+ memcpy(header.magic, rspamd_http_file_magic, sizeof(rspamd_http_file_magic));
+ header.mtime = htdata->last_modified;
+ header.next_check = map->next_check;
+ header.data_off = sizeof(header);
+
+ if (htdata->etag) {
+ header.data_off += RSPAMD_FSTRING_LEN(htdata->etag);
+ header.etag_len = RSPAMD_FSTRING_LEN(htdata->etag);
+ }
+ else {
+ header.etag_len = 0;
+ }
+
+ if (write(fd, &header, sizeof(header)) != sizeof(header)) {
+ msg_err_map("cannot write file %s (header stage): %s", path, strerror(errno));
+ rspamd_file_unlock(fd, FALSE);
+ close(fd);
+
+ return FALSE;
+ }
+
+ if (header.etag_len > 0) {
+ if (write(fd, RSPAMD_FSTRING_DATA(htdata->etag), header.etag_len) !=
+ header.etag_len) {
+ msg_err_map("cannot write file %s (etag stage): %s", path, strerror(errno));
+ rspamd_file_unlock(fd, FALSE);
+ close(fd);
+
+ return FALSE;
+ }
+ }
+
+ /* Now write the rest */
+ if (write(fd, data, len) != len) {
+ msg_err_map("cannot write file %s (data stage): %s", path, strerror(errno));
+ rspamd_file_unlock(fd, FALSE);
+ close(fd);
+
+ return FALSE;
+ }
+
+ rspamd_file_unlock(fd, FALSE);
+ close(fd);
+
+ msg_info_map("saved data from %s in %s, %uz bytes", bk->uri, path, len + sizeof(header) + header.etag_len);
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_map_update_http_cached_file(struct rspamd_map *map,
+ struct rspamd_map_backend *bk,
+ struct http_map_data *htdata)
+{
+ gchar path[PATH_MAX];
+ guchar digest[rspamd_cryptobox_HASHBYTES];
+ struct rspamd_config *cfg = map->cfg;
+ gint fd;
+ struct rspamd_http_file_data header;
+
+ if (!rspamd_map_has_http_cached_file(map, bk)) {
+ return FALSE;
+ }
+
+ rspamd_cryptobox_hash(digest, bk->uri, strlen(bk->uri), NULL, 0);
+ rspamd_snprintf(path, sizeof(path), "%s%c%*xs.map", cfg->maps_cache_dir,
+ G_DIR_SEPARATOR, 20, digest);
+
+ fd = rspamd_file_xopen(path, O_WRONLY,
+ 00600, FALSE);
+
+ if (fd == -1) {
+ return FALSE;
+ }
+
+ if (!rspamd_file_lock(fd, FALSE)) {
+ msg_err_map("cannot lock file %s: %s", path, strerror(errno));
+ close(fd);
+
+ return FALSE;
+ }
+
+ memcpy(header.magic, rspamd_http_file_magic, sizeof(rspamd_http_file_magic));
+ header.mtime = htdata->last_modified;
+ header.next_check = map->next_check;
+ header.data_off = sizeof(header);
+
+ if (htdata->etag) {
+ header.data_off += RSPAMD_FSTRING_LEN(htdata->etag);
+ header.etag_len = RSPAMD_FSTRING_LEN(htdata->etag);
+ }
+ else {
+ header.etag_len = 0;
+ }
+
+ if (write(fd, &header, sizeof(header)) != sizeof(header)) {
+ msg_err_map("cannot update file %s (header stage): %s", path, strerror(errno));
+ rspamd_file_unlock(fd, FALSE);
+ close(fd);
+
+ return FALSE;
+ }
+
+ if (header.etag_len > 0) {
+ if (write(fd, RSPAMD_FSTRING_DATA(htdata->etag), header.etag_len) !=
+ header.etag_len) {
+ msg_err_map("cannot update file %s (etag stage): %s", path, strerror(errno));
+ rspamd_file_unlock(fd, FALSE);
+ close(fd);
+
+ return FALSE;
+ }
+ }
+
+ rspamd_file_unlock(fd, FALSE);
+ close(fd);
+
+ return TRUE;
+}
+
+
+static gboolean
+rspamd_map_read_http_cached_file(struct rspamd_map *map,
+ struct rspamd_map_backend *bk,
+ struct http_map_data *htdata,
+ struct map_cb_data *cbdata)
+{
+ gchar path[PATH_MAX];
+ guchar digest[rspamd_cryptobox_HASHBYTES];
+ struct rspamd_config *cfg = map->cfg;
+ gint fd;
+ struct stat st;
+ struct rspamd_http_file_data header;
+
+ if (cfg->maps_cache_dir == NULL || cfg->maps_cache_dir[0] == '\0') {
+ return FALSE;
+ }
+
+ rspamd_cryptobox_hash(digest, bk->uri, strlen(bk->uri), NULL, 0);
+ rspamd_snprintf(path, sizeof(path), "%s%c%*xs.map", cfg->maps_cache_dir,
+ G_DIR_SEPARATOR, 20, digest);
+
+ fd = rspamd_file_xopen(path, O_RDONLY, 00600, FALSE);
+
+ if (fd == -1) {
+ return FALSE;
+ }
+
+ if (!rspamd_file_lock(fd, FALSE)) {
+ msg_err_map("cannot lock file %s: %s", path, strerror(errno));
+ close(fd);
+
+ return FALSE;
+ }
+
+ (void) fstat(fd, &st);
+
+ if (read(fd, &header, sizeof(header)) != sizeof(header)) {
+ msg_err_map("cannot read file %s (header stage): %s", path, strerror(errno));
+ rspamd_file_unlock(fd, FALSE);
+ close(fd);
+
+ return FALSE;
+ }
+
+ if (memcmp(header.magic, rspamd_http_file_magic,
+ sizeof(rspamd_http_file_magic)) != 0) {
+ msg_warn_map("invalid or old version magic in file %s; ignore it", path);
+ rspamd_file_unlock(fd, FALSE);
+ close(fd);
+
+ return FALSE;
+ }
+
+ double now = rspamd_get_calendar_ticks();
+
+ if (header.next_check > now) {
+ map->next_check = header.next_check;
+ }
+ else {
+ map->next_check = now;
+ }
+
+ htdata->last_modified = header.mtime;
+
+ if (header.etag_len > 0) {
+ rspamd_fstring_t *etag = rspamd_fstring_sized_new(header.etag_len);
+
+ if (read(fd, RSPAMD_FSTRING_DATA(etag), header.etag_len) != header.etag_len) {
+ msg_err_map("cannot read file %s (etag stage): %s", path,
+ strerror(errno));
+ rspamd_file_unlock(fd, FALSE);
+ rspamd_fstring_free(etag);
+ close(fd);
+
+ return FALSE;
+ }
+
+ etag->len = header.etag_len;
+
+ if (htdata->etag) {
+ /* FIXME: should be dealt somehow better */
+ msg_warn_map("etag is already defined as %V; cached is %V; ignore cached",
+ htdata->etag, etag);
+ rspamd_fstring_free(etag);
+ }
+ else {
+ htdata->etag = etag;
+ }
+ }
+
+ rspamd_file_unlock(fd, FALSE);
+ close(fd);
+
+ /* Now read file data */
+ /* Perform buffered read: fail-safe */
+ if (!read_map_file_chunks(map, cbdata, path,
+ st.st_size - header.data_off, header.data_off)) {
+ return FALSE;
+ }
+
+ struct tm tm;
+ gchar ncheck_buf[32], lm_buf[32];
+
+ rspamd_localtime(map->next_check, &tm);
+ strftime(ncheck_buf, sizeof(ncheck_buf) - 1, "%Y-%m-%d %H:%M:%S", &tm);
+ rspamd_localtime(htdata->last_modified, &tm);
+ strftime(lm_buf, sizeof(lm_buf) - 1, "%Y-%m-%d %H:%M:%S", &tm);
+
+ msg_info_map("read cached data for %s from %s, %uz bytes; next check at: %s;"
+ " last modified on: %s; etag: %V",
+ bk->uri,
+ path,
+ (size_t) (st.st_size - header.data_off),
+ ncheck_buf,
+ lm_buf,
+ htdata->etag);
+
+ return TRUE;
+}
+
+/**
+ * Async HTTP callback
+ */
+static void
+rspamd_map_common_http_callback(struct rspamd_map *map,
+ struct rspamd_map_backend *bk,
+ struct map_periodic_cbdata *periodic,
+ gboolean check)
+{
+ struct http_map_data *data;
+ struct http_callback_data *cbd;
+ guint flags = RSPAMD_HTTP_CLIENT_SIMPLE | RSPAMD_HTTP_CLIENT_SHARED;
+
+ data = bk->data.hd;
+
+ if (g_atomic_int_get(&data->cache->available) == 1) {
+ /* Read cached data */
+ if (check) {
+ if (data->last_modified < data->cache->last_modified) {
+ msg_info_map("need to reread cached map triggered by %s "
+ "(%d our modify time, %d cached modify time)",
+ bk->uri,
+ (int) data->last_modified,
+ (int) data->cache->last_modified);
+ periodic->need_modify = TRUE;
+ /* Reset the whole chain */
+ periodic->cur_backend = 0;
+ rspamd_map_process_periodic(periodic);
+ }
+ else {
+ if (map->active_http) {
+ /* Check even if there is a cached version */
+ goto check;
+ }
+ else {
+ /* Switch to the next backend */
+ periodic->cur_backend++;
+ rspamd_map_process_periodic(periodic);
+ }
+ }
+
+ return;
+ }
+ else {
+ if (map->active_http &&
+ data->last_modified > data->cache->last_modified) {
+ goto check;
+ }
+ else if (rspamd_map_read_cached(map, bk, periodic, data->host)) {
+ /* Switch to the next backend */
+ periodic->cur_backend++;
+ data->last_modified = data->cache->last_modified;
+ rspamd_map_process_periodic(periodic);
+
+ return;
+ }
+ }
+ }
+ else if (!map->active_http) {
+ /* Switch to the next backend */
+ periodic->cur_backend++;
+ rspamd_map_process_periodic(periodic);
+
+ return;
+ }
+
+check:
+ cbd = g_malloc0(sizeof(struct http_callback_data));
+
+ cbd->event_loop = map->event_loop;
+ cbd->addrs = g_ptr_array_sized_new(4);
+ cbd->map = map;
+ cbd->data = data;
+ cbd->check = check;
+ cbd->periodic = periodic;
+ MAP_RETAIN(periodic, "periodic");
+ cbd->bk = bk;
+ MAP_RETAIN(bk, "rspamd_map_backend");
+ cbd->stage = http_map_terminated;
+ REF_INIT_RETAIN(cbd, free_http_cbdata);
+
+ msg_debug_map("%s map data from %s", check ? "checking" : "reading",
+ data->host);
+
+ /* Try address */
+ rspamd_inet_addr_t *addr = NULL;
+
+ if (rspamd_parse_inet_address(&addr, data->host,
+ strlen(data->host), RSPAMD_INET_ADDRESS_PARSE_DEFAULT)) {
+ rspamd_inet_address_set_port(addr, cbd->data->port);
+ g_ptr_array_add(cbd->addrs, (void *) addr);
+
+ if (bk->protocol == MAP_PROTO_HTTPS) {
+ flags |= RSPAMD_HTTP_CLIENT_SSL;
+ }
+
+ cbd->conn = rspamd_http_connection_new_client(
+ NULL,
+ NULL,
+ http_map_error,
+ http_map_finish,
+ flags,
+ addr);
+
+ if (cbd->conn != NULL) {
+ cbd->stage = http_map_http_conn;
+ write_http_request(cbd);
+ cbd->addr = addr;
+ MAP_RELEASE(cbd, "http_callback_data");
+ }
+ else {
+ msg_warn_map("cannot load map: cannot connect to %s: %s",
+ data->host, strerror(errno));
+ MAP_RELEASE(cbd, "http_callback_data");
+ }
+
+ return;
+ }
+ else if (map->r->r) {
+ /* Send both A and AAAA requests */
+ guint nreq = 0;
+
+ if (rdns_make_request_full(map->r->r, rspamd_map_dns_callback, cbd,
+ map->cfg->dns_timeout, map->cfg->dns_retransmits, 1,
+ data->host, RDNS_REQUEST_A)) {
+ MAP_RETAIN(cbd, "http_callback_data");
+ nreq++;
+ }
+ if (rdns_make_request_full(map->r->r, rspamd_map_dns_callback, cbd,
+ map->cfg->dns_timeout, map->cfg->dns_retransmits, 1,
+ data->host, RDNS_REQUEST_AAAA)) {
+ MAP_RETAIN(cbd, "http_callback_data");
+ nreq++;
+ }
+
+ if (nreq == 2) {
+ cbd->stage = http_map_resolve_host2;
+ }
+ else if (nreq == 1) {
+ cbd->stage = http_map_resolve_host1;
+ }
+
+ map->tmp_dtor = free_http_cbdata_dtor;
+ map->tmp_dtor_data = cbd;
+ }
+ else {
+ msg_warn_map("cannot load map: DNS resolver is not initialized");
+ cbd->periodic->errored = TRUE;
+ }
+
+ MAP_RELEASE(cbd, "http_callback_data");
+}
+
+static void
+rspamd_map_http_check_callback(struct map_periodic_cbdata *cbd)
+{
+ struct rspamd_map *map;
+ struct rspamd_map_backend *bk;
+
+ map = cbd->map;
+ bk = g_ptr_array_index(cbd->map->backends, cbd->cur_backend);
+
+ rspamd_map_common_http_callback(map, bk, cbd, TRUE);
+}
+
+static void
+rspamd_map_http_read_callback(struct map_periodic_cbdata *cbd)
+{
+ struct rspamd_map *map;
+ struct rspamd_map_backend *bk;
+
+ map = cbd->map;
+ bk = g_ptr_array_index(cbd->map->backends, cbd->cur_backend);
+ rspamd_map_common_http_callback(map, bk, cbd, FALSE);
+}
+
+static void
+rspamd_map_file_check_callback(struct map_periodic_cbdata *periodic)
+{
+ struct rspamd_map *map;
+ struct file_map_data *data;
+ struct rspamd_map_backend *bk;
+
+ map = periodic->map;
+ bk = g_ptr_array_index(map->backends, periodic->cur_backend);
+ data = bk->data.fd;
+
+ if (data->need_modify) {
+ periodic->need_modify = TRUE;
+ periodic->cur_backend = 0;
+ data->need_modify = FALSE;
+
+ rspamd_map_process_periodic(periodic);
+
+ return;
+ }
+
+ map = periodic->map;
+ /* Switch to the next backend as the rest is handled by ev_stat */
+ periodic->cur_backend++;
+ rspamd_map_process_periodic(periodic);
+}
+
+static void
+rspamd_map_static_check_callback(struct map_periodic_cbdata *periodic)
+{
+ struct rspamd_map *map;
+ struct static_map_data *data;
+ struct rspamd_map_backend *bk;
+
+ map = periodic->map;
+ bk = g_ptr_array_index(map->backends, periodic->cur_backend);
+ data = bk->data.sd;
+
+ if (!data->processed) {
+ periodic->need_modify = TRUE;
+ periodic->cur_backend = 0;
+
+ rspamd_map_process_periodic(periodic);
+
+ return;
+ }
+
+ /* Switch to the next backend */
+ periodic->cur_backend++;
+ rspamd_map_process_periodic(periodic);
+}
+
+static void
+rspamd_map_file_read_callback(struct map_periodic_cbdata *periodic)
+{
+ struct rspamd_map *map;
+ struct file_map_data *data;
+ struct rspamd_map_backend *bk;
+
+ map = periodic->map;
+
+ bk = g_ptr_array_index(map->backends, periodic->cur_backend);
+ data = bk->data.fd;
+
+ msg_info_map("rereading map file %s", data->filename);
+
+ if (!read_map_file(map, data, bk, periodic)) {
+ periodic->errored = TRUE;
+ }
+
+ /* Switch to the next backend */
+ periodic->cur_backend++;
+ rspamd_map_process_periodic(periodic);
+}
+
+static void
+rspamd_map_static_read_callback(struct map_periodic_cbdata *periodic)
+{
+ struct rspamd_map *map;
+ struct static_map_data *data;
+ struct rspamd_map_backend *bk;
+
+ map = periodic->map;
+
+ bk = g_ptr_array_index(map->backends, periodic->cur_backend);
+ data = bk->data.sd;
+
+ msg_info_map("rereading static map");
+
+ if (!read_map_static(map, data, bk, periodic)) {
+ periodic->errored = TRUE;
+ }
+
+ /* Switch to the next backend */
+ periodic->cur_backend++;
+ rspamd_map_process_periodic(periodic);
+}
+
+static void
+rspamd_map_process_periodic(struct map_periodic_cbdata *cbd)
+{
+ struct rspamd_map_backend *bk;
+ struct rspamd_map *map;
+
+ map = cbd->map;
+ map->scheduled_check = NULL;
+
+ if (!map->file_only && !cbd->locked) {
+ if (!g_atomic_int_compare_and_exchange(cbd->map->locked,
+ 0, 1)) {
+ msg_debug_map(
+ "don't try to reread map %s as it is locked by other process, "
+ "will reread it later",
+ cbd->map->name);
+ rspamd_map_schedule_periodic(map, RSPAMD_MAP_SCHEDULE_LOCKED);
+ MAP_RELEASE(cbd, "periodic");
+
+ return;
+ }
+ else {
+ msg_debug_map("locked map %s", cbd->map->name);
+ cbd->locked = TRUE;
+ }
+ }
+
+ if (cbd->errored) {
+ /* We should not check other backends if some backend has failed*/
+ rspamd_map_schedule_periodic(cbd->map, RSPAMD_MAP_SCHEDULE_ERROR);
+
+ if (cbd->locked) {
+ g_atomic_int_set(cbd->map->locked, 0);
+ cbd->locked = FALSE;
+ }
+
+ /* Also set error flag for the map consumer */
+ cbd->cbdata.errored = true;
+
+ msg_debug_map("unlocked map %s, refcount=%d", cbd->map->name,
+ cbd->ref.refcount);
+ MAP_RELEASE(cbd, "periodic");
+
+ return;
+ }
+
+ /* For each backend we need to check for modifications */
+ if (cbd->cur_backend >= cbd->map->backends->len) {
+ /* Last backend */
+ msg_debug_map("finished map: %d of %d", cbd->cur_backend,
+ cbd->map->backends->len);
+ MAP_RELEASE(cbd, "periodic");
+
+ return;
+ }
+
+ if (cbd->map->wrk && cbd->map->wrk->state == rspamd_worker_state_running) {
+ bk = g_ptr_array_index(cbd->map->backends, cbd->cur_backend);
+ g_assert(bk != NULL);
+
+ if (cbd->need_modify) {
+ /* Load data from the next backend */
+ switch (bk->protocol) {
+ case MAP_PROTO_HTTP:
+ case MAP_PROTO_HTTPS:
+ rspamd_map_http_read_callback(cbd);
+ break;
+ case MAP_PROTO_FILE:
+ rspamd_map_file_read_callback(cbd);
+ break;
+ case MAP_PROTO_STATIC:
+ rspamd_map_static_read_callback(cbd);
+ break;
+ }
+ }
+ else {
+ /* Check the next backend */
+ switch (bk->protocol) {
+ case MAP_PROTO_HTTP:
+ case MAP_PROTO_HTTPS:
+ rspamd_map_http_check_callback(cbd);
+ break;
+ case MAP_PROTO_FILE:
+ rspamd_map_file_check_callback(cbd);
+ break;
+ case MAP_PROTO_STATIC:
+ rspamd_map_static_check_callback(cbd);
+ break;
+ }
+ }
+ }
+}
+
+static void
+rspamd_map_on_stat(struct ev_loop *loop, ev_stat *w, int revents)
+{
+ struct rspamd_map *map = (struct rspamd_map *) w->data;
+
+ if (w->attr.st_nlink > 0) {
+ msg_info_map("old mtime is %t (size = %Hz), "
+ "new mtime is %t (size = %Hz) for map file %s",
+ w->prev.st_mtime, (gsize) w->prev.st_size,
+ w->attr.st_mtime, (gsize) w->attr.st_size,
+ w->path);
+
+ /* Fire need modify flag */
+ struct rspamd_map_backend *bk;
+ guint i;
+
+ PTR_ARRAY_FOREACH(map->backends, i, bk)
+ {
+ if (bk->protocol == MAP_PROTO_FILE) {
+ bk->data.fd->need_modify = TRUE;
+ }
+ }
+
+ map->next_check = 0;
+
+ if (map->scheduled_check) {
+ ev_timer_stop(map->event_loop, &map->scheduled_check->ev);
+ MAP_RELEASE(map->scheduled_check, "rspamd_map_on_stat");
+ map->scheduled_check = NULL;
+ }
+
+ rspamd_map_schedule_periodic(map, RSPAMD_MAP_SCHEDULE_INIT);
+ }
+}
+
+/* Start watching event for all maps */
+void rspamd_map_watch(struct rspamd_config *cfg,
+ struct ev_loop *event_loop,
+ struct rspamd_dns_resolver *resolver,
+ struct rspamd_worker *worker,
+ enum rspamd_map_watch_type how)
+{
+ GList *cur = cfg->maps;
+ struct rspamd_map *map;
+ struct rspamd_map_backend *bk;
+ guint i;
+
+ g_assert(how > RSPAMD_MAP_WATCH_MIN && how < RSPAMD_MAP_WATCH_MAX);
+
+ /* First of all do synced read of data */
+ while (cur) {
+ map = cur->data;
+ map->event_loop = event_loop;
+ map->r = resolver;
+
+ if (map->wrk == NULL && how != RSPAMD_MAP_WATCH_WORKER) {
+ /* Generic scanner map */
+ map->wrk = worker;
+
+ if (how == RSPAMD_MAP_WATCH_PRIMARY_CONTROLLER) {
+ map->active_http = TRUE;
+ }
+ else {
+ map->active_http = FALSE;
+ }
+ }
+ else if (map->wrk != NULL && map->wrk == worker) {
+ /* Map is bound to a specific worker */
+ map->active_http = TRUE;
+ }
+ else {
+ /* Skip map for this worker as irrelevant */
+ cur = g_list_next(cur);
+ continue;
+ }
+
+ if (!map->active_http) {
+ /* Check cached version more frequently as it is cheap */
+
+ if (map->poll_timeout >= cfg->map_timeout &&
+ cfg->map_file_watch_multiplier < 1.0) {
+ map->poll_timeout =
+ map->poll_timeout * cfg->map_file_watch_multiplier;
+ }
+ }
+
+ map->file_only = TRUE;
+ map->static_only = TRUE;
+
+ PTR_ARRAY_FOREACH(map->backends, i, bk)
+ {
+ bk->event_loop = event_loop;
+
+ if (bk->protocol == MAP_PROTO_FILE) {
+ struct file_map_data *data;
+
+ data = bk->data.fd;
+
+ if (map->user_data == NULL || *map->user_data == NULL) {
+ /* Map has not been read, init it's reading if possible */
+ struct stat st;
+
+ if (stat(data->filename, &st) != -1) {
+ data->need_modify = TRUE;
+ }
+ }
+
+ ev_stat_init(&data->st_ev, rspamd_map_on_stat,
+ data->filename, map->poll_timeout * cfg->map_file_watch_multiplier);
+ data->st_ev.data = map;
+ ev_stat_start(event_loop, &data->st_ev);
+ map->static_only = FALSE;
+ }
+ else if ((bk->protocol == MAP_PROTO_HTTP ||
+ bk->protocol == MAP_PROTO_HTTPS)) {
+ if (map->active_http) {
+ map->non_trivial = TRUE;
+ }
+
+ map->static_only = FALSE;
+ map->file_only = FALSE;
+ }
+ }
+
+ rspamd_map_schedule_periodic(map, RSPAMD_MAP_SCHEDULE_INIT);
+
+ cur = g_list_next(cur);
+ }
+}
+
+void rspamd_map_preload(struct rspamd_config *cfg)
+{
+ GList *cur = cfg->maps;
+ struct rspamd_map *map;
+ struct rspamd_map_backend *bk;
+ guint i;
+ gboolean map_ok;
+
+ /* First of all do synced read of data */
+ while (cur) {
+ map = cur->data;
+ map_ok = TRUE;
+
+ PTR_ARRAY_FOREACH(map->backends, i, bk)
+ {
+ if (!(bk->protocol == MAP_PROTO_FILE ||
+ bk->protocol == MAP_PROTO_STATIC)) {
+
+ if (bk->protocol == MAP_PROTO_HTTP ||
+ bk->protocol == MAP_PROTO_HTTPS) {
+ if (!rspamd_map_has_http_cached_file(map, bk)) {
+
+ if (!map->fallback_backend) {
+ map_ok = FALSE;
+ }
+ break;
+ }
+ else {
+ continue; /* We are yet fine */
+ }
+ }
+ map_ok = FALSE;
+ break;
+ }
+ }
+
+ if (map_ok) {
+ struct map_periodic_cbdata fake_cbd;
+ gboolean succeed = TRUE;
+
+ memset(&fake_cbd, 0, sizeof(fake_cbd));
+ fake_cbd.cbdata.state = 0;
+ fake_cbd.cbdata.prev_data = *map->user_data;
+ fake_cbd.cbdata.cur_data = NULL;
+ fake_cbd.cbdata.map = map;
+ fake_cbd.map = map;
+
+ PTR_ARRAY_FOREACH(map->backends, i, bk)
+ {
+ fake_cbd.cur_backend = i;
+
+ if (bk->protocol == MAP_PROTO_FILE) {
+ if (!read_map_file(map, bk->data.fd, bk, &fake_cbd)) {
+ succeed = FALSE;
+ break;
+ }
+ }
+ else if (bk->protocol == MAP_PROTO_STATIC) {
+ if (!read_map_static(map, bk->data.sd, bk, &fake_cbd)) {
+ succeed = FALSE;
+ break;
+ }
+ }
+ else if (bk->protocol == MAP_PROTO_HTTP ||
+ bk->protocol == MAP_PROTO_HTTPS) {
+ if (!rspamd_map_read_http_cached_file(map, bk, bk->data.hd,
+ &fake_cbd.cbdata)) {
+
+ if (map->fallback_backend) {
+ /* Try fallback */
+ g_assert(map->fallback_backend->protocol ==
+ MAP_PROTO_FILE);
+ if (!read_map_file(map,
+ map->fallback_backend->data.fd,
+ map->fallback_backend, &fake_cbd)) {
+ succeed = FALSE;
+ break;
+ }
+ }
+ else {
+ succeed = FALSE;
+ break;
+ }
+ }
+ }
+ else {
+ g_assert_not_reached();
+ }
+ }
+
+ if (succeed) {
+ map->fin_callback(&fake_cbd.cbdata, map->user_data);
+
+ if (map->on_load_function) {
+ map->on_load_function(map, map->on_load_ud);
+ }
+ }
+ else {
+ msg_info_map("preload of %s failed", map->name);
+ }
+ }
+
+ cur = g_list_next(cur);
+ }
+}
+
+void rspamd_map_remove_all(struct rspamd_config *cfg)
+{
+ struct rspamd_map *map;
+ GList *cur;
+ struct rspamd_map_backend *bk;
+ struct map_cb_data cbdata;
+ guint i;
+
+ for (cur = cfg->maps; cur != NULL; cur = g_list_next(cur)) {
+ map = cur->data;
+
+ if (map->tmp_dtor) {
+ map->tmp_dtor(map->tmp_dtor_data);
+ }
+
+ if (map->dtor) {
+ cbdata.prev_data = NULL;
+ cbdata.map = map;
+ cbdata.cur_data = *map->user_data;
+
+ map->dtor(&cbdata);
+ *map->user_data = NULL;
+ }
+
+ if (map->on_load_ud_dtor) {
+ map->on_load_ud_dtor(map->on_load_ud);
+ }
+
+ for (i = 0; i < map->backends->len; i++) {
+ bk = g_ptr_array_index(map->backends, i);
+
+ MAP_RELEASE(bk, "rspamd_map_backend");
+ }
+
+ if (map->fallback_backend) {
+ MAP_RELEASE(map->fallback_backend, "rspamd_map_backend");
+ }
+ }
+
+ g_list_free(cfg->maps);
+ cfg->maps = NULL;
+}
+
+static const gchar *
+rspamd_map_check_proto(struct rspamd_config *cfg,
+ const gchar *map_line, struct rspamd_map_backend *bk)
+{
+ const gchar *pos = map_line, *end, *end_key;
+
+ g_assert(bk != NULL);
+ g_assert(pos != NULL);
+
+ end = pos + strlen(pos);
+
+ /* Static check */
+ if (g_ascii_strcasecmp(pos, "static") == 0) {
+ bk->protocol = MAP_PROTO_STATIC;
+ bk->uri = g_strdup(pos);
+
+ return pos;
+ }
+ else if (g_ascii_strcasecmp(pos, "zst+static") == 0) {
+ bk->protocol = MAP_PROTO_STATIC;
+ bk->uri = g_strdup(pos + 4);
+ bk->is_compressed = TRUE;
+
+ return pos + 4;
+ }
+
+ for (;;) {
+ if (g_ascii_strncasecmp(pos, "sign+", sizeof("sign+") - 1) == 0) {
+ bk->is_signed = TRUE;
+ pos += sizeof("sign+") - 1;
+ }
+ else if (g_ascii_strncasecmp(pos, "fallback+", sizeof("fallback+") - 1) == 0) {
+ bk->is_fallback = TRUE;
+ pos += sizeof("fallback+") - 1;
+ }
+ else if (g_ascii_strncasecmp(pos, "key=", sizeof("key=") - 1) == 0) {
+ pos += sizeof("key=") - 1;
+ end_key = memchr(pos, '+', end - pos);
+
+ if (end_key != NULL) {
+ bk->trusted_pubkey = rspamd_pubkey_from_base32(pos, end_key - pos,
+ RSPAMD_KEYPAIR_SIGN, RSPAMD_CRYPTOBOX_MODE_25519);
+
+ if (bk->trusted_pubkey == NULL) {
+ msg_err_config("cannot read pubkey from map: %s",
+ map_line);
+ return NULL;
+ }
+ pos = end_key + 1;
+ }
+ else if (end - pos > 64) {
+ /* Try hex encoding */
+ bk->trusted_pubkey = rspamd_pubkey_from_hex(pos, 64,
+ RSPAMD_KEYPAIR_SIGN, RSPAMD_CRYPTOBOX_MODE_25519);
+
+ if (bk->trusted_pubkey == NULL) {
+ msg_err_config("cannot read pubkey from map: %s",
+ map_line);
+ return NULL;
+ }
+ pos += 64;
+ }
+ else {
+ msg_err_config("cannot read pubkey from map: %s",
+ map_line);
+ return NULL;
+ }
+
+ if (*pos == '+' || *pos == ':') {
+ pos++;
+ }
+ }
+ else {
+ /* No known flags */
+ break;
+ }
+ }
+
+ bk->protocol = MAP_PROTO_FILE;
+
+ if (g_ascii_strncasecmp(pos, "http://", sizeof("http://") - 1) == 0) {
+ bk->protocol = MAP_PROTO_HTTP;
+ /* Include http:// */
+ bk->uri = g_strdup(pos);
+ pos += sizeof("http://") - 1;
+ }
+ else if (g_ascii_strncasecmp(pos, "https://", sizeof("https://") - 1) == 0) {
+ bk->protocol = MAP_PROTO_HTTPS;
+ /* Include https:// */
+ bk->uri = g_strdup(pos);
+ pos += sizeof("https://") - 1;
+ }
+ else if (g_ascii_strncasecmp(pos, "file://", sizeof("file://") - 1) == 0) {
+ pos += sizeof("file://") - 1;
+ /* Exclude file:// */
+ bk->uri = g_strdup(pos);
+ }
+ else if (*pos == '/') {
+ /* Trivial file case */
+ bk->uri = g_strdup(pos);
+ }
+ else {
+ msg_err_config("invalid map fetching protocol: %s", map_line);
+
+ return NULL;
+ }
+
+ if (bk->protocol != MAP_PROTO_FILE && bk->is_signed) {
+ msg_err_config("signed maps are no longer supported for HTTP(s): %s", map_line);
+ }
+
+ return pos;
+}
+
+gboolean
+rspamd_map_is_map(const gchar *map_line)
+{
+ gboolean ret = FALSE;
+
+ g_assert(map_line != NULL);
+
+ if (map_line[0] == '/') {
+ ret = TRUE;
+ }
+ else if (g_ascii_strncasecmp(map_line, "sign+", sizeof("sign+") - 1) == 0) {
+ ret = TRUE;
+ }
+ else if (g_ascii_strncasecmp(map_line, "fallback+", sizeof("fallback+") - 1) == 0) {
+ ret = TRUE;
+ }
+ else if (g_ascii_strncasecmp(map_line, "file://", sizeof("file://") - 1) == 0) {
+ ret = TRUE;
+ }
+ else if (g_ascii_strncasecmp(map_line, "http://", sizeof("http://") - 1) == 0) {
+ ret = TRUE;
+ }
+ else if (g_ascii_strncasecmp(map_line, "https://", sizeof("https://") - 1) == 0) {
+ ret = TRUE;
+ }
+
+ return ret;
+}
+
+static void
+rspamd_map_backend_dtor(struct rspamd_map_backend *bk)
+{
+ switch (bk->protocol) {
+ case MAP_PROTO_FILE:
+ if (bk->data.fd) {
+ ev_stat_stop(bk->event_loop, &bk->data.fd->st_ev);
+ g_free(bk->data.fd->filename);
+ g_free(bk->data.fd);
+ }
+ break;
+ case MAP_PROTO_STATIC:
+ if (bk->data.sd) {
+ if (bk->data.sd->data) {
+ g_free(bk->data.sd->data);
+ }
+
+ g_free(bk->data.sd);
+ }
+ break;
+ case MAP_PROTO_HTTP:
+ case MAP_PROTO_HTTPS:
+ if (bk->data.hd) {
+ struct http_map_data *data = bk->data.hd;
+
+ g_free(data->host);
+ g_free(data->path);
+ g_free(data->rest);
+
+ if (data->userinfo) {
+ g_free(data->userinfo);
+ }
+
+ if (data->etag) {
+ rspamd_fstring_free(data->etag);
+ }
+
+ /*
+ * Clear cached file, but check if a worker is an active http worker
+ * as cur_cache_cbd is meaningful merely for active worker, who actually
+ * owns the cache
+ */
+ if (bk->map && bk->map->active_http) {
+ if (g_atomic_int_compare_and_exchange(&data->cache->available, 1, 0)) {
+ if (data->cur_cache_cbd) {
+ msg_info("clear shared memory cache for a map in %s as backend \"%s\" is closing",
+ data->cur_cache_cbd->shm->shm_name,
+ bk->uri);
+ MAP_RELEASE(data->cur_cache_cbd->shm,
+ "rspamd_http_map_cached_cbdata");
+ ev_timer_stop(data->cur_cache_cbd->event_loop,
+ &data->cur_cache_cbd->timeout);
+ g_free(data->cur_cache_cbd);
+ data->cur_cache_cbd = NULL;
+ }
+ }
+ }
+
+ g_free(bk->data.hd);
+ }
+ break;
+ }
+
+ if (bk->trusted_pubkey) {
+ rspamd_pubkey_unref(bk->trusted_pubkey);
+ }
+
+ g_free(bk->uri);
+ g_free(bk);
+}
+
+static struct rspamd_map_backend *
+rspamd_map_parse_backend(struct rspamd_config *cfg, const gchar *map_line)
+{
+ struct rspamd_map_backend *bk;
+ struct file_map_data *fdata = NULL;
+ struct http_map_data *hdata = NULL;
+ struct static_map_data *sdata = NULL;
+ struct http_parser_url up;
+ const gchar *end, *p;
+ rspamd_ftok_t tok;
+
+ bk = g_malloc0(sizeof(*bk));
+ REF_INIT_RETAIN(bk, rspamd_map_backend_dtor);
+
+ if (!rspamd_map_check_proto(cfg, map_line, bk)) {
+ goto err;
+ }
+
+ if (bk->is_fallback && bk->protocol != MAP_PROTO_FILE) {
+ msg_err_config("fallback backend must be file for %s", bk->uri);
+
+ goto err;
+ }
+
+ end = map_line + strlen(map_line);
+ if (end - map_line > 5) {
+ p = end - 5;
+ if (g_ascii_strcasecmp(p, ".zstd") == 0) {
+ bk->is_compressed = TRUE;
+ }
+ p = end - 4;
+ if (g_ascii_strcasecmp(p, ".zst") == 0) {
+ bk->is_compressed = TRUE;
+ }
+ }
+
+ /* Now check for each proto separately */
+ if (bk->protocol == MAP_PROTO_FILE) {
+ fdata = g_malloc0(sizeof(struct file_map_data));
+
+ if (access(bk->uri, R_OK) == -1) {
+ if (errno != ENOENT) {
+ msg_err_config("cannot open file '%s': %s", bk->uri, strerror(errno));
+ goto err;
+ }
+
+ msg_info_config(
+ "map '%s' is not found, but it can be loaded automatically later",
+ bk->uri);
+ }
+
+ fdata->filename = g_strdup(bk->uri);
+ bk->data.fd = fdata;
+ }
+ else if (bk->protocol == MAP_PROTO_HTTP || bk->protocol == MAP_PROTO_HTTPS) {
+ hdata = g_malloc0(sizeof(struct http_map_data));
+
+ memset(&up, 0, sizeof(up));
+ if (http_parser_parse_url(bk->uri, strlen(bk->uri), FALSE,
+ &up) != 0) {
+ msg_err_config("cannot parse HTTP url: %s", bk->uri);
+ goto err;
+ }
+ else {
+ if (!(up.field_set & 1u << UF_HOST)) {
+ msg_err_config("cannot parse HTTP url: %s: no host", bk->uri);
+ goto err;
+ }
+
+ tok.begin = bk->uri + up.field_data[UF_HOST].off;
+ tok.len = up.field_data[UF_HOST].len;
+ hdata->host = rspamd_ftokdup(&tok);
+
+ if (up.field_set & (1u << UF_PORT)) {
+ hdata->port = up.port;
+ }
+ else {
+ if (bk->protocol == MAP_PROTO_HTTP) {
+ hdata->port = 80;
+ }
+ else {
+ hdata->port = 443;
+ }
+ }
+
+ if (up.field_set & (1u << UF_PATH)) {
+ tok.begin = bk->uri + up.field_data[UF_PATH].off;
+ tok.len = up.field_data[UF_PATH].len;
+
+ hdata->path = rspamd_ftokdup(&tok);
+
+ /* We also need to check query + fragment */
+ if (up.field_set & ((1u << UF_QUERY) | (1u << UF_FRAGMENT))) {
+ tok.begin = bk->uri + up.field_data[UF_PATH].off +
+ up.field_data[UF_PATH].len;
+ tok.len = strlen(tok.begin);
+ hdata->rest = rspamd_ftokdup(&tok);
+ }
+ else {
+ hdata->rest = g_strdup("");
+ }
+ }
+
+ if (up.field_set & (1u << UF_USERINFO)) {
+ /* Create authorisation header for basic auth */
+ guint len = sizeof("Basic ") +
+ up.field_data[UF_USERINFO].len * 8 / 5 + 4;
+ hdata->userinfo = g_malloc(len);
+ rspamd_snprintf(hdata->userinfo, len, "Basic %*Bs",
+ (int) up.field_data[UF_USERINFO].len,
+ bk->uri + up.field_data[UF_USERINFO].off);
+
+ msg_debug("added userinfo for the map from the URL: %s", hdata->host);
+ }
+ else {
+ /* Try to obtain authentication data from options in the configuration */
+ const ucl_object_t *auth_obj, *opts_obj;
+
+ opts_obj = ucl_object_lookup(cfg->cfg_ucl_obj, "options");
+ if (opts_obj != NULL) {
+ auth_obj = ucl_object_lookup(opts_obj, "http_auth");
+ if (auth_obj != NULL && ucl_object_type(auth_obj) == UCL_OBJECT) {
+ const ucl_object_t *host_obj;
+
+ /*
+ * Search first by the full URL and then by the host part
+ */
+ host_obj = ucl_object_lookup(auth_obj, map_line);
+
+ if (host_obj == NULL) {
+ host_obj = ucl_object_lookup(auth_obj, hdata->host);
+ }
+
+ if (host_obj != NULL && ucl_object_type(host_obj) == UCL_OBJECT) {
+ const ucl_object_t *user_obj, *password_obj;
+
+ user_obj = ucl_object_lookup(host_obj, "user");
+ password_obj = ucl_object_lookup(host_obj, "password");
+
+ if (user_obj != NULL && password_obj != NULL &&
+ ucl_object_type(user_obj) == UCL_STRING &&
+ ucl_object_type(password_obj) == UCL_STRING) {
+
+ gchar *tmpbuf;
+ unsigned tlen;
+
+ /* User + password + ':' */
+ tlen = strlen(ucl_object_tostring(user_obj)) +
+ strlen(ucl_object_tostring(password_obj)) + 1;
+ tmpbuf = g_malloc(tlen + 1);
+ rspamd_snprintf(tmpbuf, tlen + 1, "%s:%s",
+ ucl_object_tostring(user_obj),
+ ucl_object_tostring(password_obj));
+ /* Base64 encoding is not so greedy, but we add some space for simplicity */
+ tlen *= 2;
+ tlen += sizeof("Basic ") - 1;
+ hdata->userinfo = g_malloc(tlen + 1);
+ rspamd_snprintf(hdata->userinfo, tlen + 1, "Basic %Bs", tmpbuf);
+ g_free(tmpbuf);
+ msg_debug("added userinfo for the map from the configuration: %s", map_line);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ hdata->cache = rspamd_mempool_alloc0_shared(cfg->cfg_pool,
+ sizeof(*hdata->cache));
+
+ bk->data.hd = hdata;
+ }
+ else if (bk->protocol == MAP_PROTO_STATIC) {
+ sdata = g_malloc0(sizeof(*sdata));
+ bk->data.sd = sdata;
+ }
+
+ bk->id = rspamd_cryptobox_fast_hash_specific(RSPAMD_CRYPTOBOX_T1HA,
+ bk->uri, strlen(bk->uri),
+ 0xdeadbabe);
+
+ return bk;
+
+err:
+ MAP_RELEASE(bk, "rspamd_map_backend");
+
+ if (hdata) {
+ g_free(hdata);
+ }
+
+ if (fdata) {
+ g_free(fdata);
+ }
+
+ if (sdata) {
+ g_free(sdata);
+ }
+
+ return NULL;
+}
+
+static void
+rspamd_map_calculate_hash(struct rspamd_map *map)
+{
+ struct rspamd_map_backend *bk;
+ guint i;
+ rspamd_cryptobox_hash_state_t st;
+ gchar *cksum_encoded, cksum[rspamd_cryptobox_HASHBYTES];
+
+ rspamd_cryptobox_hash_init(&st, NULL, 0);
+
+ for (i = 0; i < map->backends->len; i++) {
+ bk = g_ptr_array_index(map->backends, i);
+ rspamd_cryptobox_hash_update(&st, bk->uri, strlen(bk->uri));
+ }
+
+ rspamd_cryptobox_hash_final(&st, cksum);
+ cksum_encoded = rspamd_encode_base32(cksum, sizeof(cksum), RSPAMD_BASE32_DEFAULT);
+ rspamd_strlcpy(map->tag, cksum_encoded, sizeof(map->tag));
+ g_free(cksum_encoded);
+}
+
+static gboolean
+rspamd_map_add_static_string(struct rspamd_config *cfg,
+ const ucl_object_t *elt,
+ GString *target)
+{
+ gsize sz;
+ const gchar *dline;
+
+ if (ucl_object_type(elt) != UCL_STRING) {
+ msg_err_config("map has static backend but `data` is "
+ "not string like: %s",
+ ucl_object_type_to_string(elt->type));
+ return FALSE;
+ }
+
+ /* Otherwise, we copy data to the backend */
+ dline = ucl_object_tolstring(elt, &sz);
+
+ if (sz == 0) {
+ msg_err_config("map has static backend but empty no data");
+ return FALSE;
+ }
+
+ g_string_append_len(target, dline, sz);
+ g_string_append_c(target, '\n');
+
+ return TRUE;
+}
+
+struct rspamd_map *
+rspamd_map_add(struct rspamd_config *cfg,
+ const gchar *map_line,
+ const gchar *description,
+ map_cb_t read_callback,
+ map_fin_cb_t fin_callback,
+ map_dtor_t dtor,
+ void **user_data,
+ struct rspamd_worker *worker,
+ int flags)
+{
+ struct rspamd_map *map;
+ struct rspamd_map_backend *bk;
+
+ bk = rspamd_map_parse_backend(cfg, map_line);
+ if (bk == NULL) {
+ return NULL;
+ }
+
+ if (bk->is_fallback) {
+ msg_err_config("cannot add map with fallback only backend: %s", bk->uri);
+ REF_RELEASE(bk);
+
+ return NULL;
+ }
+
+ map = rspamd_mempool_alloc0(cfg->cfg_pool, sizeof(struct rspamd_map));
+ map->read_callback = read_callback;
+ map->fin_callback = fin_callback;
+ map->dtor = dtor;
+ map->user_data = user_data;
+ map->cfg = cfg;
+ map->id = rspamd_random_uint64_fast();
+ map->locked =
+ rspamd_mempool_alloc0_shared(cfg->cfg_pool, sizeof(gint));
+ map->backends = g_ptr_array_sized_new(1);
+ map->wrk = worker;
+ rspamd_mempool_add_destructor(cfg->cfg_pool, rspamd_ptr_array_free_hard,
+ map->backends);
+ g_ptr_array_add(map->backends, bk);
+ map->name = rspamd_mempool_strdup(cfg->cfg_pool, map_line);
+ map->no_file_read = (flags & RSPAMD_MAP_FILE_NO_READ);
+
+ if (bk->protocol == MAP_PROTO_FILE) {
+ map->poll_timeout = (cfg->map_timeout * cfg->map_file_watch_multiplier);
+ }
+ else {
+ map->poll_timeout = cfg->map_timeout;
+ }
+
+ if (description != NULL) {
+ map->description = rspamd_mempool_strdup(cfg->cfg_pool, description);
+ }
+
+ rspamd_map_calculate_hash(map);
+ msg_info_map("added map %s", bk->uri);
+ bk->map = map;
+
+ cfg->maps = g_list_prepend(cfg->maps, map);
+
+ return map;
+}
+
+struct rspamd_map *
+rspamd_map_add_fake(struct rspamd_config *cfg,
+ const gchar *description,
+ const gchar *name)
+{
+ struct rspamd_map *map;
+
+ map = rspamd_mempool_alloc0(cfg->cfg_pool, sizeof(struct rspamd_map));
+ map->cfg = cfg;
+ map->id = rspamd_random_uint64_fast();
+ map->name = rspamd_mempool_strdup(cfg->cfg_pool, name);
+ map->user_data = (void **) &map; /* to prevent null pointer dereferencing */
+
+ if (description != NULL) {
+ map->description = rspamd_mempool_strdup(cfg->cfg_pool, description);
+ }
+
+ return map;
+}
+
+static inline void
+rspamd_map_add_backend(struct rspamd_map *map, struct rspamd_map_backend *bk)
+{
+ if (bk->is_fallback) {
+ if (map->fallback_backend) {
+ msg_warn_map("redefining fallback backend from %s to %s",
+ map->fallback_backend->uri, bk->uri);
+ }
+
+ map->fallback_backend = bk;
+ }
+ else {
+ g_ptr_array_add(map->backends, bk);
+ }
+
+ bk->map = map;
+}
+
+struct rspamd_map *
+rspamd_map_add_from_ucl(struct rspamd_config *cfg,
+ const ucl_object_t *obj,
+ const gchar *description,
+ map_cb_t read_callback,
+ map_fin_cb_t fin_callback,
+ map_dtor_t dtor,
+ void **user_data,
+ struct rspamd_worker *worker,
+ gint flags)
+{
+ ucl_object_iter_t it = NULL;
+ const ucl_object_t *cur, *elt;
+ struct rspamd_map *map;
+ struct rspamd_map_backend *bk;
+ guint i;
+
+ g_assert(obj != NULL);
+
+ if (ucl_object_type(obj) == UCL_STRING) {
+ /* Just a plain string */
+ return rspamd_map_add(cfg, ucl_object_tostring(obj), description,
+ read_callback, fin_callback, dtor, user_data, worker, flags);
+ }
+
+ map = rspamd_mempool_alloc0(cfg->cfg_pool, sizeof(struct rspamd_map));
+ map->read_callback = read_callback;
+ map->fin_callback = fin_callback;
+ map->dtor = dtor;
+ map->user_data = user_data;
+ map->cfg = cfg;
+ map->id = rspamd_random_uint64_fast();
+ map->locked =
+ rspamd_mempool_alloc0_shared(cfg->cfg_pool, sizeof(gint));
+ map->backends = g_ptr_array_new();
+ map->wrk = worker;
+ map->no_file_read = (flags & RSPAMD_MAP_FILE_NO_READ);
+ rspamd_mempool_add_destructor(cfg->cfg_pool, rspamd_ptr_array_free_hard,
+ map->backends);
+ map->poll_timeout = cfg->map_timeout;
+
+ if (description) {
+ map->description = rspamd_mempool_strdup(cfg->cfg_pool, description);
+ }
+
+ if (ucl_object_type(obj) == UCL_ARRAY) {
+ /* Add array of maps as multiple backends */
+ while ((cur = ucl_object_iterate(obj, &it, true)) != NULL) {
+ if (ucl_object_type(cur) == UCL_STRING) {
+ bk = rspamd_map_parse_backend(cfg, ucl_object_tostring(cur));
+
+ if (bk != NULL) {
+ rspamd_map_add_backend(map, bk);
+
+ if (!map->name) {
+ map->name = rspamd_mempool_strdup(cfg->cfg_pool,
+ ucl_object_tostring(cur));
+ }
+ }
+ }
+ else {
+ msg_err_config("bad map element type: %s",
+ ucl_object_type_to_string(ucl_object_type(cur)));
+ }
+ }
+
+ if (map->backends->len == 0) {
+ msg_err_config("map has no urls to be loaded: empty list");
+ goto err;
+ }
+ }
+ else if (ucl_object_type(obj) == UCL_OBJECT) {
+ elt = ucl_object_lookup(obj, "name");
+ if (elt && ucl_object_type(elt) == UCL_STRING) {
+ map->name = rspamd_mempool_strdup(cfg->cfg_pool,
+ ucl_object_tostring(elt));
+ }
+
+ elt = ucl_object_lookup(obj, "description");
+ if (elt && ucl_object_type(elt) == UCL_STRING) {
+ map->description = rspamd_mempool_strdup(cfg->cfg_pool,
+ ucl_object_tostring(elt));
+ }
+
+ elt = ucl_object_lookup_any(obj, "timeout", "poll", "poll_time",
+ "watch_interval", NULL);
+ if (elt) {
+ map->poll_timeout = ucl_object_todouble(elt);
+ }
+
+ elt = ucl_object_lookup_any(obj, "upstreams", "url", "urls", NULL);
+ if (elt == NULL) {
+ msg_err_config("map has no urls to be loaded: no elt");
+ goto err;
+ }
+
+ if (ucl_object_type(elt) == UCL_ARRAY) {
+ /* Add array of maps as multiple backends */
+ it = ucl_object_iterate_new(elt);
+
+ while ((cur = ucl_object_iterate_safe(it, true)) != NULL) {
+ if (ucl_object_type(cur) == UCL_STRING) {
+ bk = rspamd_map_parse_backend(cfg, ucl_object_tostring(cur));
+
+ if (bk != NULL) {
+ rspamd_map_add_backend(map, bk);
+
+ if (!map->name) {
+ map->name = rspamd_mempool_strdup(cfg->cfg_pool,
+ ucl_object_tostring(cur));
+ }
+ }
+ }
+ else {
+ msg_err_config("bad map element type: %s",
+ ucl_object_type_to_string(ucl_object_type(cur)));
+ ucl_object_iterate_free(it);
+ goto err;
+ }
+ }
+
+ ucl_object_iterate_free(it);
+
+ if (map->backends->len == 0) {
+ msg_err_config("map has no urls to be loaded: empty object list");
+ goto err;
+ }
+ }
+ else if (ucl_object_type(elt) == UCL_STRING) {
+ bk = rspamd_map_parse_backend(cfg, ucl_object_tostring(elt));
+
+ if (bk != NULL) {
+ rspamd_map_add_backend(map, bk);
+
+ if (!map->name) {
+ map->name = rspamd_mempool_strdup(cfg->cfg_pool,
+ ucl_object_tostring(elt));
+ }
+ }
+ }
+
+ if (!map->backends || map->backends->len == 0) {
+ msg_err_config("map has no urls to be loaded: no valid backends");
+ goto err;
+ }
+ }
+ else {
+ msg_err_config("map has invalid type for value: %s",
+ ucl_object_type_to_string(ucl_object_type(obj)));
+ goto err;
+ }
+
+ gboolean all_local = TRUE;
+
+ PTR_ARRAY_FOREACH(map->backends, i, bk)
+ {
+ if (bk->protocol == MAP_PROTO_STATIC) {
+ GString *map_data;
+ /* We need data field in ucl */
+ elt = ucl_object_lookup(obj, "data");
+
+ if (elt == NULL) {
+ msg_err_config("map has static backend but no `data` field");
+ goto err;
+ }
+
+
+ if (ucl_object_type(elt) == UCL_STRING) {
+ map_data = g_string_sized_new(32);
+
+ if (rspamd_map_add_static_string(cfg, elt, map_data)) {
+ bk->data.sd->data = map_data->str;
+ bk->data.sd->len = map_data->len;
+ g_string_free(map_data, FALSE);
+ }
+ else {
+ g_string_free(map_data, TRUE);
+ msg_err_config("map has static backend with invalid `data` field");
+ goto err;
+ }
+ }
+ else if (ucl_object_type(elt) == UCL_ARRAY) {
+ map_data = g_string_sized_new(32);
+ it = ucl_object_iterate_new(elt);
+
+ while ((cur = ucl_object_iterate_safe(it, true))) {
+ if (!rspamd_map_add_static_string(cfg, cur, map_data)) {
+ g_string_free(map_data, TRUE);
+ msg_err_config("map has static backend with invalid "
+ "`data` field");
+ ucl_object_iterate_free(it);
+ goto err;
+ }
+ }
+
+ ucl_object_iterate_free(it);
+ bk->data.sd->data = map_data->str;
+ bk->data.sd->len = map_data->len;
+ g_string_free(map_data, FALSE);
+ }
+ }
+ else if (bk->protocol != MAP_PROTO_FILE) {
+ all_local = FALSE;
+ }
+ }
+
+ if (all_local) {
+ map->poll_timeout = (map->poll_timeout *
+ cfg->map_file_watch_multiplier);
+ }
+
+ rspamd_map_calculate_hash(map);
+ msg_debug_map("added map from ucl");
+
+ cfg->maps = g_list_prepend(cfg->maps, map);
+
+ return map;
+
+err:
+
+ if (map) {
+ PTR_ARRAY_FOREACH(map->backends, i, bk)
+ {
+ MAP_RELEASE(bk, "rspamd_map_backend");
+ }
+ }
+
+ return NULL;
+}
+
+rspamd_map_traverse_function
+rspamd_map_get_traverse_function(struct rspamd_map *map)
+{
+ if (map) {
+ return map->traverse_function;
+ }
+
+ return NULL;
+}
+
+void rspamd_map_traverse(struct rspamd_map *map, rspamd_map_traverse_cb cb,
+ gpointer cbdata, gboolean reset_hits)
+{
+ if (*map->user_data && map->traverse_function) {
+ map->traverse_function(*map->user_data, cb, cbdata, reset_hits);
+ }
+}
+
+void rspamd_map_set_on_load_function(struct rspamd_map *map, rspamd_map_on_load_function cb,
+ gpointer cbdata, GDestroyNotify dtor)
+{
+ if (map) {
+ map->on_load_function = cb;
+ map->on_load_ud = cbdata;
+ map->on_load_ud_dtor = dtor;
+ }
+}
diff --git a/src/libserver/maps/map.h b/src/libserver/maps/map.h
new file mode 100644
index 0000000..04df16e
--- /dev/null
+++ b/src/libserver/maps/map.h
@@ -0,0 +1,168 @@
+#ifndef RSPAMD_MAP_H
+#define RSPAMD_MAP_H
+
+#include "config.h"
+#include "contrib/libev/ev.h"
+
+#include "ucl.h"
+#include "mem_pool.h"
+#include "radix.h"
+#include "dns.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Maps API is designed to load lists data from different dynamic sources.
+ * It monitor files and HTTP locations for modifications and reload them if they are
+ * modified.
+ */
+struct map_cb_data;
+struct rspamd_worker;
+
+/**
+ * Common map object
+ */
+struct rspamd_config;
+struct rspamd_map;
+
+/**
+ * Callback types
+ */
+typedef gchar *(*map_cb_t)(gchar *chunk, gint len,
+ struct map_cb_data *data, gboolean final);
+
+typedef void (*map_fin_cb_t)(struct map_cb_data *data, void **target);
+
+typedef void (*map_dtor_t)(struct map_cb_data *data);
+
+typedef gboolean (*rspamd_map_traverse_cb)(gconstpointer key,
+ gconstpointer value, gsize hits, gpointer ud);
+
+typedef void (*rspamd_map_traverse_function)(void *data,
+ rspamd_map_traverse_cb cb,
+ gpointer cbdata, gboolean reset_hits);
+typedef void (*rspamd_map_on_load_function)(struct rspamd_map *map, gpointer ud);
+
+/**
+ * Callback data for async load
+ */
+struct map_cb_data {
+ struct rspamd_map *map;
+ gint state;
+ bool errored;
+ void *prev_data;
+ void *cur_data;
+};
+
+/**
+ * Returns TRUE if line looks like a map definition
+ * @param map_line
+ * @return
+ */
+gboolean rspamd_map_is_map(const gchar *map_line);
+
+enum rspamd_map_flags {
+ RSPAMD_MAP_DEFAULT = 0,
+ RSPAMD_MAP_FILE_ONLY = 1u << 0u,
+ RSPAMD_MAP_FILE_NO_READ = 1u << 1u,
+};
+
+/**
+ * Add map from line
+ */
+struct rspamd_map *rspamd_map_add(struct rspamd_config *cfg,
+ const gchar *map_line,
+ const gchar *description,
+ map_cb_t read_callback,
+ map_fin_cb_t fin_callback,
+ map_dtor_t dtor,
+ void **user_data,
+ struct rspamd_worker *worker,
+ int flags);
+
+/**
+ * Add map from ucl
+ */
+struct rspamd_map *rspamd_map_add_from_ucl(struct rspamd_config *cfg,
+ const ucl_object_t *obj,
+ const gchar *description,
+ map_cb_t read_callback,
+ map_fin_cb_t fin_callback,
+ map_dtor_t dtor,
+ void **user_data,
+ struct rspamd_worker *worker,
+ int flags);
+
+/**
+ * Adds a fake map structure (for logging purposes mainly)
+ * @param cfg
+ * @param description
+ * @return
+ */
+struct rspamd_map *rspamd_map_add_fake(struct rspamd_config *cfg,
+ const gchar *description,
+ const gchar *name);
+
+
+enum rspamd_map_watch_type {
+ RSPAMD_MAP_WATCH_MIN = 9,
+ RSPAMD_MAP_WATCH_PRIMARY_CONTROLLER,
+ RSPAMD_MAP_WATCH_SCANNER,
+ RSPAMD_MAP_WATCH_WORKER,
+ RSPAMD_MAP_WATCH_MAX
+};
+
+/**
+ * Start watching of maps by adding events to libevent event loop
+ */
+void rspamd_map_watch(struct rspamd_config *cfg,
+ struct ev_loop *event_loop,
+ struct rspamd_dns_resolver *resolver,
+ struct rspamd_worker *worker,
+ enum rspamd_map_watch_type how);
+
+/**
+ * Preloads maps where all backends are file
+ * @param cfg
+ */
+void rspamd_map_preload(struct rspamd_config *cfg);
+
+/**
+ * Remove all maps watched (remove events)
+ */
+void rspamd_map_remove_all(struct rspamd_config *cfg);
+
+/**
+ * Get traverse function for specific map
+ * @param map
+ * @return
+ */
+rspamd_map_traverse_function rspamd_map_get_traverse_function(struct rspamd_map *map);
+
+/**
+ * Perform map traverse
+ * @param map
+ * @param cb
+ * @param cbdata
+ * @param reset_hits
+ * @return
+ */
+void rspamd_map_traverse(struct rspamd_map *map, rspamd_map_traverse_cb cb,
+ gpointer cbdata, gboolean reset_hits);
+
+/**
+ * Set map on load callback
+ * @param map
+ * @param cb
+ * @param cbdata
+ */
+void rspamd_map_set_on_load_function(struct rspamd_map *map, rspamd_map_on_load_function cb,
+ gpointer cbdata, GDestroyNotify dtor);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/maps/map_helpers.c b/src/libserver/maps/map_helpers.c
new file mode 100644
index 0000000..65478c5
--- /dev/null
+++ b/src/libserver/maps/map_helpers.c
@@ -0,0 +1,1845 @@
+/*
+ * 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 "map_helpers.h"
+#include "map_private.h"
+#include "khash.h"
+#include "radix.h"
+#include "rspamd.h"
+#include "cryptobox.h"
+#include "mempool_vars_internal.h"
+#include "contrib/fastutf8/fastutf8.h"
+#include "contrib/cdb/cdb.h"
+
+#ifdef WITH_HYPERSCAN
+#include "hs.h"
+#include "hyperscan_tools.h"
+#endif
+#ifndef WITH_PCRE2
+#include <pcre.h>
+#else
+#include <pcre2.h>
+#endif
+
+
+static const guint64 map_hash_seed = 0xdeadbabeULL;
+static const gchar *const hash_fill = "1";
+
+struct rspamd_map_helper_value {
+ gsize hits;
+ gconstpointer key;
+ gchar value[]; /* Null terminated */
+};
+
+#define rspamd_map_ftok_hash(t) (rspamd_icase_hash((t).begin, (t).len, rspamd_hash_seed()))
+#define rspamd_map_ftok_equal(a, b) ((a).len == (b).len && rspamd_lc_cmp((a).begin, (b).begin, (a).len) == 0)
+
+KHASH_INIT(rspamd_map_hash, rspamd_ftok_t,
+ struct rspamd_map_helper_value *, true,
+ rspamd_map_ftok_hash, rspamd_map_ftok_equal);
+
+struct rspamd_radix_map_helper {
+ rspamd_mempool_t *pool;
+ khash_t(rspamd_map_hash) * htb;
+ radix_compressed_t *trie;
+ struct rspamd_map *map;
+ rspamd_cryptobox_fast_hash_state_t hst;
+};
+
+struct rspamd_hash_map_helper {
+ rspamd_mempool_t *pool;
+ khash_t(rspamd_map_hash) * htb;
+ struct rspamd_map *map;
+ rspamd_cryptobox_fast_hash_state_t hst;
+};
+
+struct rspamd_cdb_map_helper {
+ GQueue cdbs;
+ struct rspamd_map *map;
+ rspamd_cryptobox_fast_hash_state_t hst;
+ gsize total_size;
+};
+
+struct rspamd_regexp_map_helper {
+ rspamd_cryptobox_hash_state_t hst;
+ guchar re_digest[rspamd_cryptobox_HASHBYTES];
+ rspamd_mempool_t *pool;
+ struct rspamd_map *map;
+ GPtrArray *regexps;
+ GPtrArray *values;
+ khash_t(rspamd_map_hash) * htb;
+ enum rspamd_regexp_map_flags map_flags;
+#ifdef WITH_HYPERSCAN
+ rspamd_hyperscan_t *hs_db;
+ hs_scratch_t *hs_scratch;
+ gchar **patterns;
+ gint *flags;
+ gint *ids;
+#endif
+};
+
+/**
+ * FSM for parsing lists
+ */
+
+#define MAP_STORE_KEY \
+ do { \
+ while (g_ascii_isspace(*c) && p > c) { c++; } \
+ key = g_malloc(p - c + 1); \
+ rspamd_strlcpy(key, c, p - c + 1); \
+ stripped_key = g_strstrip(key); \
+ } while (0)
+
+#define MAP_STORE_VALUE \
+ do { \
+ while (g_ascii_isspace(*c) && p > c) { c++; } \
+ value = g_malloc(p - c + 1); \
+ rspamd_strlcpy(value, c, p - c + 1); \
+ stripped_value = g_strstrip(value); \
+ } while (0)
+
+gchar *
+rspamd_parse_kv_list(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ rspamd_map_insert_func func,
+ const gchar *default_value,
+ gboolean final)
+{
+ enum {
+ map_skip_spaces_before_key = 0,
+ map_read_key,
+ map_read_key_quoted,
+ map_read_key_slashed,
+ map_skip_spaces_after_key,
+ map_backslash_quoted,
+ map_backslash_slashed,
+ map_read_key_after_slash,
+ map_read_value,
+ map_read_comment_start,
+ map_skip_comment,
+ map_read_eol,
+ };
+
+ gchar *c, *p, *key = NULL, *value = NULL, *stripped_key, *stripped_value, *end;
+ struct rspamd_map *map = data->map;
+ guint line_number = 0;
+
+ p = chunk;
+ c = p;
+ end = p + len;
+
+ while (p < end) {
+ switch (data->state) {
+ case map_skip_spaces_before_key:
+ if (g_ascii_isspace(*p)) {
+ p++;
+ }
+ else {
+ if (*p == '"') {
+ p++;
+ c = p;
+ data->state = map_read_key_quoted;
+ }
+ else if (*p == '/') {
+ /* Note that c is on '/' here as '/' is a part of key */
+ c = p;
+ p++;
+ data->state = map_read_key_slashed;
+ }
+ else {
+ c = p;
+ data->state = map_read_key;
+ }
+ }
+ break;
+ case map_read_key:
+ /* read key */
+ /* Check here comments, eol and end of buffer */
+ if (*p == '#' && (p == c || *(p - 1) != '\\')) {
+ if (p - c > 0) {
+ /* Store a single key */
+ MAP_STORE_KEY;
+ func(data->cur_data, stripped_key, default_value);
+ msg_debug_map("insert key only pair: %s -> %s; line: %d",
+ stripped_key, default_value, line_number);
+ g_free(key);
+ }
+
+ key = NULL;
+ data->state = map_read_comment_start;
+ }
+ else if (*p == '\r' || *p == '\n') {
+ if (p - c > 0) {
+ /* Store a single key */
+ MAP_STORE_KEY;
+ func(data->cur_data, stripped_key, default_value);
+ msg_debug_map("insert key only pair: %s -> %s; line: %d",
+ stripped_key, default_value, line_number);
+ g_free(key);
+ }
+
+ data->state = map_read_eol;
+ key = NULL;
+ }
+ else if (g_ascii_isspace(*p)) {
+ if (p - c > 0) {
+ MAP_STORE_KEY;
+ data->state = map_skip_spaces_after_key;
+ }
+ else {
+ msg_err_map("empty or invalid key found on line %d", line_number);
+ data->state = map_skip_comment;
+ }
+ }
+ else {
+ p++;
+ }
+ break;
+ case map_read_key_quoted:
+ if (*p == '\\') {
+ data->state = map_backslash_quoted;
+ p++;
+ }
+ else if (*p == '"') {
+ /* Allow empty keys in this case */
+ if (p - c >= 0) {
+ MAP_STORE_KEY;
+ data->state = map_skip_spaces_after_key;
+ }
+ else {
+ g_assert_not_reached();
+ }
+ p++;
+ }
+ else {
+ p++;
+ }
+ break;
+ case map_read_key_slashed:
+ if (*p == '\\') {
+ data->state = map_backslash_slashed;
+ p++;
+ }
+ else if (*p == '/') {
+ /* Allow empty keys in this case */
+ if (p - c >= 0) {
+ data->state = map_read_key_after_slash;
+ }
+ else {
+ g_assert_not_reached();
+ }
+ }
+ else {
+ p++;
+ }
+ break;
+ case map_read_key_after_slash:
+ /*
+ * This state is equal to reading of key but '/' is not
+ * treated specially
+ */
+ if (*p == '#') {
+ if (p - c > 0) {
+ /* Store a single key */
+ MAP_STORE_KEY;
+ func(data->cur_data, stripped_key, default_value);
+ msg_debug_map("insert key only pair: %s -> %s; line: %d",
+ stripped_key, default_value, line_number);
+ g_free(key);
+ key = NULL;
+ }
+
+ data->state = map_read_comment_start;
+ }
+ else if (*p == '\r' || *p == '\n') {
+ if (p - c > 0) {
+ /* Store a single key */
+ MAP_STORE_KEY;
+ func(data->cur_data, stripped_key, default_value);
+
+ msg_debug_map("insert key only pair: %s -> %s; line: %d",
+ stripped_key, default_value, line_number);
+ g_free(key);
+ key = NULL;
+ }
+
+ data->state = map_read_eol;
+ key = NULL;
+ }
+ else if (g_ascii_isspace(*p)) {
+ if (p - c > 0) {
+ MAP_STORE_KEY;
+ data->state = map_skip_spaces_after_key;
+ }
+ else {
+ msg_err_map("empty or invalid key found on line %d", line_number);
+ data->state = map_skip_comment;
+ }
+ }
+ else {
+ p++;
+ }
+ break;
+ case map_backslash_quoted:
+ p++;
+ data->state = map_read_key_quoted;
+ break;
+ case map_backslash_slashed:
+ p++;
+ data->state = map_read_key_slashed;
+ break;
+ case map_skip_spaces_after_key:
+ if (*p == ' ' || *p == '\t') {
+ p++;
+ }
+ else {
+ c = p;
+ data->state = map_read_value;
+ }
+ break;
+ case map_read_value:
+ if (key == NULL) {
+ /* Ignore line */
+ msg_err_map("empty or invalid key found on line %d", line_number);
+ data->state = map_skip_comment;
+ }
+ else {
+ if (*p == '#') {
+ if (p - c > 0) {
+ /* Store a single key */
+ MAP_STORE_VALUE;
+ func(data->cur_data, stripped_key, stripped_value);
+ msg_debug_map("insert key value pair: %s -> %s; line: %d",
+ stripped_key, stripped_value, line_number);
+ g_free(key);
+ g_free(value);
+ key = NULL;
+ value = NULL;
+ }
+ else {
+ func(data->cur_data, stripped_key, default_value);
+ msg_debug_map("insert key only pair: %s -> %s; line: %d",
+ stripped_key, default_value, line_number);
+ g_free(key);
+ key = NULL;
+ }
+
+ data->state = map_read_comment_start;
+ }
+ else if (*p == '\r' || *p == '\n') {
+ if (p - c > 0) {
+ /* Store a single key */
+ MAP_STORE_VALUE;
+ func(data->cur_data, stripped_key, stripped_value);
+ msg_debug_map("insert key value pair: %s -> %s",
+ stripped_key, stripped_value);
+ g_free(key);
+ g_free(value);
+ key = NULL;
+ value = NULL;
+ }
+ else {
+ func(data->cur_data, stripped_key, default_value);
+ msg_debug_map("insert key only pair: %s -> %s",
+ stripped_key, default_value);
+ g_free(key);
+ key = NULL;
+ }
+
+ data->state = map_read_eol;
+ key = NULL;
+ }
+ else {
+ p++;
+ }
+ }
+ break;
+ case map_read_comment_start:
+ if (*p == '#') {
+ data->state = map_skip_comment;
+ p++;
+ key = NULL;
+ value = NULL;
+ }
+ else {
+ g_assert_not_reached();
+ }
+ break;
+ case map_skip_comment:
+ if (*p == '\r' || *p == '\n') {
+ data->state = map_read_eol;
+ }
+ else {
+ p++;
+ }
+ break;
+ case map_read_eol:
+ /* Skip \r\n and whitespaces */
+ if (*p == '\r' || *p == '\n') {
+ if (*p == '\n') {
+ /* We don't care about \r only line separators, they are too rare */
+ line_number++;
+ }
+ p++;
+ }
+ else {
+ data->state = map_skip_spaces_before_key;
+ }
+ break;
+ default:
+ g_assert_not_reached();
+ break;
+ }
+ }
+
+ if (final) {
+ /* Examine the state */
+ switch (data->state) {
+ case map_read_key:
+ case map_read_key_slashed:
+ case map_read_key_quoted:
+ case map_read_key_after_slash:
+ if (p - c > 0) {
+ /* Store a single key */
+ MAP_STORE_KEY;
+ func(data->cur_data, stripped_key, default_value);
+ msg_debug_map("insert key only pair: %s -> %s",
+ stripped_key, default_value);
+ g_free(key);
+ key = NULL;
+ }
+ break;
+ case map_read_value:
+ if (key == NULL) {
+ /* Ignore line */
+ msg_err_map("empty or invalid key found on line %d", line_number);
+ data->state = map_skip_comment;
+ }
+ else {
+ if (p - c > 0) {
+ /* Store a single key */
+ MAP_STORE_VALUE;
+ func(data->cur_data, stripped_key, stripped_value);
+ msg_debug_map("insert key value pair: %s -> %s",
+ stripped_key, stripped_value);
+ g_free(key);
+ g_free(value);
+ key = NULL;
+ value = NULL;
+ }
+ else {
+ func(data->cur_data, stripped_key, default_value);
+ msg_debug_map("insert key only pair: %s -> %s",
+ stripped_key, default_value);
+ g_free(key);
+ key = NULL;
+ }
+ }
+ break;
+ }
+
+ data->state = map_skip_spaces_before_key;
+ }
+
+ return c;
+}
+
+/**
+ * Radix tree helper function
+ */
+void rspamd_map_helper_insert_radix(gpointer st, gconstpointer key, gconstpointer value)
+{
+ struct rspamd_radix_map_helper *r = (struct rspamd_radix_map_helper *) st;
+ struct rspamd_map_helper_value *val;
+ gsize vlen;
+ khiter_t k;
+ gconstpointer nk;
+ rspamd_ftok_t tok;
+ gint res;
+ struct rspamd_map *map;
+
+ map = r->map;
+ tok.begin = key;
+ tok.len = strlen(key);
+
+ k = kh_get(rspamd_map_hash, r->htb, tok);
+
+ if (k == kh_end(r->htb)) {
+ nk = rspamd_mempool_strdup(r->pool, key);
+ tok.begin = nk;
+ k = kh_put(rspamd_map_hash, r->htb, tok, &res);
+ }
+ else {
+ val = kh_value(r->htb, k);
+
+ if (strcmp(value, val->value) == 0) {
+ /* Same element, skip */
+ return;
+ }
+ else {
+ msg_warn_map("duplicate radix entry found for map %s: %s (old value: '%s', new: '%s')",
+ map->name, key, val->value, value);
+ }
+
+ nk = kh_key(r->htb, k).begin;
+ val->key = nk;
+ kh_value(r->htb, k) = val;
+
+ return; /* do not touch radix in case of exact duplicate */
+ }
+
+ vlen = strlen(value);
+ val = rspamd_mempool_alloc0(r->pool, sizeof(*val) +
+ vlen + 1);
+ memcpy(val->value, value, vlen);
+
+ nk = kh_key(r->htb, k).begin;
+ val->key = nk;
+ kh_value(r->htb, k) = val;
+ rspamd_radix_add_iplist(key, ",", r->trie, val, FALSE,
+ r->map->name);
+ rspamd_cryptobox_fast_hash_update(&r->hst, nk, tok.len);
+}
+
+void rspamd_map_helper_insert_radix_resolve(gpointer st, gconstpointer key, gconstpointer value)
+{
+ struct rspamd_radix_map_helper *r = (struct rspamd_radix_map_helper *) st;
+ struct rspamd_map_helper_value *val;
+ gsize vlen;
+ khiter_t k;
+ gconstpointer nk;
+ rspamd_ftok_t tok;
+ gint res;
+ struct rspamd_map *map;
+
+ map = r->map;
+
+ if (!key) {
+ msg_warn_map("cannot insert NULL value in the map: %s",
+ map->name);
+ return;
+ }
+
+ tok.begin = key;
+ tok.len = strlen(key);
+
+ k = kh_get(rspamd_map_hash, r->htb, tok);
+
+ if (k == kh_end(r->htb)) {
+ nk = rspamd_mempool_strdup(r->pool, key);
+ tok.begin = nk;
+ k = kh_put(rspamd_map_hash, r->htb, tok, &res);
+ }
+ else {
+ val = kh_value(r->htb, k);
+
+ if (strcmp(value, val->value) == 0) {
+ /* Same element, skip */
+ return;
+ }
+ else {
+ msg_warn_map("duplicate radix entry found for map %s: %s (old value: '%s', new: '%s')",
+ map->name, key, val->value, value);
+ }
+
+ nk = kh_key(r->htb, k).begin;
+ val->key = nk;
+ kh_value(r->htb, k) = val;
+
+ return; /* do not touch radix in case of exact duplicate */
+ }
+
+ vlen = strlen(value);
+ val = rspamd_mempool_alloc0(r->pool, sizeof(*val) +
+ vlen + 1);
+ memcpy(val->value, value, vlen);
+ nk = kh_key(r->htb, k).begin;
+ val->key = nk;
+ kh_value(r->htb, k) = val;
+ rspamd_radix_add_iplist(key, ",", r->trie, val, TRUE,
+ r->map->name);
+ rspamd_cryptobox_fast_hash_update(&r->hst, nk, tok.len);
+}
+
+void rspamd_map_helper_insert_hash(gpointer st, gconstpointer key, gconstpointer value)
+{
+ struct rspamd_hash_map_helper *ht = st;
+ struct rspamd_map_helper_value *val;
+ khiter_t k;
+ gconstpointer nk;
+ gsize vlen;
+ gint r;
+ rspamd_ftok_t tok;
+ struct rspamd_map *map;
+
+ tok.begin = key;
+ tok.len = strlen(key);
+ map = ht->map;
+
+ k = kh_get(rspamd_map_hash, ht->htb, tok);
+
+ if (k == kh_end(ht->htb)) {
+ nk = rspamd_mempool_strdup(ht->pool, key);
+ tok.begin = nk;
+ k = kh_put(rspamd_map_hash, ht->htb, tok, &r);
+ }
+ else {
+ val = kh_value(ht->htb, k);
+
+ if (strcmp(value, val->value) == 0) {
+ /* Same element, skip */
+ return;
+ }
+ else {
+ msg_warn_map("duplicate hash entry found for map %s: %s (old value: '%s', new: '%s')",
+ map->name, key, val->value, value);
+ }
+ }
+
+ /* Null termination due to alloc0 */
+ vlen = strlen(value);
+ val = rspamd_mempool_alloc0(ht->pool, sizeof(*val) + vlen + 1);
+ memcpy(val->value, value, vlen);
+
+ tok = kh_key(ht->htb, k);
+ nk = tok.begin;
+ val->key = nk;
+ kh_value(ht->htb, k) = val;
+
+ rspamd_cryptobox_fast_hash_update(&ht->hst, nk, tok.len);
+}
+
+void rspamd_map_helper_insert_re(gpointer st, gconstpointer key, gconstpointer value)
+{
+ struct rspamd_regexp_map_helper *re_map = st;
+ struct rspamd_map *map;
+ rspamd_regexp_t *re;
+ gchar *escaped;
+ GError *err = NULL;
+ gint pcre_flags;
+ gsize escaped_len;
+ struct rspamd_map_helper_value *val;
+ khiter_t k;
+ rspamd_ftok_t tok;
+ gconstpointer nk;
+ gsize vlen;
+ gint r;
+
+ map = re_map->map;
+
+ tok.begin = key;
+ tok.len = strlen(key);
+
+ k = kh_get(rspamd_map_hash, re_map->htb, tok);
+
+ if (k == kh_end(re_map->htb)) {
+ nk = rspamd_mempool_strdup(re_map->pool, key);
+ tok.begin = nk;
+ k = kh_put(rspamd_map_hash, re_map->htb, tok, &r);
+ }
+ else {
+ val = kh_value(re_map->htb, k);
+
+ /* Always warn about regexp duplicate as it's likely a bad mistake */
+ msg_warn_map("duplicate re entry found for map %s: %s (old value: '%s', new: '%s')",
+ map->name, key, val->value, value);
+
+ if (strcmp(val->value, value) == 0) {
+ /* Same value, skip */
+ return;
+ }
+
+ /* Replace value but do not touch regexp */
+ nk = kh_key(re_map->htb, k).begin;
+ val->key = nk;
+ kh_value(re_map->htb, k) = val;
+
+ return;
+ }
+
+ /* Check regexp stuff */
+ if (re_map->map_flags & RSPAMD_REGEXP_MAP_FLAG_GLOB) {
+ escaped = rspamd_str_regexp_escape(key, strlen(key), &escaped_len,
+ RSPAMD_REGEXP_ESCAPE_GLOB | RSPAMD_REGEXP_ESCAPE_UTF);
+ re = rspamd_regexp_new(escaped, NULL, &err);
+ g_free(escaped);
+ }
+ else {
+ re = rspamd_regexp_new(key, NULL, &err);
+ }
+
+ if (re == NULL) {
+ msg_err_map("cannot parse regexp %s: %e", key, err);
+
+ if (err) {
+ g_error_free(err);
+ }
+
+ return;
+ }
+
+ vlen = strlen(value);
+ val = rspamd_mempool_alloc0(re_map->pool, sizeof(*val) +
+ vlen + 1);
+ memcpy(val->value, value, vlen); /* Null terminated due to alloc0 previously */
+ nk = kh_key(re_map->htb, k).begin;
+ val->key = nk;
+ kh_value(re_map->htb, k) = val;
+ rspamd_cryptobox_hash_update(&re_map->hst, nk, tok.len);
+
+ pcre_flags = rspamd_regexp_get_pcre_flags(re);
+
+#ifndef WITH_PCRE2
+ if (pcre_flags & PCRE_FLAG(UTF8)) {
+ re_map->map_flags |= RSPAMD_REGEXP_MAP_FLAG_UTF;
+ }
+#else
+ if (pcre_flags & PCRE_FLAG(UTF)) {
+ re_map->map_flags |= RSPAMD_REGEXP_MAP_FLAG_UTF;
+ }
+#endif
+
+ g_ptr_array_add(re_map->regexps, re);
+ g_ptr_array_add(re_map->values, val);
+}
+
+static void
+rspamd_map_helper_traverse_regexp(void *data,
+ rspamd_map_traverse_cb cb,
+ gpointer cbdata,
+ gboolean reset_hits)
+{
+ rspamd_ftok_t tok;
+ struct rspamd_map_helper_value *val;
+ struct rspamd_regexp_map_helper *re_map = data;
+
+ kh_foreach(re_map->htb, tok, val, {
+ if (!cb(tok.begin, val->value, val->hits, cbdata)) {
+ break;
+ }
+
+ if (reset_hits) {
+ val->hits = 0;
+ }
+ });
+}
+
+struct rspamd_hash_map_helper *
+rspamd_map_helper_new_hash(struct rspamd_map *map)
+{
+ struct rspamd_hash_map_helper *htb;
+ rspamd_mempool_t *pool;
+
+ if (map) {
+ pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ map->tag, 0);
+ }
+ else {
+ pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ NULL, 0);
+ }
+
+ htb = rspamd_mempool_alloc0_type(pool, struct rspamd_hash_map_helper);
+ htb->htb = kh_init(rspamd_map_hash);
+ htb->pool = pool;
+ htb->map = map;
+ rspamd_cryptobox_fast_hash_init(&htb->hst, map_hash_seed);
+
+ return htb;
+}
+
+void rspamd_map_helper_destroy_hash(struct rspamd_hash_map_helper *r)
+{
+ if (r == NULL || r->pool == NULL) {
+ return;
+ }
+
+ rspamd_mempool_t *pool = r->pool;
+ kh_destroy(rspamd_map_hash, r->htb);
+ memset(r, 0, sizeof(*r));
+ rspamd_mempool_delete(pool);
+}
+
+static void
+rspamd_map_helper_traverse_hash(void *data,
+ rspamd_map_traverse_cb cb,
+ gpointer cbdata,
+ gboolean reset_hits)
+{
+ rspamd_ftok_t tok;
+ struct rspamd_map_helper_value *val;
+ struct rspamd_hash_map_helper *ht = data;
+
+ kh_foreach(ht->htb, tok, val, {
+ if (!cb(tok.begin, val->value, val->hits, cbdata)) {
+ break;
+ }
+
+ if (reset_hits) {
+ val->hits = 0;
+ }
+ });
+}
+
+struct rspamd_radix_map_helper *
+rspamd_map_helper_new_radix(struct rspamd_map *map)
+{
+ struct rspamd_radix_map_helper *r;
+ rspamd_mempool_t *pool;
+ const gchar *name = "unnamed";
+
+ if (map) {
+ pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ map->tag, 0);
+ name = map->name;
+ }
+ else {
+ pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ NULL, 0);
+ }
+
+ r = rspamd_mempool_alloc0_type(pool, struct rspamd_radix_map_helper);
+ r->trie = radix_create_compressed_with_pool(pool, name);
+ r->htb = kh_init(rspamd_map_hash);
+ r->pool = pool;
+ r->map = map;
+ rspamd_cryptobox_fast_hash_init(&r->hst, map_hash_seed);
+
+ return r;
+}
+
+void rspamd_map_helper_destroy_radix(struct rspamd_radix_map_helper *r)
+{
+ if (r == NULL || !r->pool) {
+ return;
+ }
+
+ kh_destroy(rspamd_map_hash, r->htb);
+ rspamd_mempool_t *pool = r->pool;
+ memset(r, 0, sizeof(*r));
+ rspamd_mempool_delete(pool);
+}
+
+static void
+rspamd_map_helper_traverse_radix(void *data,
+ rspamd_map_traverse_cb cb,
+ gpointer cbdata,
+ gboolean reset_hits)
+{
+ rspamd_ftok_t tok;
+ struct rspamd_map_helper_value *val;
+ struct rspamd_radix_map_helper *r = data;
+
+ kh_foreach(r->htb, tok, val, {
+ if (!cb(tok.begin, val->value, val->hits, cbdata)) {
+ break;
+ }
+
+ if (reset_hits) {
+ val->hits = 0;
+ }
+ });
+}
+
+struct rspamd_regexp_map_helper *
+rspamd_map_helper_new_regexp(struct rspamd_map *map,
+ enum rspamd_regexp_map_flags flags)
+{
+ struct rspamd_regexp_map_helper *re_map;
+ rspamd_mempool_t *pool;
+
+ pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ map->tag, 0);
+
+ re_map = rspamd_mempool_alloc0_type(pool, struct rspamd_regexp_map_helper);
+ re_map->pool = pool;
+ re_map->values = g_ptr_array_new();
+ re_map->regexps = g_ptr_array_new();
+ re_map->map = map;
+ re_map->map_flags = flags;
+ re_map->htb = kh_init(rspamd_map_hash);
+ rspamd_cryptobox_hash_init(&re_map->hst, NULL, 0);
+
+ return re_map;
+}
+
+
+void rspamd_map_helper_destroy_regexp(struct rspamd_regexp_map_helper *re_map)
+{
+ rspamd_regexp_t *re;
+ guint i;
+
+ if (!re_map || !re_map->regexps) {
+ return;
+ }
+
+#ifdef WITH_HYPERSCAN
+ if (re_map->hs_scratch) {
+ hs_free_scratch(re_map->hs_scratch);
+ }
+ if (re_map->hs_db) {
+ rspamd_hyperscan_free(re_map->hs_db, false);
+ }
+ if (re_map->patterns) {
+ for (i = 0; i < re_map->regexps->len; i++) {
+ g_free(re_map->patterns[i]);
+ }
+
+ g_free(re_map->patterns);
+ }
+ if (re_map->flags) {
+ g_free(re_map->flags);
+ }
+ if (re_map->ids) {
+ g_free(re_map->ids);
+ }
+#endif
+
+ for (i = 0; i < re_map->regexps->len; i++) {
+ re = g_ptr_array_index(re_map->regexps, i);
+ rspamd_regexp_unref(re);
+ }
+
+ g_ptr_array_free(re_map->regexps, TRUE);
+ g_ptr_array_free(re_map->values, TRUE);
+ kh_destroy(rspamd_map_hash, re_map->htb);
+
+ rspamd_mempool_t *pool = re_map->pool;
+ memset(re_map, 0, sizeof(*re_map));
+ rspamd_mempool_delete(pool);
+}
+
+gchar *
+rspamd_kv_list_read(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final)
+{
+ if (data->cur_data == NULL) {
+ data->cur_data = rspamd_map_helper_new_hash(data->map);
+ }
+
+ return rspamd_parse_kv_list(
+ chunk,
+ len,
+ data,
+ rspamd_map_helper_insert_hash,
+ "",
+ final);
+}
+
+void rspamd_kv_list_fin(struct map_cb_data *data, void **target)
+{
+ struct rspamd_map *map = data->map;
+ struct rspamd_hash_map_helper *htb;
+
+ if (data->errored) {
+ /* Clean up the current data and do not touch prev data */
+ if (data->cur_data) {
+ msg_info_map("cleanup unfinished new data as error occurred for %s",
+ map->name);
+ htb = (struct rspamd_hash_map_helper *) data->cur_data;
+ rspamd_map_helper_destroy_hash(htb);
+ data->cur_data = NULL;
+ }
+ }
+ else {
+ if (data->cur_data) {
+ htb = (struct rspamd_hash_map_helper *) data->cur_data;
+ msg_info_map("read hash of %d elements from %s", kh_size(htb->htb),
+ map->name);
+ data->map->traverse_function = rspamd_map_helper_traverse_hash;
+ data->map->nelts = kh_size(htb->htb);
+ data->map->digest = rspamd_cryptobox_fast_hash_final(&htb->hst);
+ }
+
+ if (target) {
+ *target = data->cur_data;
+ }
+
+ if (data->prev_data) {
+ htb = (struct rspamd_hash_map_helper *) data->prev_data;
+ rspamd_map_helper_destroy_hash(htb);
+ }
+ }
+}
+
+void rspamd_kv_list_dtor(struct map_cb_data *data)
+{
+ struct rspamd_hash_map_helper *htb;
+
+ if (data->cur_data) {
+ htb = (struct rspamd_hash_map_helper *) data->cur_data;
+ rspamd_map_helper_destroy_hash(htb);
+ }
+}
+
+gchar *
+rspamd_radix_read(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final)
+{
+ struct rspamd_radix_map_helper *r;
+ struct rspamd_map *map = data->map;
+
+ if (data->cur_data == NULL) {
+ r = rspamd_map_helper_new_radix(map);
+ data->cur_data = r;
+ }
+
+ return rspamd_parse_kv_list(
+ chunk,
+ len,
+ data,
+ rspamd_map_helper_insert_radix,
+ hash_fill,
+ final);
+}
+
+void rspamd_radix_fin(struct map_cb_data *data, void **target)
+{
+ struct rspamd_map *map = data->map;
+ struct rspamd_radix_map_helper *r;
+
+ if (data->errored) {
+ /* Clean up the current data and do not touch prev data */
+ if (data->cur_data) {
+ msg_info_map("cleanup unfinished new data as error occurred for %s",
+ map->name);
+ r = (struct rspamd_radix_map_helper *) data->cur_data;
+ rspamd_map_helper_destroy_radix(r);
+ data->cur_data = NULL;
+ }
+ }
+ else {
+ if (data->cur_data) {
+ r = (struct rspamd_radix_map_helper *) data->cur_data;
+ msg_info_map("read radix trie of %z elements: %s",
+ radix_get_size(r->trie), radix_get_info(r->trie));
+ data->map->traverse_function = rspamd_map_helper_traverse_radix;
+ data->map->nelts = kh_size(r->htb);
+ data->map->digest = rspamd_cryptobox_fast_hash_final(&r->hst);
+ }
+
+ if (target) {
+ *target = data->cur_data;
+ }
+
+ if (data->prev_data) {
+ r = (struct rspamd_radix_map_helper *) data->prev_data;
+ rspamd_map_helper_destroy_radix(r);
+ }
+ }
+}
+
+void rspamd_radix_dtor(struct map_cb_data *data)
+{
+ struct rspamd_radix_map_helper *r;
+
+ if (data->cur_data) {
+ r = (struct rspamd_radix_map_helper *) data->cur_data;
+ rspamd_map_helper_destroy_radix(r);
+ }
+}
+
+#ifdef WITH_HYPERSCAN
+
+static gboolean
+rspamd_try_load_re_map_cache(struct rspamd_regexp_map_helper *re_map)
+{
+ gchar fp[PATH_MAX];
+ struct rspamd_map *map;
+
+ map = re_map->map;
+
+ if (!map->cfg->hs_cache_dir) {
+ return FALSE;
+ }
+
+ rspamd_snprintf(fp, sizeof(fp), "%s/%*xs.hsmc",
+ map->cfg->hs_cache_dir,
+ (gint) rspamd_cryptobox_HASHBYTES / 2, re_map->re_digest);
+
+ re_map->hs_db = rspamd_hyperscan_maybe_load(fp, 0);
+
+ return re_map->hs_db != NULL;
+}
+
+static gboolean
+rspamd_try_save_re_map_cache(struct rspamd_regexp_map_helper *re_map)
+{
+ gchar fp[PATH_MAX], np[PATH_MAX];
+ gsize len;
+ gint fd;
+ char *bytes = NULL;
+ struct rspamd_map *map;
+
+ map = re_map->map;
+
+ if (!map->cfg->hs_cache_dir) {
+ return FALSE;
+ }
+
+ rspamd_snprintf(fp, sizeof(fp), "%s/hsmc-XXXXXXXXXXXXX",
+ re_map->map->cfg->hs_cache_dir);
+
+ if ((fd = g_mkstemp_full(fp, O_WRONLY | O_CREAT | O_EXCL, 00644)) != -1) {
+ if (hs_serialize_database(rspamd_hyperscan_get_database(re_map->hs_db), &bytes, &len) == HS_SUCCESS) {
+ if (write(fd, bytes, len) == -1) {
+ msg_warn_map("cannot write hyperscan cache to %s: %s",
+ fp, strerror(errno));
+ unlink(fp);
+ free(bytes);
+ }
+ else {
+ free(bytes);
+ fsync(fd);
+
+ rspamd_snprintf(np, sizeof(np), "%s/%*xs.hsmc",
+ re_map->map->cfg->hs_cache_dir,
+ (gint) rspamd_cryptobox_HASHBYTES / 2, re_map->re_digest);
+
+ if (rename(fp, np) == -1) {
+ msg_warn_map("cannot rename hyperscan cache from %s to %s: %s",
+ fp, np, strerror(errno));
+ unlink(fp);
+ }
+ else {
+ msg_info_map("written cached hyperscan data for %s to %s (%Hz length)",
+ map->name, np, len);
+ rspamd_hyperscan_notice_known(np);
+ }
+ }
+ }
+ else {
+ msg_warn_map("cannot serialize hyperscan cache to %s: %s",
+ fp, strerror(errno));
+ unlink(fp);
+ }
+
+
+ close(fd);
+ }
+
+ return FALSE;
+}
+
+#endif
+
+static void
+rspamd_re_map_finalize(struct rspamd_regexp_map_helper *re_map)
+{
+#ifdef WITH_HYPERSCAN
+ guint i;
+ hs_platform_info_t plt;
+ hs_compile_error_t *err;
+ struct rspamd_map *map;
+ rspamd_regexp_t *re;
+ gint pcre_flags;
+
+ map = re_map->map;
+
+#if !defined(__aarch64__) && !defined(__powerpc64__)
+ if (!(map->cfg->libs_ctx->crypto_ctx->cpu_config & CPUID_SSSE3)) {
+ msg_info_map("disable hyperscan for map %s, ssse3 instructions are not supported by CPU",
+ map->name);
+ return;
+ }
+#endif
+
+ if (hs_populate_platform(&plt) != HS_SUCCESS) {
+ msg_err_map("cannot populate hyperscan platform");
+ return;
+ }
+
+ re_map->patterns = g_new(gchar *, re_map->regexps->len);
+ re_map->flags = g_new(gint, re_map->regexps->len);
+ re_map->ids = g_new(gint, re_map->regexps->len);
+
+ for (i = 0; i < re_map->regexps->len; i++) {
+ const gchar *pat;
+ gchar *escaped;
+ gint pat_flags;
+
+ re = g_ptr_array_index(re_map->regexps, i);
+ pcre_flags = rspamd_regexp_get_pcre_flags(re);
+ pat = rspamd_regexp_get_pattern(re);
+ pat_flags = rspamd_regexp_get_flags(re);
+
+ if (pat_flags & RSPAMD_REGEXP_FLAG_UTF) {
+ escaped = rspamd_str_regexp_escape(pat, strlen(pat), NULL,
+ RSPAMD_REGEXP_ESCAPE_RE | RSPAMD_REGEXP_ESCAPE_UTF);
+ re_map->flags[i] |= HS_FLAG_UTF8;
+ }
+ else {
+ escaped = rspamd_str_regexp_escape(pat, strlen(pat), NULL,
+ RSPAMD_REGEXP_ESCAPE_RE);
+ }
+
+ re_map->patterns[i] = escaped;
+ re_map->flags[i] = HS_FLAG_SINGLEMATCH;
+
+#ifndef WITH_PCRE2
+ if (pcre_flags & PCRE_FLAG(UTF8)) {
+ re_map->flags[i] |= HS_FLAG_UTF8;
+ }
+#else
+ if (pcre_flags & PCRE_FLAG(UTF)) {
+ re_map->flags[i] |= HS_FLAG_UTF8;
+ }
+#endif
+ if (pcre_flags & PCRE_FLAG(CASELESS)) {
+ re_map->flags[i] |= HS_FLAG_CASELESS;
+ }
+ if (pcre_flags & PCRE_FLAG(MULTILINE)) {
+ re_map->flags[i] |= HS_FLAG_MULTILINE;
+ }
+ if (pcre_flags & PCRE_FLAG(DOTALL)) {
+ re_map->flags[i] |= HS_FLAG_DOTALL;
+ }
+ if (rspamd_regexp_get_maxhits(re) == 1) {
+ re_map->flags[i] |= HS_FLAG_SINGLEMATCH;
+ }
+
+ re_map->ids[i] = i;
+ }
+
+ if (re_map->regexps->len > 0 && re_map->patterns) {
+
+ if (!rspamd_try_load_re_map_cache(re_map)) {
+ gdouble ts1 = rspamd_get_ticks(FALSE);
+ hs_database_t *hs_db = NULL;
+
+ if (hs_compile_multi((const gchar **) re_map->patterns,
+ re_map->flags,
+ re_map->ids,
+ re_map->regexps->len,
+ HS_MODE_BLOCK,
+ &plt,
+ &hs_db,
+ &err) != HS_SUCCESS) {
+
+ msg_err_map("cannot create tree of regexp when processing '%s': %s",
+ err->expression >= 0 ? re_map->patterns[err->expression] : "unknown regexp", err->message);
+ re_map->hs_db = NULL;
+ hs_free_compile_error(err);
+
+ return;
+ }
+
+ if (re_map->map->cfg->hs_cache_dir) {
+ char fpath[PATH_MAX];
+ rspamd_snprintf(fpath, sizeof(fpath), "%s/%*xs.hsmc",
+ re_map->map->cfg->hs_cache_dir,
+ (gint) rspamd_cryptobox_HASHBYTES / 2, re_map->re_digest);
+ re_map->hs_db = rspamd_hyperscan_from_raw_db(hs_db, fpath);
+ }
+ else {
+ re_map->hs_db = rspamd_hyperscan_from_raw_db(hs_db, NULL);
+ }
+
+ ts1 = (rspamd_get_ticks(FALSE) - ts1) * 1000.0;
+ msg_info_map("hyperscan compiled %d regular expressions from %s in %.1f ms",
+ re_map->regexps->len, re_map->map->name, ts1);
+ rspamd_try_save_re_map_cache(re_map);
+ }
+ else {
+ msg_info_map("hyperscan read %d cached regular expressions from %s",
+ re_map->regexps->len, re_map->map->name);
+ }
+
+ if (hs_alloc_scratch(rspamd_hyperscan_get_database(re_map->hs_db), &re_map->hs_scratch) != HS_SUCCESS) {
+ msg_err_map("cannot allocate scratch space for hyperscan");
+ rspamd_hyperscan_free(re_map->hs_db, true);
+ re_map->hs_db = NULL;
+ }
+ }
+ else {
+ msg_err_map("regexp map is empty");
+ }
+#endif
+}
+
+gchar *
+rspamd_regexp_list_read_single(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final)
+{
+ struct rspamd_regexp_map_helper *re_map;
+
+ if (data->cur_data == NULL) {
+ re_map = rspamd_map_helper_new_regexp(data->map, 0);
+ data->cur_data = re_map;
+ }
+
+ return rspamd_parse_kv_list(
+ chunk,
+ len,
+ data,
+ rspamd_map_helper_insert_re,
+ hash_fill,
+ final);
+}
+
+gchar *
+rspamd_glob_list_read_single(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final)
+{
+ struct rspamd_regexp_map_helper *re_map;
+
+ if (data->cur_data == NULL) {
+ re_map = rspamd_map_helper_new_regexp(data->map, RSPAMD_REGEXP_MAP_FLAG_GLOB);
+ data->cur_data = re_map;
+ }
+
+ return rspamd_parse_kv_list(
+ chunk,
+ len,
+ data,
+ rspamd_map_helper_insert_re,
+ hash_fill,
+ final);
+}
+
+gchar *
+rspamd_regexp_list_read_multiple(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final)
+{
+ struct rspamd_regexp_map_helper *re_map;
+
+ if (data->cur_data == NULL) {
+ re_map = rspamd_map_helper_new_regexp(data->map,
+ RSPAMD_REGEXP_MAP_FLAG_MULTIPLE);
+ data->cur_data = re_map;
+ }
+
+ return rspamd_parse_kv_list(
+ chunk,
+ len,
+ data,
+ rspamd_map_helper_insert_re,
+ hash_fill,
+ final);
+}
+
+gchar *
+rspamd_glob_list_read_multiple(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final)
+{
+ struct rspamd_regexp_map_helper *re_map;
+
+ if (data->cur_data == NULL) {
+ re_map = rspamd_map_helper_new_regexp(data->map,
+ RSPAMD_REGEXP_MAP_FLAG_GLOB | RSPAMD_REGEXP_MAP_FLAG_MULTIPLE);
+ data->cur_data = re_map;
+ }
+
+ return rspamd_parse_kv_list(
+ chunk,
+ len,
+ data,
+ rspamd_map_helper_insert_re,
+ hash_fill,
+ final);
+}
+
+
+void rspamd_regexp_list_fin(struct map_cb_data *data, void **target)
+{
+ struct rspamd_regexp_map_helper *re_map = NULL, *old_re_map;
+ struct rspamd_map *map = data->map;
+
+ if (data->errored) {
+ /* Clean up the current data and do not touch prev data */
+ if (data->cur_data) {
+ msg_info_map("cleanup unfinished new data as error occurred for %s",
+ map->name);
+ re_map = (struct rspamd_regexp_map_helper *) data->cur_data;
+ rspamd_map_helper_destroy_regexp(re_map);
+ data->cur_data = NULL;
+ }
+ }
+ else {
+ if (data->cur_data) {
+ re_map = data->cur_data;
+ rspamd_cryptobox_hash_final(&re_map->hst, re_map->re_digest);
+ memcpy(&data->map->digest, re_map->re_digest, sizeof(data->map->digest));
+ rspamd_re_map_finalize(re_map);
+ msg_info_map("read regexp list of %ud elements",
+ re_map->regexps->len);
+ data->map->traverse_function = rspamd_map_helper_traverse_regexp;
+ data->map->nelts = kh_size(re_map->htb);
+ }
+
+ if (target) {
+ *target = data->cur_data;
+ }
+
+ if (data->prev_data) {
+ old_re_map = data->prev_data;
+ rspamd_map_helper_destroy_regexp(old_re_map);
+ }
+ }
+}
+void rspamd_regexp_list_dtor(struct map_cb_data *data)
+{
+ if (data->cur_data) {
+ rspamd_map_helper_destroy_regexp(data->cur_data);
+ }
+}
+
+#ifdef WITH_HYPERSCAN
+static int
+rspamd_match_hs_single_handler(unsigned int id, unsigned long long from,
+ unsigned long long to,
+ unsigned int flags, void *context)
+{
+ guint *i = context;
+ /* Always return non-zero as we need a single match here */
+
+ *i = id;
+
+ return 1;
+}
+#endif
+
+gconstpointer
+rspamd_match_regexp_map_single(struct rspamd_regexp_map_helper *map,
+ const gchar *in, gsize len)
+{
+ guint i;
+ rspamd_regexp_t *re;
+ gint res = 0;
+ gpointer ret = NULL;
+ struct rspamd_map_helper_value *val;
+ gboolean validated = FALSE;
+
+ g_assert(in != NULL);
+
+ if (map == NULL || len == 0 || map->regexps == NULL) {
+ return NULL;
+ }
+
+ if (map->map_flags & RSPAMD_REGEXP_MAP_FLAG_UTF) {
+ if (rspamd_fast_utf8_validate(in, len) == 0) {
+ validated = TRUE;
+ }
+ }
+ else {
+ validated = TRUE;
+ }
+
+#ifdef WITH_HYPERSCAN
+ if (map->hs_db && map->hs_scratch) {
+
+ if (validated) {
+
+ res = hs_scan(rspamd_hyperscan_get_database(map->hs_db), in, len, 0,
+ map->hs_scratch,
+ rspamd_match_hs_single_handler, (void *) &i);
+
+ if (res == HS_SCAN_TERMINATED) {
+ res = 1;
+ val = g_ptr_array_index(map->values, i);
+
+ ret = val->value;
+ val->hits++;
+ }
+
+ return ret;
+ }
+ }
+#endif
+
+ if (!res) {
+ /* PCRE version */
+ for (i = 0; i < map->regexps->len; i++) {
+ re = g_ptr_array_index(map->regexps, i);
+
+ if (rspamd_regexp_search(re, in, len, NULL, NULL, !validated, NULL)) {
+ val = g_ptr_array_index(map->values, i);
+
+ ret = val->value;
+ val->hits++;
+ break;
+ }
+ }
+ }
+
+ return ret;
+}
+
+#ifdef WITH_HYPERSCAN
+struct rspamd_multiple_cbdata {
+ GPtrArray *ar;
+ struct rspamd_regexp_map_helper *map;
+};
+
+static int
+rspamd_match_hs_multiple_handler(unsigned int id, unsigned long long from,
+ unsigned long long to,
+ unsigned int flags, void *context)
+{
+ struct rspamd_multiple_cbdata *cbd = context;
+ struct rspamd_map_helper_value *val;
+
+
+ if (id < cbd->map->values->len) {
+ val = g_ptr_array_index(cbd->map->values, id);
+ val->hits++;
+ g_ptr_array_add(cbd->ar, val->value);
+ }
+
+ /* Always return zero as we need all matches here */
+ return 0;
+}
+#endif
+
+GPtrArray *
+rspamd_match_regexp_map_all(struct rspamd_regexp_map_helper *map,
+ const gchar *in, gsize len)
+{
+ guint i;
+ rspamd_regexp_t *re;
+ GPtrArray *ret;
+ gint res = 0;
+ gboolean validated = FALSE;
+ struct rspamd_map_helper_value *val;
+
+ if (map == NULL || map->regexps == NULL || len == 0) {
+ return NULL;
+ }
+
+ g_assert(in != NULL);
+
+ if (map->map_flags & RSPAMD_REGEXP_MAP_FLAG_UTF) {
+ if (rspamd_fast_utf8_validate(in, len) == 0) {
+ validated = TRUE;
+ }
+ }
+ else {
+ validated = TRUE;
+ }
+
+ ret = g_ptr_array_new();
+
+#ifdef WITH_HYPERSCAN
+ if (map->hs_db && map->hs_scratch) {
+
+ if (validated) {
+ struct rspamd_multiple_cbdata cbd;
+
+ cbd.ar = ret;
+ cbd.map = map;
+
+ if (hs_scan(rspamd_hyperscan_get_database(map->hs_db), in, len,
+ 0, map->hs_scratch,
+ rspamd_match_hs_multiple_handler, &cbd) == HS_SUCCESS) {
+ res = 1;
+ }
+ }
+ }
+#endif
+
+ if (!res) {
+ /* PCRE version */
+ for (i = 0; i < map->regexps->len; i++) {
+ re = g_ptr_array_index(map->regexps, i);
+
+ if (rspamd_regexp_search(re, in, len, NULL, NULL,
+ !validated, NULL)) {
+ val = g_ptr_array_index(map->values, i);
+ val->hits++;
+ g_ptr_array_add(ret, val->value);
+ }
+ }
+ }
+
+ if (ret->len > 0) {
+ return ret;
+ }
+
+ g_ptr_array_free(ret, TRUE);
+
+ return NULL;
+}
+
+gconstpointer
+rspamd_match_hash_map(struct rspamd_hash_map_helper *map, const gchar *in,
+ gsize len)
+{
+ khiter_t k;
+ struct rspamd_map_helper_value *val;
+ rspamd_ftok_t tok;
+
+ if (map == NULL || map->htb == NULL) {
+ return NULL;
+ }
+
+ tok.begin = in;
+ tok.len = len;
+
+ k = kh_get(rspamd_map_hash, map->htb, tok);
+
+ if (k != kh_end(map->htb)) {
+ val = kh_value(map->htb, k);
+ val->hits++;
+
+ return val->value;
+ }
+
+ return NULL;
+}
+
+gconstpointer
+rspamd_match_radix_map(struct rspamd_radix_map_helper *map,
+ const guchar *in, gsize inlen)
+{
+ struct rspamd_map_helper_value *val;
+
+ if (map == NULL || map->trie == NULL) {
+ return NULL;
+ }
+
+ val = (struct rspamd_map_helper_value *) radix_find_compressed(map->trie,
+ in, inlen);
+
+ if (val != (gconstpointer) RADIX_NO_VALUE) {
+ val->hits++;
+
+ return val->value;
+ }
+
+ return NULL;
+}
+
+gconstpointer
+rspamd_match_radix_map_addr(struct rspamd_radix_map_helper *map,
+ const rspamd_inet_addr_t *addr)
+{
+ struct rspamd_map_helper_value *val;
+
+ if (map == NULL || map->trie == NULL) {
+ return NULL;
+ }
+
+ val = (struct rspamd_map_helper_value *) radix_find_compressed_addr(map->trie, addr);
+
+ if (val != (gconstpointer) RADIX_NO_VALUE) {
+ val->hits++;
+
+ return val->value;
+ }
+
+ return NULL;
+}
+
+
+/*
+ * CBD stuff
+ */
+
+struct rspamd_cdb_map_helper *
+rspamd_map_helper_new_cdb(struct rspamd_map *map)
+{
+ struct rspamd_cdb_map_helper *n;
+
+ n = g_malloc0(sizeof(*n));
+ n->cdbs = (GQueue) G_QUEUE_INIT;
+ n->map = map;
+
+ rspamd_cryptobox_fast_hash_init(&n->hst, map_hash_seed);
+
+ return n;
+}
+
+void rspamd_map_helper_destroy_cdb(struct rspamd_cdb_map_helper *c)
+{
+ if (c == NULL) {
+ return;
+ }
+
+ GList *cur = c->cdbs.head;
+
+ while (cur) {
+ struct cdb *cdb = (struct cdb *) cur->data;
+
+ cdb_free(cdb);
+ g_free(cdb->filename);
+ close(cdb->cdb_fd);
+ g_free(cdb);
+
+ cur = g_list_next(cur);
+ }
+
+ g_queue_clear(&c->cdbs);
+
+ g_free(c);
+}
+
+gchar *
+rspamd_cdb_list_read(gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final)
+{
+ struct rspamd_cdb_map_helper *cdb_data;
+ struct cdb *found = NULL;
+ struct rspamd_map *map = data->map;
+
+ g_assert(map->no_file_read);
+
+ if (data->cur_data == NULL) {
+ cdb_data = rspamd_map_helper_new_cdb(data->map);
+ data->cur_data = cdb_data;
+ }
+ else {
+ cdb_data = (struct rspamd_cdb_map_helper *) data->cur_data;
+ }
+
+ GList *cur = cdb_data->cdbs.head;
+
+ while (cur) {
+ struct cdb *elt = (struct cdb *) cur->data;
+
+ if (strcmp(elt->filename, chunk) == 0) {
+ found = elt;
+ break;
+ }
+
+ cur = g_list_next(cur);
+ }
+
+ if (found == NULL) {
+ /* New cdb */
+ gint fd;
+ struct cdb *cdb;
+
+ fd = rspamd_file_xopen(chunk, O_RDONLY, 0, TRUE);
+
+ if (fd == -1) {
+ msg_err_map("cannot open cdb map from %s: %s", chunk, strerror(errno));
+
+ return NULL;
+ }
+
+ cdb = g_malloc0(sizeof(struct cdb));
+
+ if (cdb_init(cdb, fd) == -1) {
+ g_free(cdb);
+ msg_err_map("cannot init cdb map from %s: %s", chunk, strerror(errno));
+
+ return NULL;
+ }
+
+ cdb->filename = g_strdup(chunk);
+ g_queue_push_tail(&cdb_data->cdbs, cdb);
+ cdb_data->total_size += cdb->cdb_fsize;
+ rspamd_cryptobox_fast_hash_update(&cdb_data->hst, chunk, len);
+ }
+
+ return chunk + len;
+}
+
+void rspamd_cdb_list_fin(struct map_cb_data *data, void **target)
+{
+ struct rspamd_map *map = data->map;
+ struct rspamd_cdb_map_helper *cdb_data;
+
+ if (data->errored) {
+ /* Clean up the current data and do not touch prev data */
+ if (data->cur_data) {
+ msg_info_map("cleanup unfinished new data as error occurred for %s",
+ map->name);
+ cdb_data = (struct rspamd_cdb_map_helper *) data->cur_data;
+ rspamd_map_helper_destroy_cdb(cdb_data);
+ data->cur_data = NULL;
+ }
+ }
+ else {
+ if (data->cur_data) {
+ cdb_data = (struct rspamd_cdb_map_helper *) data->cur_data;
+ msg_info_map("read cdb of %Hz size", cdb_data->total_size);
+ data->map->traverse_function = NULL;
+ data->map->nelts = 0;
+ data->map->digest = rspamd_cryptobox_fast_hash_final(&cdb_data->hst);
+ }
+
+ if (target) {
+ *target = data->cur_data;
+ }
+
+ if (data->prev_data) {
+ cdb_data = (struct rspamd_cdb_map_helper *) data->prev_data;
+ rspamd_map_helper_destroy_cdb(cdb_data);
+ }
+ }
+}
+void rspamd_cdb_list_dtor(struct map_cb_data *data)
+{
+ if (data->cur_data) {
+ rspamd_map_helper_destroy_cdb(data->cur_data);
+ }
+}
+
+gconstpointer
+rspamd_match_cdb_map(struct rspamd_cdb_map_helper *map,
+ const gchar *in, gsize inlen)
+{
+ if (map == NULL || map->cdbs.head == NULL) {
+ return NULL;
+ }
+
+ GList *cur = map->cdbs.head;
+ static rspamd_ftok_t found;
+
+ while (cur) {
+ struct cdb *cdb = (struct cdb *) cur->data;
+
+ if (cdb_find(cdb, in, inlen) > 0) {
+ /* Extract and push value to lua as string */
+ unsigned vlen;
+ gconstpointer vpos;
+
+ vpos = cdb->cdb_mem + cdb_datapos(cdb);
+ vlen = cdb_datalen(cdb);
+ found.len = vlen;
+ found.begin = vpos;
+
+ return &found; /* Do not reuse! */
+ }
+
+ cur = g_list_next(cur);
+ }
+
+ return NULL;
+}
diff --git a/src/libserver/maps/map_helpers.h b/src/libserver/maps/map_helpers.h
new file mode 100644
index 0000000..82c62b6
--- /dev/null
+++ b/src/libserver/maps/map_helpers.h
@@ -0,0 +1,269 @@
+/*-
+ * Copyright 2018 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_MAP_HELPERS_H
+#define RSPAMD_MAP_HELPERS_H
+
+#include "config.h"
+#include "map.h"
+#include "addr.h"
+
+/**
+ * @file map_helpers.h
+ *
+ * Defines helper structures to deal with different map types
+ */
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Common structures, abstract for simplicity
+ */
+struct rspamd_radix_map_helper;
+struct rspamd_hash_map_helper;
+struct rspamd_regexp_map_helper;
+struct rspamd_cdb_map_helper;
+struct rspamd_map_helper_value;
+
+enum rspamd_regexp_map_flags {
+ RSPAMD_REGEXP_MAP_FLAG_UTF = (1u << 0),
+ RSPAMD_REGEXP_MAP_FLAG_MULTIPLE = (1u << 1),
+ RSPAMD_REGEXP_MAP_FLAG_GLOB = (1u << 2),
+};
+
+typedef void (*rspamd_map_insert_func)(gpointer st, gconstpointer key,
+ gconstpointer value);
+
+/**
+ * Radix list is a list like ip/mask
+ */
+gchar *rspamd_radix_read(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final);
+
+void rspamd_radix_fin(struct map_cb_data *data, void **target);
+
+void rspamd_radix_dtor(struct map_cb_data *data);
+
+/**
+ * Kv list is an ordinal list of keys and values separated by whitespace
+ */
+gchar *rspamd_kv_list_read(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final);
+
+void rspamd_kv_list_fin(struct map_cb_data *data, void **target);
+
+void rspamd_kv_list_dtor(struct map_cb_data *data);
+
+/**
+ * Cdb is a cdb mapped file with shared data
+ * chunk must be filename!
+ */
+gchar *rspamd_cdb_list_read(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final);
+void rspamd_cdb_list_fin(struct map_cb_data *data, void **target);
+void rspamd_cdb_list_dtor(struct map_cb_data *data);
+
+/**
+ * Regexp list is a list of regular expressions
+ */
+
+gchar *rspamd_regexp_list_read_single(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final);
+
+gchar *rspamd_regexp_list_read_multiple(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final);
+
+gchar *rspamd_glob_list_read_single(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final);
+
+gchar *rspamd_glob_list_read_multiple(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ gboolean final);
+
+void rspamd_regexp_list_fin(struct map_cb_data *data, void **target);
+
+void rspamd_regexp_list_dtor(struct map_cb_data *data);
+
+/**
+ * FSM for lists parsing (support comments, blank lines and partial replies)
+ */
+gchar *
+rspamd_parse_kv_list(
+ gchar *chunk,
+ gint len,
+ struct map_cb_data *data,
+ rspamd_map_insert_func func,
+ const gchar *default_value,
+ gboolean final);
+
+/**
+ * Find a single (any) matching regexp for the specified text or NULL if
+ * no matches found
+ * @param map
+ * @param in
+ * @param len
+ * @return
+ */
+gconstpointer rspamd_match_regexp_map_single(struct rspamd_regexp_map_helper *map,
+ const gchar *in, gsize len);
+
+/**
+ * Find a multiple (all) matching regexp for the specified text or NULL if
+ * no matches found. Returns GPtrArray that *must* be freed by a caller if not NULL
+ * @param map
+ * @param in
+ * @param len
+ * @return
+ */
+GPtrArray *rspamd_match_regexp_map_all(struct rspamd_regexp_map_helper *map,
+ const gchar *in, gsize len);
+
+/**
+ * Find value matching specific key in a hash map
+ * @param map
+ * @param in
+ * @param len
+ * @return
+ */
+gconstpointer rspamd_match_hash_map(struct rspamd_hash_map_helper *map,
+ const gchar *in, gsize len);
+
+/**
+ * Find value matching specific key in a cdb map
+ * @param map
+ * @param in
+ * @param len
+ * @return rspamd_ftok_t pointer (allocated in a static buffer!)
+ */
+gconstpointer rspamd_match_cdb_map(struct rspamd_cdb_map_helper *map,
+ const gchar *in, gsize len);
+
+/**
+ * Find value matching specific key in a hash map
+ * @param map
+ * @param in raw ip address
+ * @param inlen ip address length (4 for IPv4 and 16 for IPv6)
+ * @return
+ */
+gconstpointer rspamd_match_radix_map(struct rspamd_radix_map_helper *map,
+ const guchar *in, gsize inlen);
+
+gconstpointer rspamd_match_radix_map_addr(struct rspamd_radix_map_helper *map,
+ const rspamd_inet_addr_t *addr);
+
+/**
+ * Creates radix map helper
+ * @param map
+ * @return
+ */
+struct rspamd_radix_map_helper *rspamd_map_helper_new_radix(struct rspamd_map *map);
+
+/**
+ * Inserts new value into radix map
+ * @param st
+ * @param key
+ * @param value
+ */
+void rspamd_map_helper_insert_radix(gpointer st, gconstpointer key, gconstpointer value);
+
+/**
+ * Inserts new value into radix map performing synchronous resolving
+ * @param st
+ * @param key
+ * @param value
+ */
+void rspamd_map_helper_insert_radix_resolve(gpointer st, gconstpointer key,
+ gconstpointer value);
+
+/**
+ * Destroys radix map helper
+ * @param r
+ */
+void rspamd_map_helper_destroy_radix(struct rspamd_radix_map_helper *r);
+
+
+/**
+ * Creates hash map helper
+ * @param map
+ * @return
+ */
+struct rspamd_hash_map_helper *rspamd_map_helper_new_hash(struct rspamd_map *map);
+
+/**
+ * Inserts a new value into a hash map
+ * @param st
+ * @param key
+ * @param value
+ */
+void rspamd_map_helper_insert_hash(gpointer st, gconstpointer key, gconstpointer value);
+
+/**
+ * Destroys hash map helper
+ * @param r
+ */
+void rspamd_map_helper_destroy_hash(struct rspamd_hash_map_helper *r);
+
+/**
+ * Create new regexp map
+ * @param map
+ * @param flags
+ * @return
+ */
+struct rspamd_regexp_map_helper *rspamd_map_helper_new_regexp(struct rspamd_map *map,
+ enum rspamd_regexp_map_flags flags);
+
+/**
+ * Inserts a new regexp into regexp map
+ * @param st
+ * @param key
+ * @param value
+ */
+void rspamd_map_helper_insert_re(gpointer st, gconstpointer key, gconstpointer value);
+
+/**
+ * Destroy regexp map
+ * @param re_map
+ */
+void rspamd_map_helper_destroy_regexp(struct rspamd_regexp_map_helper *re_map);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/maps/map_private.h b/src/libserver/maps/map_private.h
new file mode 100644
index 0000000..60751c0
--- /dev/null
+++ b/src/libserver/maps/map_private.h
@@ -0,0 +1,226 @@
+/*-
+ * 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 SRC_LIBUTIL_MAP_PRIVATE_H_
+#define SRC_LIBUTIL_MAP_PRIVATE_H_
+
+#include "config.h"
+#include "mem_pool.h"
+#include "keypair.h"
+#include "unix-std.h"
+#include "map.h"
+#include "ref.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void (*rspamd_map_tmp_dtor)(gpointer p);
+
+extern guint rspamd_map_log_id;
+#define msg_err_map(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "map", map->tag, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_map(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ "map", map->tag, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_map(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ "map", map->tag, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_map(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_map_log_id, "map", map->tag, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+enum fetch_proto {
+ MAP_PROTO_FILE,
+ MAP_PROTO_HTTP,
+ MAP_PROTO_HTTPS,
+ MAP_PROTO_STATIC
+};
+
+/**
+ * Data specific to file maps
+ */
+struct file_map_data {
+ gchar *filename;
+ gboolean need_modify;
+ ev_stat st_ev;
+};
+
+
+struct http_map_data;
+
+struct rspamd_http_map_cached_cbdata {
+ ev_timer timeout;
+ struct ev_loop *event_loop;
+ struct rspamd_storage_shmem *shm;
+ struct rspamd_map *map;
+ struct http_map_data *data;
+ guint64 gen;
+ time_t last_checked;
+};
+
+struct rspamd_map_cachepoint {
+ gint available;
+ gsize len;
+ time_t last_modified;
+ gchar shmem_name[256];
+};
+
+/**
+ * Data specific to HTTP maps
+ */
+struct http_map_data {
+ /* Shared cache data */
+ struct rspamd_map_cachepoint *cache;
+ /* Non-shared for cache owner, used to cleanup cache */
+ struct rspamd_http_map_cached_cbdata *cur_cache_cbd;
+ gchar *userinfo;
+ gchar *path;
+ gchar *host;
+ gchar *rest;
+ rspamd_fstring_t *etag;
+ time_t last_modified;
+ time_t last_checked;
+ gboolean request_sent;
+ guint64 gen;
+ guint16 port;
+};
+
+struct static_map_data {
+ guchar *data;
+ gsize len;
+ gboolean processed;
+};
+
+union rspamd_map_backend_data {
+ struct file_map_data *fd;
+ struct http_map_data *hd;
+ struct static_map_data *sd;
+};
+
+
+struct rspamd_map;
+struct rspamd_map_backend {
+ enum fetch_proto protocol;
+ gboolean is_signed;
+ gboolean is_compressed;
+ gboolean is_fallback;
+ struct rspamd_map *map;
+ struct ev_loop *event_loop;
+ guint32 id;
+ struct rspamd_cryptobox_pubkey *trusted_pubkey;
+ union rspamd_map_backend_data data;
+ gchar *uri;
+ ref_entry_t ref;
+};
+
+struct map_periodic_cbdata;
+
+struct rspamd_map {
+ struct rspamd_dns_resolver *r;
+ struct rspamd_config *cfg;
+ GPtrArray *backends;
+ struct rspamd_map_backend *fallback_backend;
+ map_cb_t read_callback;
+ map_fin_cb_t fin_callback;
+ map_dtor_t dtor;
+ void **user_data;
+ struct ev_loop *event_loop;
+ struct rspamd_worker *wrk;
+ gchar *description;
+ gchar *name;
+ guint32 id;
+ struct map_periodic_cbdata *scheduled_check;
+ rspamd_map_tmp_dtor tmp_dtor;
+ gpointer tmp_dtor_data;
+ rspamd_map_traverse_function traverse_function;
+ rspamd_map_on_load_function on_load_function;
+ gpointer on_load_ud;
+ GDestroyNotify on_load_ud_dtor;
+ gpointer lua_map;
+ gsize nelts;
+ guint64 digest;
+ /* Should we check HTTP or just load cached data */
+ ev_tstamp timeout;
+ gdouble poll_timeout;
+ time_t next_check;
+ bool active_http;
+ bool non_trivial; /* E.g. has http backends in active mode */
+ bool file_only; /* No HTTP backends found */
+ bool static_only; /* No need to check */
+ bool no_file_read; /* Do not read files */
+ /* Shared lock for temporary disabling of map reading (e.g. when this map is written by UI) */
+ gint *locked;
+ gchar tag[MEMPOOL_UID_LEN];
+};
+
+enum rspamd_map_http_stage {
+ http_map_resolve_host2 = 0, /* 2 requests sent */
+ http_map_resolve_host1, /* 1 requests sent */
+ http_map_http_conn, /* http connection */
+ http_map_terminated /* terminated when doing resolving */
+};
+
+struct map_periodic_cbdata {
+ struct rspamd_map *map;
+ struct map_cb_data cbdata;
+ ev_timer ev;
+ gboolean need_modify;
+ gboolean errored;
+ gboolean locked;
+ guint cur_backend;
+ ref_entry_t ref;
+};
+
+static const gchar rspamd_http_file_magic[] =
+ {'r', 'm', 'c', 'd', '2', '0', '0', '0'};
+
+struct rspamd_http_file_data {
+ guchar magic[sizeof(rspamd_http_file_magic)];
+ goffset data_off;
+ gulong mtime;
+ gulong next_check;
+ gulong etag_len;
+};
+
+struct http_callback_data {
+ struct ev_loop *event_loop;
+ struct rspamd_http_connection *conn;
+ GPtrArray *addrs;
+ rspamd_inet_addr_t *addr;
+ struct rspamd_map *map;
+ struct rspamd_map_backend *bk;
+ struct http_map_data *data;
+ struct map_periodic_cbdata *periodic;
+ struct rspamd_cryptobox_pubkey *pk;
+ struct rspamd_storage_shmem *shmem_data;
+ gsize data_len;
+ gboolean check;
+ enum rspamd_map_http_stage stage;
+ ev_tstamp timeout;
+
+ ref_entry_t ref;
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SRC_LIBUTIL_MAP_PRIVATE_H_ */
diff --git a/src/libserver/mempool_vars_internal.h b/src/libserver/mempool_vars_internal.h
new file mode 100644
index 0000000..6c95538
--- /dev/null
+++ b/src/libserver/mempool_vars_internal.h
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+#ifndef RSPAMD_MEMPOOL_VARS_INTERNAL_H
+#define RSPAMD_MEMPOOL_VARS_INTERNAL_H
+
+/* Basic rspamd mempool variables names */
+#define RSPAMD_MEMPOOL_AVG_WORDS_LEN "avg_words_len"
+#define RSPAMD_MEMPOOL_SHORT_WORDS_CNT "short_words_cnt"
+#define RSPAMD_MEMPOOL_HEADERS_HASH "headers_hash"
+#define RSPAMD_MEMPOOL_MTA_TAG "MTA-Tag"
+#define RSPAMD_MEMPOOL_MTA_NAME "MTA-Name"
+#define RSPAMD_MEMPOOL_SPF_DOMAIN "spf_domain"
+#define RSPAMD_MEMPOOL_SPF_RECORD "spf_record"
+#define RSPAMD_MEMPOOL_PRINCIPAL_RECIPIENT "principal_recipient"
+#define RSPAMD_MEMPOOL_PROFILE "profile"
+#define RSPAMD_MEMPOOL_MILTER_REPLY "milter_reply"
+#define RSPAMD_MEMPOOL_DKIM_SIGNATURE "dkim-signature"
+#define RSPAMD_MEMPOOL_DMARC_CHECKS "dmarc_checks"
+#define RSPAMD_MEMPOOL_DKIM_BH_CACHE "dkim_bh_cache"
+#define RSPAMD_MEMPOOL_DKIM_CHECK_RESULTS "dkim_results"
+#define RSPAMD_MEMPOOL_DKIM_SIGN_KEY "dkim_key"
+#define RSPAMD_MEMPOOL_DKIM_SIGN_SELECTOR "dkim_selector"
+#define RSPAMD_MEMPOOL_ARC_SIGN_KEY "arc_key"
+#define RSPAMD_MEMPOOL_ARC_SIGN_SELECTOR "arc_selector"
+#define RSPAMD_MEMPOOL_STAT_SIGNATURE "stat_signature"
+#define RSPAMD_MEMPOOL_FUZZY_RESULT "fuzzy_hashes"
+#define RSPAMD_MEMPOOL_SPAM_LEARNS "spam_learns"
+#define RSPAMD_MEMPOOL_HAM_LEARNS "ham_learns"
+#define RSPAMD_MEMPOOL_RE_MAPS_CACHE "re_maps_cache"
+#define RSPAMD_MEMPOOL_HTTP_STAT_BACKEND_RUNTIME "stat_http_runtime"
+#define RSPAMD_MEMPOOL_FUZZY_STAT "fuzzy_stat"
+
+#endif
diff --git a/src/libserver/milter.c b/src/libserver/milter.c
new file mode 100644
index 0000000..cfb7d3c
--- /dev/null
+++ b/src/libserver/milter.c
@@ -0,0 +1,2232 @@
+/*-
+ * Copyright 2017 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 "milter.h"
+#include "milter_internal.h"
+#include "email_addr.h"
+#include "addr.h"
+#include "unix-std.h"
+#include "logger.h"
+#include "ottery.h"
+#include "libserver/http/http_connection.h"
+#include "libserver/http/http_private.h"
+#include "libserver/protocol_internal.h"
+#include "libserver/cfg_file_private.h"
+#include "libmime/scan_result.h"
+#include "libserver/worker_util.h"
+#include "utlist.h"
+
+#define msg_err_milter(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "milter", priv->pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_milter(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ "milter", priv->pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_milter(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ "milter", priv->pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_milter(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_milter_log_id, "milter", priv->pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+INIT_LOG_MODULE(milter)
+
+static const struct rspamd_milter_context *milter_ctx = NULL;
+
+static gboolean rspamd_milter_handle_session(
+ struct rspamd_milter_session *session,
+ struct rspamd_milter_private *priv);
+static inline void rspamd_milter_plan_io(struct rspamd_milter_session *session,
+ struct rspamd_milter_private *priv, gshort what);
+
+static GQuark
+rspamd_milter_quark(void)
+{
+ return g_quark_from_static_string("milter");
+}
+
+static void
+rspamd_milter_obuf_free(struct rspamd_milter_outbuf *obuf)
+{
+ if (obuf) {
+ if (obuf->buf) {
+ rspamd_fstring_free(obuf->buf);
+ }
+
+ g_free(obuf);
+ }
+}
+
+#define RSPAMD_MILTER_RESET_COMMON (1 << 0)
+#define RSPAMD_MILTER_RESET_IO (1 << 1)
+#define RSPAMD_MILTER_RESET_ADDR (1 << 2)
+#define RSPAMD_MILTER_RESET_MACRO (1 << 3)
+#define RSPAMD_MILTER_RESET_ALL (RSPAMD_MILTER_RESET_COMMON | \
+ RSPAMD_MILTER_RESET_IO | \
+ RSPAMD_MILTER_RESET_ADDR | \
+ RSPAMD_MILTER_RESET_MACRO)
+#define RSPAMD_MILTER_RESET_QUIT_NC (RSPAMD_MILTER_RESET_COMMON | \
+ RSPAMD_MILTER_RESET_ADDR | \
+ RSPAMD_MILTER_RESET_MACRO)
+#define RSPAMD_MILTER_RESET_ABORT (RSPAMD_MILTER_RESET_COMMON)
+
+static void
+rspamd_milter_session_reset(struct rspamd_milter_session *session,
+ guint how)
+{
+ struct rspamd_milter_outbuf *obuf, *obuf_tmp;
+ struct rspamd_milter_private *priv = session->priv;
+ struct rspamd_email_address *cur;
+ guint i;
+
+ if (how & RSPAMD_MILTER_RESET_IO) {
+ msg_debug_milter("cleanup IO on abort");
+
+ DL_FOREACH_SAFE(priv->out_chain, obuf, obuf_tmp)
+ {
+ rspamd_milter_obuf_free(obuf);
+ }
+
+ priv->out_chain = NULL;
+
+ if (priv->parser.buf) {
+ priv->parser.buf->len = 0;
+ }
+ }
+
+ if (how & RSPAMD_MILTER_RESET_COMMON) {
+ msg_debug_milter("cleanup common data on abort");
+
+ if (session->message) {
+ session->message->len = 0;
+ msg_debug_milter("cleanup message on abort");
+ }
+
+ if (session->rcpts) {
+ PTR_ARRAY_FOREACH(session->rcpts, i, cur)
+ {
+ rspamd_email_address_free(cur);
+ }
+
+ msg_debug_milter("cleanup %d recipients on abort",
+ (gint) session->rcpts->len);
+
+ g_ptr_array_free(session->rcpts, TRUE);
+ session->rcpts = NULL;
+ }
+
+ if (session->from) {
+ msg_debug_milter("cleanup from");
+ rspamd_email_address_free(session->from);
+ session->from = NULL;
+ }
+
+ if (priv->headers) {
+ msg_debug_milter("cleanup headers");
+ gchar *k;
+ GArray *ar;
+
+ kh_foreach(priv->headers, k, ar, {
+ g_free(k);
+ g_array_free(ar, TRUE);
+ });
+
+ kh_clear(milter_headers_hash_t, priv->headers);
+ }
+
+ priv->cur_hdr = 0;
+ }
+
+ if (how & RSPAMD_MILTER_RESET_ADDR) {
+ if (session->addr) {
+ msg_debug_milter("cleanup addr");
+ rspamd_inet_address_free(session->addr);
+ session->addr = NULL;
+ }
+ if (session->hostname) {
+ msg_debug_milter("cleanup hostname");
+ session->hostname->len = 0;
+ }
+ }
+
+ if (how & RSPAMD_MILTER_RESET_MACRO) {
+ if (session->macros) {
+ msg_debug_milter("cleanup macros");
+ g_hash_table_unref(session->macros);
+ session->macros = NULL;
+ }
+ }
+}
+
+static void
+rspamd_milter_session_dtor(struct rspamd_milter_session *session)
+{
+ struct rspamd_milter_private *priv;
+
+ if (session) {
+ priv = session->priv;
+ msg_debug_milter("destroying milter session");
+
+ rspamd_ev_watcher_stop(priv->event_loop, &priv->ev);
+ rspamd_milter_session_reset(session, RSPAMD_MILTER_RESET_ALL);
+ close(priv->fd);
+
+ if (priv->parser.buf) {
+ rspamd_fstring_free(priv->parser.buf);
+ }
+
+ if (session->message) {
+ rspamd_fstring_free(session->message);
+ }
+
+ if (session->helo) {
+ rspamd_fstring_free(session->helo);
+ }
+
+ if (session->hostname) {
+ rspamd_fstring_free(session->hostname);
+ }
+
+ if (priv->headers) {
+ gchar *k;
+ GArray *ar;
+
+ kh_foreach(priv->headers, k, ar, {
+ g_free(k);
+ g_array_free(ar, TRUE);
+ });
+
+ kh_destroy(milter_headers_hash_t, priv->headers);
+ }
+
+ if (milter_ctx->sessions_cache) {
+ rspamd_worker_session_cache_remove(milter_ctx->sessions_cache,
+ session);
+ }
+
+ rspamd_mempool_delete(priv->pool);
+ g_free(priv);
+ g_free(session);
+ }
+}
+
+static void
+rspamd_milter_on_protocol_error(struct rspamd_milter_session *session,
+ struct rspamd_milter_private *priv, GError *err)
+{
+ msg_debug_milter("protocol error: %e", err);
+ priv->state = RSPAMD_MILTER_WANNA_DIE;
+ REF_RETAIN(session);
+ priv->err_cb(priv->fd, session, priv->ud, err);
+ REF_RELEASE(session);
+ g_error_free(err);
+
+ rspamd_milter_plan_io(session, priv, EV_WRITE);
+}
+
+static void
+rspamd_milter_on_protocol_ping(struct rspamd_milter_session *session,
+ struct rspamd_milter_private *priv)
+{
+ GError *err = NULL;
+ static const gchar reply[] = "HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n"
+ "Server: rspamd/2.7 (milter mode)\r\n"
+ "Content-Length: 6\r\n"
+ "Content-Type: text/plain\r\n"
+ "\r\n"
+ "pong\r\n";
+
+ if (write(priv->fd, reply, sizeof(reply)) == -1) {
+ gint serrno = errno;
+ msg_err_milter("cannot write pong reply: %s", strerror(serrno));
+ g_set_error(&err, rspamd_milter_quark(), serrno, "ping command IO error: %s",
+ strerror(serrno));
+ priv->state = RSPAMD_MILTER_WANNA_DIE;
+ REF_RETAIN(session);
+ priv->err_cb(priv->fd, session, priv->ud, err);
+ REF_RELEASE(session);
+ g_error_free(err);
+ }
+ else {
+ priv->state = RSPAMD_MILTER_PONG_AND_DIE;
+ rspamd_milter_plan_io(session, priv, EV_WRITE);
+ }
+}
+
+static gint
+rspamd_milter_http_on_url(http_parser *parser, const gchar *at, size_t length)
+{
+ GString *url = (GString *) parser->data;
+
+ g_string_append_len(url, at, length);
+
+ return 0;
+}
+
+static void
+rspamd_milter_io_handler(gint fd, gshort what, void *ud)
+{
+ struct rspamd_milter_session *session = ud;
+ struct rspamd_milter_private *priv;
+ GError *err;
+
+ priv = session->priv;
+
+ if (what == EV_TIMEOUT) {
+ msg_debug_milter("connection timed out");
+ err = g_error_new(rspamd_milter_quark(), ETIMEDOUT, "connection "
+ "timed out");
+ rspamd_milter_on_protocol_error(session, priv, err);
+ }
+ else {
+ rspamd_milter_handle_session(session, priv);
+ }
+}
+
+static inline void
+rspamd_milter_plan_io(struct rspamd_milter_session *session,
+ struct rspamd_milter_private *priv, gshort what)
+{
+ rspamd_ev_watcher_reschedule(priv->event_loop, &priv->ev, what);
+}
+
+
+#define READ_INT_32(pos, var) \
+ do { \
+ memcpy(&(var), (pos), sizeof(var)); \
+ (pos) += sizeof(var); \
+ (var) = ntohl(var); \
+ } while (0)
+#define READ_INT_16(pos, var) \
+ do { \
+ memcpy(&(var), (pos), sizeof(var)); \
+ (pos) += sizeof(var); \
+ (var) = ntohs(var); \
+ } while (0)
+
+static gboolean
+rspamd_milter_process_command(struct rspamd_milter_session *session,
+ struct rspamd_milter_private *priv)
+{
+ GError *err;
+ rspamd_fstring_t *buf;
+ const guchar *pos, *end, *zero;
+ guint cmdlen;
+ guint32 version, actions, protocol;
+
+ buf = priv->parser.buf;
+ pos = buf->str + priv->parser.cmd_start;
+ cmdlen = priv->parser.datalen;
+ end = pos + cmdlen;
+
+ switch (priv->parser.cur_cmd) {
+ case RSPAMD_MILTER_CMD_ABORT:
+ msg_debug_milter("got abort command");
+ rspamd_milter_session_reset(session, RSPAMD_MILTER_RESET_ABORT);
+ break;
+ case RSPAMD_MILTER_CMD_BODY:
+ if (!session->message) {
+ session->message = rspamd_fstring_sized_new(
+ RSPAMD_MILTER_MESSAGE_CHUNK);
+ }
+
+ msg_debug_milter("got body chunk: %d bytes", (int) cmdlen);
+ session->message = rspamd_fstring_append(session->message,
+ pos, cmdlen);
+ break;
+ case RSPAMD_MILTER_CMD_CONNECT:
+ msg_debug_milter("got connect command");
+
+ /*
+ * char hostname[]: Hostname, NUL terminated
+ * char family: Protocol family
+ * uint16 port: Port number (SMFIA_INET or SMFIA_INET6 only)
+ * char address[]: IP address (ASCII) or unix socket path, NUL terminated
+ */
+ zero = memchr(pos, '\0', cmdlen);
+
+ if (zero == NULL || zero > (end - sizeof(guint16) + 1)) {
+ err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
+ "connect command (no name)");
+ rspamd_milter_on_protocol_error(session, priv, err);
+
+ return FALSE;
+ }
+ else {
+ guchar proto;
+ guint16 port;
+ gchar ip6_str[INET6_ADDRSTRLEN + 3];
+ gsize r;
+
+ /*
+ * Important notice: Postfix do NOT use this command to pass
+ * client's info (e.g. hostname is not really here)
+ * Sendmail will pass it here
+ */
+ if (session->hostname == NULL) {
+ session->hostname = rspamd_fstring_new_init(pos, zero - pos);
+ msg_debug_milter("got hostname on connect phase: %V",
+ session->hostname);
+ }
+ else {
+ session->hostname = rspamd_fstring_assign(session->hostname,
+ pos, zero - pos);
+ msg_debug_milter("rewrote hostname on connect phase: %V",
+ session->hostname);
+ }
+
+ pos = zero + 1;
+ proto = *pos++;
+
+ if (proto == RSPAMD_MILTER_CONN_UNKNOWN) {
+ /* We have no information about host */
+ msg_debug_milter("unknown connect address");
+ }
+ else {
+ READ_INT_16(pos, port);
+
+ if (pos >= end) {
+ /* No IP somehow */
+ msg_debug_milter("unknown connect IP/socket");
+ }
+ else {
+ zero = memchr(pos, '\0', end - pos);
+
+ if (zero == NULL) {
+ err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
+ "connect command (no zero terminated IP)");
+ rspamd_milter_on_protocol_error(session, priv, err);
+
+ return FALSE;
+ }
+
+ switch (proto) {
+ case RSPAMD_MILTER_CONN_UNIX:
+ session->addr = rspamd_inet_address_new(AF_UNIX,
+ pos);
+ break;
+
+ case RSPAMD_MILTER_CONN_INET:
+ session->addr = rspamd_inet_address_new(AF_INET, NULL);
+
+ if (!rspamd_parse_inet_address_ip(pos, zero - pos,
+ session->addr)) {
+ err = g_error_new(rspamd_milter_quark(), EINVAL,
+ "invalid connect command (bad IPv4)");
+ rspamd_milter_on_protocol_error(session, priv,
+ err);
+
+ return FALSE;
+ }
+
+ rspamd_inet_address_set_port(session->addr, port);
+ break;
+
+ case RSPAMD_MILTER_CONN_INET6:
+ session->addr = rspamd_inet_address_new(AF_INET6, NULL);
+
+ if (zero - pos > sizeof("IPv6:") &&
+ rspamd_lc_cmp(pos, "IPv6:",
+ sizeof("IPv6:") - 1) == 0) {
+ /* Kill sendmail please */
+ pos += sizeof("IPv6:") - 1;
+
+ if (*pos != '[') {
+ /* Add explicit braces */
+ r = rspamd_snprintf(ip6_str, sizeof(ip6_str),
+ "[%*s]", (int) (zero - pos), pos);
+ }
+ else {
+ r = rspamd_strlcpy(ip6_str, pos, sizeof(ip6_str));
+ }
+ }
+ else {
+ r = rspamd_strlcpy(ip6_str, pos, sizeof(ip6_str));
+ }
+
+ if (!rspamd_parse_inet_address_ip(ip6_str, r,
+ session->addr)) {
+ err = g_error_new(rspamd_milter_quark(), EINVAL,
+ "invalid connect command (bad IPv6)");
+ rspamd_milter_on_protocol_error(session, priv,
+ err);
+
+ return FALSE;
+ }
+
+ rspamd_inet_address_set_port(session->addr, port);
+ break;
+
+ default:
+ err = g_error_new(rspamd_milter_quark(), EINVAL,
+ "invalid connect command (bad protocol: %c)",
+ proto);
+ rspamd_milter_on_protocol_error(session, priv,
+ err);
+
+ return FALSE;
+ }
+ }
+ }
+
+ msg_info_milter("got connection from %s",
+ rspamd_inet_address_to_string_pretty(session->addr));
+ }
+ break;
+ case RSPAMD_MILTER_CMD_MACRO:
+ msg_debug_milter("got macro command");
+ /*
+ * Format is
+ * 1 byte - command associated (we don't care about it)
+ * 0-terminated name
+ * 0-terminated value
+ * ...
+ */
+ if (session->macros == NULL) {
+ session->macros = g_hash_table_new_full(rspamd_ftok_icase_hash,
+ rspamd_ftok_icase_equal,
+ rspamd_fstring_mapped_ftok_free,
+ rspamd_fstring_mapped_ftok_free);
+ }
+
+ /* Ignore one byte */
+ pos++;
+
+ while (pos < end) {
+ zero = memchr(pos, '\0', cmdlen);
+
+ if (zero == NULL || zero >= end) {
+ err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
+ "macro command (no name)");
+ rspamd_milter_on_protocol_error(session, priv, err);
+
+ return FALSE;
+ }
+ else {
+ rspamd_fstring_t *name, *value;
+ rspamd_ftok_t *name_tok, *value_tok;
+ const guchar *zero_val;
+
+ zero_val = memchr(zero + 1, '\0', end - zero - 1);
+
+ if (zero_val != NULL && end > zero_val) {
+ name = rspamd_fstring_new_init(pos, zero - pos);
+ value = rspamd_fstring_new_init(zero + 1,
+ zero_val - zero - 1);
+ name_tok = rspamd_ftok_map(name);
+ value_tok = rspamd_ftok_map(value);
+
+ g_hash_table_replace(session->macros, name_tok, value_tok);
+ msg_debug_milter("got macro: %T -> %T",
+ name_tok, value_tok);
+
+ cmdlen -= zero_val - pos;
+ pos = zero_val + 1;
+ }
+ else {
+ err = g_error_new(rspamd_milter_quark(), EINVAL,
+ "invalid macro command (bad value)");
+ rspamd_milter_on_protocol_error(session, priv, err);
+
+ return FALSE;
+ }
+ }
+ }
+ break;
+ case RSPAMD_MILTER_CMD_BODYEOB:
+ msg_debug_milter("got eob command");
+ REF_RETAIN(session);
+ priv->fin_cb(priv->fd, session, priv->ud);
+ REF_RELEASE(session);
+ break;
+ case RSPAMD_MILTER_CMD_HELO:
+ msg_debug_milter("got helo command");
+
+ if (end > pos && *(end - 1) == '\0') {
+ if (session->helo == NULL) {
+ session->helo = rspamd_fstring_new_init(pos, cmdlen - 1);
+ }
+ else {
+ session->helo = rspamd_fstring_assign(session->helo,
+ pos, cmdlen - 1);
+ }
+ }
+ else if (end > pos) {
+ /* Should not happen */
+ if (session->helo == NULL) {
+ session->helo = rspamd_fstring_new_init(pos, cmdlen);
+ }
+ else {
+ session->helo = rspamd_fstring_assign(session->helo,
+ pos, cmdlen);
+ }
+ }
+
+ msg_debug_milter("got helo value: %V", session->helo);
+
+ break;
+ case RSPAMD_MILTER_CMD_QUIT_NC:
+ /* We need to reset session and start over */
+ msg_debug_milter("got quit_nc command");
+ rspamd_milter_session_reset(session, RSPAMD_MILTER_RESET_QUIT_NC);
+ break;
+ case RSPAMD_MILTER_CMD_HEADER:
+ msg_debug_milter("got header command");
+ if (!session->message) {
+ session->message = rspamd_fstring_sized_new(
+ RSPAMD_MILTER_MESSAGE_CHUNK);
+ }
+ zero = memchr(pos, '\0', cmdlen);
+
+ if (zero == NULL) {
+ err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
+ "header command (no name)");
+ rspamd_milter_on_protocol_error(session, priv, err);
+
+ return FALSE;
+ }
+ else {
+ if (end > zero && *(end - 1) == '\0') {
+ khiter_t k;
+ gint res;
+
+ k = kh_get(milter_headers_hash_t, priv->headers, (gchar *) pos);
+
+ if (k == kh_end(priv->headers)) {
+ GArray *ar;
+
+ k = kh_put(milter_headers_hash_t, priv->headers,
+ g_strdup(pos), &res);
+ ar = g_array_new(FALSE, FALSE, sizeof(gint));
+ g_array_append_val(ar, priv->cur_hdr);
+ kh_value(priv->headers, k) = ar;
+ }
+ else {
+ g_array_append_val(kh_value(priv->headers, k),
+ priv->cur_hdr);
+ }
+
+ rspamd_printf_fstring(&session->message, "%*s: %*s\r\n",
+ (int) (zero - pos), pos,
+ (int) (end - zero - 2), zero + 1);
+ priv->cur_hdr++;
+ }
+ else {
+ err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
+ "header command (bad value)");
+ rspamd_milter_on_protocol_error(session, priv, err);
+
+ return FALSE;
+ }
+ }
+ break;
+ case RSPAMD_MILTER_CMD_MAIL:
+ msg_debug_milter("mail command");
+
+ while (pos < end) {
+ struct rspamd_email_address *addr;
+ gchar *cpy;
+
+ zero = memchr(pos, '\0', end - pos);
+
+ if (zero && zero > pos) {
+ cpy = rspamd_mempool_alloc(priv->pool, zero - pos);
+ memcpy(cpy, pos, zero - pos);
+ msg_debug_milter("got mail: %*s", (int) (zero - pos), cpy);
+ addr = rspamd_email_address_from_smtp(cpy, zero - pos);
+
+ if (addr) {
+ session->from = addr;
+ }
+
+ /* TODO: parse esmtp arguments */
+ break;
+ }
+ else {
+ msg_debug_milter("got weird from: %*s", (int) (end - pos),
+ pos);
+ /* That actually should not happen */
+ cpy = rspamd_mempool_alloc(priv->pool, end - pos);
+ memcpy(cpy, pos, end - pos);
+ addr = rspamd_email_address_from_smtp(cpy, end - pos);
+
+ if (addr) {
+ session->from = addr;
+ }
+
+ break;
+ }
+ }
+ break;
+ case RSPAMD_MILTER_CMD_EOH:
+ msg_debug_milter("got eoh command");
+
+ if (!session->message) {
+ session->message = rspamd_fstring_sized_new(
+ RSPAMD_MILTER_MESSAGE_CHUNK);
+ }
+
+ session->message = rspamd_fstring_append(session->message,
+ "\r\n", 2);
+ break;
+ case RSPAMD_MILTER_CMD_OPTNEG:
+ if (cmdlen != sizeof(guint32) * 3) {
+ err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
+ "optneg command");
+ rspamd_milter_on_protocol_error(session, priv, err);
+
+ return FALSE;
+ }
+
+ READ_INT_32(pos, version);
+ READ_INT_32(pos, actions);
+ READ_INT_32(pos, protocol);
+
+ msg_debug_milter("optneg: version: %d, actions: %d, protocol: %d",
+ version, actions, protocol);
+
+ if (version < RSPAMD_MILTER_PROTO_VER) {
+ msg_warn_milter("MTA specifies too old protocol: %d, "
+ "aborting connection",
+ version);
+
+ err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
+ "protocol version: %d",
+ version);
+ rspamd_milter_on_protocol_error(session, priv, err);
+
+ return FALSE;
+ }
+
+ version = RSPAMD_MILTER_PROTO_VER;
+ actions |= RSPAMD_MILTER_ACTIONS_MASK;
+ protocol = RSPAMD_MILTER_FLAG_NOREPLY_MASK;
+
+ return rspamd_milter_send_action(session, RSPAMD_MILTER_OPTNEG,
+ version, actions, protocol);
+ break;
+ case RSPAMD_MILTER_CMD_QUIT:
+ if (priv->out_chain) {
+ msg_debug_milter("quit command, refcount: %d, "
+ "some output buffers left - draining",
+ session->ref.refcount);
+
+ priv->state = RSPAMD_MILTER_WRITE_AND_DIE;
+ }
+ else {
+ msg_debug_milter("quit command, refcount: %d",
+ session->ref.refcount);
+
+ priv->state = RSPAMD_MILTER_WANNA_DIE;
+ REF_RETAIN(session);
+ priv->fin_cb(priv->fd, session, priv->ud);
+ REF_RELEASE(session);
+ return FALSE;
+ }
+ break;
+ case RSPAMD_MILTER_CMD_RCPT:
+ msg_debug_milter("rcpt command");
+
+ while (pos < end) {
+ struct rspamd_email_address *addr;
+ gchar *cpy;
+
+ zero = memchr(pos, '\0', end - pos);
+
+ if (zero && zero > pos) {
+ cpy = rspamd_mempool_alloc(priv->pool, end - pos);
+ memcpy(cpy, pos, end - pos);
+
+ msg_debug_milter("got rcpt: %*s", (int) (zero - pos), cpy);
+ addr = rspamd_email_address_from_smtp(cpy, zero - pos);
+
+ if (addr) {
+ if (!session->rcpts) {
+ session->rcpts = g_ptr_array_sized_new(1);
+ }
+
+ g_ptr_array_add(session->rcpts, addr);
+ }
+
+ pos = zero + 1;
+ }
+ else {
+ cpy = rspamd_mempool_alloc(priv->pool, end - pos);
+ memcpy(cpy, pos, end - pos);
+
+ msg_debug_milter("got weird rcpt: %*s", (int) (end - pos),
+ pos);
+ /* That actually should not happen */
+ addr = rspamd_email_address_from_smtp(cpy, end - pos);
+
+ if (addr) {
+ if (!session->rcpts) {
+ session->rcpts = g_ptr_array_sized_new(1);
+ }
+
+ g_ptr_array_add(session->rcpts, addr);
+ }
+
+ break;
+ }
+ }
+ break;
+ case RSPAMD_MILTER_CMD_DATA:
+ if (!session->message) {
+ session->message = rspamd_fstring_sized_new(
+ RSPAMD_MILTER_MESSAGE_CHUNK);
+ }
+ msg_debug_milter("got data command");
+ /* We do not need reply as specified */
+ break;
+ default:
+ msg_debug_milter("got bad command: %c", priv->parser.cur_cmd);
+ break;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_milter_is_valid_cmd(guchar c)
+{
+ switch (c) {
+ case RSPAMD_MILTER_CMD_ABORT:
+ case RSPAMD_MILTER_CMD_BODY:
+ case RSPAMD_MILTER_CMD_CONNECT:
+ case RSPAMD_MILTER_CMD_MACRO:
+ case RSPAMD_MILTER_CMD_BODYEOB:
+ case RSPAMD_MILTER_CMD_HELO:
+ case RSPAMD_MILTER_CMD_QUIT_NC:
+ case RSPAMD_MILTER_CMD_HEADER:
+ case RSPAMD_MILTER_CMD_MAIL:
+ case RSPAMD_MILTER_CMD_EOH:
+ case RSPAMD_MILTER_CMD_OPTNEG:
+ case RSPAMD_MILTER_CMD_QUIT:
+ case RSPAMD_MILTER_CMD_RCPT:
+ case RSPAMD_MILTER_CMD_DATA:
+ case RSPAMD_MILTER_CMD_UNKNOWN:
+ return TRUE;
+ default:
+ break;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+rspamd_milter_consume_input(struct rspamd_milter_session *session,
+ struct rspamd_milter_private *priv)
+{
+ const guchar *p, *end;
+ GError *err;
+
+ p = priv->parser.buf->str + priv->parser.pos;
+ end = priv->parser.buf->str + priv->parser.buf->len;
+
+ while (p < end) {
+ msg_debug_milter("offset: %d, state: %d",
+ (gint) (p - (const guchar *) priv->parser.buf->str),
+ priv->parser.state);
+
+ switch (priv->parser.state) {
+ case st_len_1:
+ /* The first length byte in big endian order */
+ priv->parser.datalen = 0;
+ priv->parser.datalen |= ((gsize) *p) << 24;
+ priv->parser.state = st_len_2;
+ p++;
+ break;
+ case st_len_2:
+ /* The second length byte in big endian order */
+ priv->parser.datalen |= ((gsize) *p) << 16;
+ priv->parser.state = st_len_3;
+ p++;
+ break;
+ case st_len_3:
+ /* The third length byte in big endian order */
+ priv->parser.datalen |= ((gsize) *p) << 8;
+ priv->parser.state = st_len_4;
+ p++;
+ break;
+ case st_len_4:
+ /* The fourth length byte in big endian order */
+ priv->parser.datalen |= ((gsize) *p);
+ priv->parser.state = st_read_cmd;
+ p++;
+ break;
+ case st_read_cmd:
+ priv->parser.cur_cmd = *p;
+ priv->parser.state = st_read_data;
+
+ if (priv->parser.datalen < 1) {
+ err = g_error_new(rspamd_milter_quark(), EINVAL,
+ "Command length is too short");
+ rspamd_milter_on_protocol_error(session, priv, err);
+
+ return FALSE;
+ }
+ else {
+ /* Eat command itself */
+ priv->parser.datalen--;
+ }
+
+ p++;
+ priv->parser.cmd_start = p - (const guchar *) priv->parser.buf->str;
+ break;
+ case st_read_data:
+ /* We might need some more data in buffer for further steps */
+ if (priv->parser.datalen >
+ RSPAMD_MILTER_MESSAGE_CHUNK * 2) {
+ /* Check if we have HTTP input instead of milter */
+ if (priv->parser.buf->len > sizeof("GET") &&
+ memcmp(priv->parser.buf->str, "GET", 3) == 0) {
+ struct http_parser http_parser;
+ struct http_parser_settings http_callbacks;
+ GString *url = g_string_new(NULL);
+
+ /* Hack, hack, hack */
+ /*
+ * This code is assumed to read `/ping` command and
+ * handle it to monitor port's availability since
+ * milter protocol is stupid and does not allow to do that
+ * This code also assumes that HTTP request can be read
+ * as as single data chunk which is not true in some cases
+ * In general, don't use it for anything but ping checks
+ */
+ memset(&http_callbacks, 0, sizeof(http_callbacks));
+ http_parser.data = url;
+ http_parser_init(&http_parser, HTTP_REQUEST);
+ http_callbacks.on_url = rspamd_milter_http_on_url;
+ http_parser_execute(&http_parser, &http_callbacks,
+ priv->parser.buf->str, priv->parser.buf->len);
+
+ if (url->len == sizeof("/ping") - 1 &&
+ rspamd_lc_cmp(url->str, "/ping", url->len) == 0) {
+ rspamd_milter_on_protocol_ping(session, priv);
+ g_string_free(url, TRUE);
+
+ return TRUE;
+ }
+ else {
+ err = g_error_new(rspamd_milter_quark(), EINVAL,
+ "HTTP GET request is not supported in milter mode, url: %s",
+ url->str);
+ }
+
+ g_string_free(url, TRUE);
+ }
+ else if (priv->parser.buf->len > sizeof("POST") &&
+ memcmp(priv->parser.buf->str, "POST", 4) == 0) {
+ err = g_error_new(rspamd_milter_quark(), EINVAL,
+ "HTTP POST request is not supported in milter mode");
+ }
+ else {
+ err = g_error_new(rspamd_milter_quark(), E2BIG,
+ "Command length is too big: %zd",
+ priv->parser.datalen);
+ }
+
+ rspamd_milter_on_protocol_error(session, priv, err);
+
+ return FALSE;
+ }
+ if (!rspamd_milter_is_valid_cmd(priv->parser.cur_cmd)) {
+ err = g_error_new(rspamd_milter_quark(), E2BIG,
+ "Unvalid command: %c",
+ priv->parser.cur_cmd);
+ rspamd_milter_on_protocol_error(session, priv, err);
+
+ return FALSE;
+ }
+ if (priv->parser.buf->allocated < priv->parser.datalen) {
+ priv->parser.pos = p - (const guchar *) priv->parser.buf->str;
+ priv->parser.buf = rspamd_fstring_grow(priv->parser.buf,
+ priv->parser.buf->len + priv->parser.datalen);
+ /* This can realloc buffer */
+ rspamd_milter_plan_io(session, priv, EV_READ);
+ goto end;
+ }
+ else {
+ /* We may have the full command available */
+ if (p + priv->parser.datalen <= end) {
+ /* We can process command */
+ if (!rspamd_milter_process_command(session, priv)) {
+ return FALSE;
+ }
+
+ p += priv->parser.datalen;
+ priv->parser.state = st_len_1;
+ priv->parser.cur_cmd = '\0';
+ priv->parser.cmd_start = 0;
+ }
+ else {
+ /* Need to read more */
+ priv->parser.pos = p - (const guchar *) priv->parser.buf->str;
+ rspamd_milter_plan_io(session, priv, EV_READ);
+ goto end;
+ }
+ }
+ break;
+ }
+ }
+
+ /* Leftover */
+ switch (priv->parser.state) {
+ case st_read_data:
+ if (p + priv->parser.datalen <= end) {
+ if (!rspamd_milter_process_command(session, priv)) {
+ return FALSE;
+ }
+
+ priv->parser.state = st_len_1;
+ priv->parser.cur_cmd = '\0';
+ priv->parser.cmd_start = 0;
+ }
+ break;
+ default:
+ /* No need to do anything */
+ break;
+ }
+
+ if (p == end) {
+ priv->parser.buf->len = 0;
+ priv->parser.pos = 0;
+ priv->parser.cmd_start = 0;
+ }
+
+ if (priv->out_chain) {
+ rspamd_milter_plan_io(session, priv, EV_READ | EV_WRITE);
+ }
+ else {
+ rspamd_milter_plan_io(session, priv, EV_READ);
+ }
+end:
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_milter_handle_session(struct rspamd_milter_session *session,
+ struct rspamd_milter_private *priv)
+{
+ struct rspamd_milter_outbuf *obuf, *obuf_tmp;
+ gssize r, to_write;
+ GError *err;
+
+ g_assert(session != NULL);
+
+ switch (priv->state) {
+ case RSPAMD_MILTER_READ_MORE:
+ if (priv->parser.buf->len >= priv->parser.buf->allocated) {
+ priv->parser.buf = rspamd_fstring_grow(priv->parser.buf,
+ priv->parser.buf->len * 2);
+ }
+
+ r = read(priv->fd, priv->parser.buf->str + priv->parser.buf->len,
+ priv->parser.buf->allocated - priv->parser.buf->len);
+
+ msg_debug_milter("read %z bytes, %z remain, %z allocated",
+ r, priv->parser.buf->len, priv->parser.buf->allocated);
+
+ if (r == -1) {
+ if (errno == EAGAIN || errno == EINTR) {
+ rspamd_milter_plan_io(session, priv, EV_READ);
+
+ return TRUE;
+ }
+ else {
+ /* Fatal IO error */
+ err = g_error_new(rspamd_milter_quark(), errno,
+ "IO read error: %s", strerror(errno));
+ REF_RETAIN(session);
+ priv->err_cb(priv->fd, session, priv->ud, err);
+ REF_RELEASE(session);
+ g_error_free(err);
+
+ REF_RELEASE(session);
+
+ return FALSE;
+ }
+ }
+ else if (r == 0) {
+ err = g_error_new(rspamd_milter_quark(), ECONNRESET,
+ "Unexpected EOF");
+ REF_RETAIN(session);
+ priv->err_cb(priv->fd, session, priv->ud, err);
+ REF_RELEASE(session);
+ g_error_free(err);
+
+ REF_RELEASE(session);
+
+ return FALSE;
+ }
+ else {
+ priv->parser.buf->len += r;
+
+ return rspamd_milter_consume_input(session, priv);
+ }
+
+ break;
+ case RSPAMD_MILTER_WRITE_REPLY:
+ case RSPAMD_MILTER_WRITE_AND_DIE:
+ if (priv->out_chain == NULL) {
+ if (priv->state == RSPAMD_MILTER_WRITE_AND_DIE) {
+ /* Finished writing, let's die finally */
+ msg_debug_milter("output drained, terminating, refcount: %d",
+ session->ref.refcount);
+
+ /* Session should be destroyed by fin_cb... */
+ REF_RETAIN(session);
+ priv->fin_cb(priv->fd, session, priv->ud);
+ REF_RELEASE(session);
+
+ return FALSE;
+ }
+ else {
+ /* We have written everything, so we can read something */
+ priv->state = RSPAMD_MILTER_READ_MORE;
+ rspamd_milter_plan_io(session, priv, EV_READ);
+ }
+ }
+ else {
+ DL_FOREACH_SAFE(priv->out_chain, obuf, obuf_tmp)
+ {
+ to_write = obuf->buf->len - obuf->pos;
+
+ g_assert(to_write > 0);
+
+ r = write(priv->fd, obuf->buf->str + obuf->pos, to_write);
+
+ if (r == -1) {
+ if (errno == EAGAIN || errno == EINTR) {
+ rspamd_milter_plan_io(session, priv, EV_WRITE);
+ }
+ else {
+ /* Fatal IO error */
+ err = g_error_new(rspamd_milter_quark(), errno,
+ "IO write error: %s", strerror(errno));
+ REF_RETAIN(session);
+ priv->err_cb(priv->fd, session, priv->ud, err);
+ REF_RELEASE(session);
+ g_error_free(err);
+
+ REF_RELEASE(session);
+
+ return FALSE;
+ }
+ }
+ else if (r == 0) {
+ err = g_error_new(rspamd_milter_quark(), ECONNRESET,
+ "Unexpected EOF");
+ REF_RETAIN(session);
+ priv->err_cb(priv->fd, session, priv->ud, err);
+ REF_RELEASE(session);
+ g_error_free(err);
+
+ REF_RELEASE(session);
+
+ return FALSE;
+ }
+ else {
+ if (r == to_write) {
+ /* We have done with this buf */
+ DL_DELETE(priv->out_chain, obuf);
+ rspamd_milter_obuf_free(obuf);
+ }
+ else {
+ /* We need to plan another write */
+ obuf->pos += r;
+ rspamd_milter_plan_io(session, priv, EV_WRITE);
+
+ return TRUE;
+ }
+ }
+ }
+
+ /* Here we have written everything, so we can plan reading */
+ priv->state = RSPAMD_MILTER_READ_MORE;
+ rspamd_milter_plan_io(session, priv, EV_READ);
+ }
+ break;
+ case RSPAMD_MILTER_WANNA_DIE:
+ /* We are here after processing everything, so release session */
+ REF_RELEASE(session);
+ return FALSE;
+ break;
+ case RSPAMD_MILTER_PONG_AND_DIE:
+ err = g_error_new(rspamd_milter_quark(), 0,
+ "ping command");
+ REF_RETAIN(session);
+ priv->err_cb(priv->fd, session, priv->ud, err);
+ REF_RELEASE(session);
+ g_error_free(err);
+ REF_RELEASE(session);
+ return FALSE;
+ break;
+ }
+
+ return TRUE;
+}
+
+
+gboolean
+rspamd_milter_handle_socket(gint fd, ev_tstamp timeout,
+ rspamd_mempool_t *pool,
+ struct ev_loop *ev_base, rspamd_milter_finish finish_cb,
+ rspamd_milter_error error_cb, void *ud)
+{
+ struct rspamd_milter_session *session;
+ struct rspamd_milter_private *priv;
+ gint nfd = dup(fd);
+
+ if (nfd == -1) {
+ GError *err = g_error_new(rspamd_milter_quark(), errno,
+ "dup failed: %s", strerror(errno));
+ error_cb(fd, NULL, ud, err);
+
+ return FALSE;
+ }
+
+ g_assert(finish_cb != NULL);
+ g_assert(error_cb != NULL);
+ g_assert(milter_ctx != NULL);
+
+ session = g_malloc0(sizeof(*session));
+ priv = g_malloc0(sizeof(*priv));
+ priv->fd = nfd;
+ priv->ud = ud;
+ priv->fin_cb = finish_cb;
+ priv->err_cb = error_cb;
+ priv->parser.state = st_len_1;
+ priv->parser.buf = rspamd_fstring_sized_new(RSPAMD_MILTER_MESSAGE_CHUNK + 5);
+ priv->event_loop = ev_base;
+ priv->state = RSPAMD_MILTER_READ_MORE;
+ priv->pool = rspamd_mempool_new(rspamd_mempool_suggest_size(), "milter", 0);
+ priv->discard_on_reject = milter_ctx->discard_on_reject;
+ priv->quarantine_on_reject = milter_ctx->quarantine_on_reject;
+ priv->ev.timeout = timeout;
+
+ rspamd_ev_watcher_init(&priv->ev, priv->fd, EV_READ | EV_WRITE,
+ rspamd_milter_io_handler, session);
+
+ if (pool) {
+ /* Copy tag */
+ memcpy(priv->pool->tag.uid, pool->tag.uid, sizeof(pool->tag.uid));
+ }
+
+ priv->headers = kh_init(milter_headers_hash_t);
+ kh_resize(milter_headers_hash_t, priv->headers, 32);
+
+ session->priv = priv;
+ REF_INIT_RETAIN(session, rspamd_milter_session_dtor);
+
+ if (milter_ctx->sessions_cache) {
+ rspamd_worker_session_cache_add(milter_ctx->sessions_cache,
+ priv->pool->tag.uid, &session->ref.refcount, session);
+ }
+
+ return rspamd_milter_handle_session(session, priv);
+}
+
+gboolean
+rspamd_milter_set_reply(struct rspamd_milter_session *session,
+ rspamd_fstring_t *rcode,
+ rspamd_fstring_t *xcode,
+ rspamd_fstring_t *reply)
+{
+ GString *buf;
+ gboolean ret;
+
+ buf = g_string_sized_new(xcode->len + rcode->len + reply->len + 2);
+ rspamd_printf_gstring(buf, "%V %V %V", rcode, xcode, reply);
+ ret = rspamd_milter_send_action(session, RSPAMD_MILTER_REPLYCODE,
+ buf);
+ g_string_free(buf, TRUE);
+
+ return ret;
+}
+
+#define SET_COMMAND(cmd, sz, reply, pos) \
+ do { \
+ guint32 _len; \
+ _len = (sz) + 1; \
+ (reply) = rspamd_fstring_sized_new(sizeof(_len) + _len); \
+ (reply)->len = sizeof(_len) + _len; \
+ _len = htonl(_len); \
+ memcpy((reply)->str, &_len, sizeof(_len)); \
+ (reply)->str[sizeof(_len)] = (cmd); \
+ (pos) = (guchar *) (reply)->str + sizeof(_len) + 1; \
+ } while (0)
+
+gboolean
+rspamd_milter_send_action(struct rspamd_milter_session *session,
+ enum rspamd_milter_reply act, ...)
+{
+ guint32 ver, actions, protocol, idx;
+ va_list ap;
+ guchar cmd, *pos;
+ rspamd_fstring_t *reply = NULL;
+ gsize len;
+ GString *name, *value;
+ const char *reason, *body_str;
+ struct rspamd_milter_outbuf *obuf;
+ struct rspamd_milter_private *priv = session->priv;
+
+ va_start(ap, act);
+ cmd = act;
+
+ switch (act) {
+ case RSPAMD_MILTER_ACCEPT:
+ case RSPAMD_MILTER_CONTINUE:
+ case RSPAMD_MILTER_DISCARD:
+ case RSPAMD_MILTER_PROGRESS:
+ case RSPAMD_MILTER_REJECT:
+ case RSPAMD_MILTER_TEMPFAIL:
+ /* No additional arguments */
+ msg_debug_milter("send %c command", cmd);
+ SET_COMMAND(cmd, 0, reply, pos);
+ break;
+ case RSPAMD_MILTER_QUARANTINE:
+ reason = va_arg(ap, const char *);
+
+ if (reason == NULL) {
+ reason = "";
+ }
+
+ len = strlen(reason);
+ msg_debug_milter("send quarantine action %s", reason);
+ SET_COMMAND(cmd, len + 1, reply, pos);
+ memcpy(pos, reason, len + 1);
+ break;
+ case RSPAMD_MILTER_ADDHEADER:
+ name = va_arg(ap, GString *);
+ value = va_arg(ap, GString *);
+
+ /* Name and value must be zero terminated */
+ msg_debug_milter("add header command - \"%v\"=\"%v\"", name, value);
+ SET_COMMAND(cmd, name->len + value->len + 2, reply, pos);
+ memcpy(pos, name->str, name->len + 1);
+ pos += name->len + 1;
+ memcpy(pos, value->str, value->len + 1);
+ break;
+ case RSPAMD_MILTER_CHGHEADER:
+ case RSPAMD_MILTER_INSHEADER:
+ idx = va_arg(ap, guint32);
+ name = va_arg(ap, GString *);
+ value = va_arg(ap, GString *);
+
+ msg_debug_milter("change/insert header command pos = %d- \"%v\"=\"%v\"",
+ idx, name, value);
+ /* Name and value must be zero terminated */
+ SET_COMMAND(cmd, name->len + value->len + 2 + sizeof(guint32),
+ reply, pos);
+ idx = htonl(idx);
+ memcpy(pos, &idx, sizeof(idx));
+ pos += sizeof(idx);
+ memcpy(pos, name->str, name->len + 1);
+ pos += name->len + 1;
+ memcpy(pos, value->str, value->len + 1);
+ break;
+ case RSPAMD_MILTER_REPLBODY:
+ len = va_arg(ap, gsize);
+ body_str = va_arg(ap, const char *);
+ msg_debug_milter("want to change body; size = %uz",
+ len);
+ SET_COMMAND(cmd, len, reply, pos);
+ memcpy(pos, body_str, len);
+ break;
+ case RSPAMD_MILTER_REPLYCODE:
+ case RSPAMD_MILTER_ADDRCPT:
+ case RSPAMD_MILTER_DELRCPT:
+ case RSPAMD_MILTER_CHGFROM:
+ /* Single GString * argument */
+ value = va_arg(ap, GString *);
+ msg_debug_milter("command %c; value=%v", cmd, value);
+ SET_COMMAND(cmd, value->len + 1, reply, pos);
+ memcpy(pos, value->str, value->len + 1);
+ break;
+ case RSPAMD_MILTER_OPTNEG:
+ ver = va_arg(ap, guint32);
+ actions = va_arg(ap, guint32);
+ protocol = va_arg(ap, guint32);
+
+ msg_debug_milter("optneg reply: ver=%d, actions=%d, protocol=%d",
+ ver, actions, protocol);
+ ver = htonl(ver);
+ actions = htonl(actions);
+ protocol = htonl(protocol);
+ SET_COMMAND(cmd, sizeof(guint32) * 3, reply, pos);
+ memcpy(pos, &ver, sizeof(ver));
+ pos += sizeof(ver);
+ memcpy(pos, &actions, sizeof(actions));
+ pos += sizeof(actions);
+ memcpy(pos, &protocol, sizeof(protocol));
+ break;
+ default:
+ msg_err_milter("invalid command: %c", cmd);
+ break;
+ }
+
+ va_end(ap);
+
+ if (reply) {
+ obuf = g_malloc(sizeof(*obuf));
+ obuf->buf = reply;
+ obuf->pos = 0;
+ DL_APPEND(priv->out_chain, obuf);
+ priv->state = RSPAMD_MILTER_WRITE_REPLY;
+ rspamd_milter_plan_io(session, priv, EV_WRITE);
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+gboolean
+rspamd_milter_add_header(struct rspamd_milter_session *session,
+ GString *name, GString *value)
+{
+ return rspamd_milter_send_action(session, RSPAMD_MILTER_ADDHEADER,
+ name, value);
+}
+
+gboolean
+rspamd_milter_del_header(struct rspamd_milter_session *session,
+ GString *name)
+{
+ GString value;
+ guint32 idx = 1;
+
+ value.str = (gchar *) "";
+ value.len = 0;
+
+ return rspamd_milter_send_action(session, RSPAMD_MILTER_CHGHEADER,
+ idx, name, &value);
+}
+
+void rspamd_milter_session_unref(struct rspamd_milter_session *session)
+{
+ REF_RELEASE(session);
+}
+
+struct rspamd_milter_session *
+rspamd_milter_session_ref(struct rspamd_milter_session *session)
+{
+ REF_RETAIN(session);
+
+ return session;
+}
+
+#define IF_MACRO(lit) \
+ RSPAMD_FTOK_ASSIGN(&srch, (lit)); \
+ found = g_hash_table_lookup(session->macros, &srch); \
+ if (found)
+
+static void
+rspamd_milter_macro_http(struct rspamd_milter_session *session,
+ struct rspamd_http_message *msg)
+{
+ rspamd_ftok_t *found, srch;
+ struct rspamd_milter_private *priv = session->priv;
+
+ /*
+ * We assume postfix macros here, sendmail ones might be slightly
+ * different
+ */
+
+ if (!session->macros) {
+ return;
+ }
+
+ IF_MACRO("{i}")
+ {
+ rspamd_http_message_add_header_len(msg, QUEUE_ID_HEADER,
+ found->begin, found->len);
+ }
+ else
+ {
+ IF_MACRO("i")
+ {
+ rspamd_http_message_add_header_len(msg, QUEUE_ID_HEADER,
+ found->begin, found->len);
+ }
+ }
+
+ IF_MACRO("{v}")
+ {
+ rspamd_http_message_add_header_len(msg, USER_AGENT_HEADER,
+ found->begin, found->len);
+ }
+ else
+ {
+ IF_MACRO("v")
+ {
+ rspamd_http_message_add_header_len(msg, USER_AGENT_HEADER,
+ found->begin, found->len);
+ }
+ }
+
+ IF_MACRO("{cipher}")
+ {
+ rspamd_http_message_add_header_len(msg, TLS_CIPHER_HEADER,
+ found->begin, found->len);
+ }
+
+ IF_MACRO("{tls_version}")
+ {
+ rspamd_http_message_add_header_len(msg, TLS_VERSION_HEADER,
+ found->begin, found->len);
+ }
+
+ IF_MACRO("{auth_authen}")
+ {
+ rspamd_http_message_add_header_len(msg, USER_HEADER,
+ found->begin, found->len);
+ }
+
+ IF_MACRO("{rcpt_mailer}")
+ {
+ rspamd_http_message_add_header_len(msg, MAILER_HEADER,
+ found->begin, found->len);
+ }
+
+ if (milter_ctx->client_ca_name) {
+ IF_MACRO("{cert_issuer}")
+ {
+ rspamd_http_message_add_header_len(msg, CERT_ISSUER_HEADER,
+ found->begin, found->len);
+
+ if (found->len == strlen(milter_ctx->client_ca_name) &&
+ rspamd_cryptobox_memcmp(found->begin,
+ milter_ctx->client_ca_name, found->len) == 0) {
+ msg_debug_milter("process certificate issued by %T", found);
+ IF_MACRO("{cert_subject}")
+ {
+ rspamd_http_message_add_header_len(msg, USER_HEADER,
+ found->begin, found->len);
+ }
+ }
+ else {
+ msg_debug_milter("skip certificate issued by %T", found);
+ }
+ }
+ }
+ else {
+ IF_MACRO("{cert_issuer}")
+ {
+ rspamd_http_message_add_header_len(msg, CERT_ISSUER_HEADER,
+ found->begin, found->len);
+ }
+ }
+
+ if (!session->hostname || session->hostname->len == 0) {
+ IF_MACRO("{client_name}")
+ {
+ if (!(found->len == sizeof("unknown") - 1 &&
+ memcmp(found->begin, "unknown",
+ sizeof("unknown") - 1) == 0)) {
+ rspamd_http_message_add_header_len(msg, HOSTNAME_HEADER,
+ found->begin, found->len);
+ }
+ else {
+ msg_debug_milter("skip unknown hostname from being added");
+ }
+ }
+ }
+
+ IF_MACRO("{daemon_name}")
+ {
+ /* Postfix style */
+ rspamd_http_message_add_header_len(msg, MTA_NAME_HEADER,
+ found->begin, found->len);
+ }
+ else
+ {
+ /* Sendmail style */
+ IF_MACRO("{j}")
+ {
+ rspamd_http_message_add_header_len(msg, MTA_NAME_HEADER,
+ found->begin, found->len);
+ }
+ else
+ {
+ IF_MACRO("j")
+ {
+ rspamd_http_message_add_header_len(msg, MTA_NAME_HEADER,
+ found->begin, found->len);
+ }
+ }
+ }
+}
+
+struct rspamd_http_message *
+rspamd_milter_to_http(struct rspamd_milter_session *session)
+{
+ struct rspamd_http_message *msg;
+ guint i;
+ struct rspamd_email_address *rcpt;
+ struct rspamd_milter_private *priv = session->priv;
+
+ g_assert(session != NULL);
+
+ msg = rspamd_http_new_message(HTTP_REQUEST);
+
+ msg->url = rspamd_fstring_assign(msg->url, "/" MSG_CMD_CHECK_V2,
+ sizeof("/" MSG_CMD_CHECK_V2) - 1);
+
+ if (session->message) {
+ rspamd_http_message_set_body_from_fstring_steal(msg, session->message);
+ session->message = NULL;
+ }
+
+ if (session->hostname && RSPAMD_FSTRING_LEN(session->hostname) > 0) {
+ if (!(session->hostname->len == sizeof("unknown") - 1 &&
+ memcmp(RSPAMD_FSTRING_DATA(session->hostname), "unknown",
+ sizeof("unknown") - 1) == 0)) {
+ rspamd_http_message_add_header_fstr(msg, HOSTNAME_HEADER,
+ session->hostname);
+ }
+ else {
+ msg_debug_milter("skip unknown hostname from being added");
+ }
+ }
+
+ if (session->helo && session->helo->len > 0) {
+ rspamd_http_message_add_header_fstr(msg, HELO_HEADER,
+ session->helo);
+ }
+
+ if (session->from) {
+ rspamd_http_message_add_header_len(msg, FROM_HEADER,
+ session->from->raw, session->from->raw_len);
+ }
+
+ if (session->rcpts) {
+ PTR_ARRAY_FOREACH(session->rcpts, i, rcpt)
+ {
+ rspamd_http_message_add_header_len(msg, RCPT_HEADER,
+ rcpt->raw, rcpt->raw_len);
+ }
+ }
+
+ if (session->addr) {
+ if (rspamd_inet_address_get_af(session->addr) != AF_UNIX) {
+ rspamd_http_message_add_header(msg, IP_ADDR_HEADER,
+ rspamd_inet_address_to_string_pretty(session->addr));
+ }
+ else {
+ rspamd_http_message_add_header(msg, IP_ADDR_HEADER,
+ rspamd_inet_address_to_string(session->addr));
+ }
+ }
+
+ rspamd_milter_macro_http(session, msg);
+ rspamd_http_message_add_header(msg, FLAGS_HEADER, "milter,body_block");
+
+ return msg;
+}
+
+void *
+rspamd_milter_update_userdata(struct rspamd_milter_session *session,
+ void *ud)
+{
+ struct rspamd_milter_private *priv = session->priv;
+ void *prev_ud;
+
+ prev_ud = priv->ud;
+ priv->ud = ud;
+
+ return prev_ud;
+}
+
+static void
+rspamd_milter_remove_header_safe(struct rspamd_milter_session *session,
+ const gchar *key, gint nhdr)
+{
+ gint i;
+ GString *hname, *hvalue;
+ struct rspamd_milter_private *priv = session->priv;
+ khiter_t k;
+ GArray *ar;
+
+ k = kh_get(milter_headers_hash_t, priv->headers, (char *) key);
+
+ if (k != kh_end(priv->headers)) {
+ ar = kh_val(priv->headers, k);
+
+ hname = g_string_new(key);
+ hvalue = g_string_new("");
+
+ if (nhdr > 0) {
+ if (ar->len >= nhdr) {
+ rspamd_milter_send_action(session,
+ RSPAMD_MILTER_CHGHEADER,
+ nhdr, hname, hvalue);
+ priv->cur_hdr--;
+ }
+ }
+ else if (nhdr == 0) {
+ /* We need to clear all headers */
+ for (i = ar->len; i > 0; i--) {
+ rspamd_milter_send_action(session,
+ RSPAMD_MILTER_CHGHEADER,
+ i, hname, hvalue);
+ priv->cur_hdr--;
+ }
+ }
+ else {
+ /* Remove from the end */
+ if (nhdr >= -(ar->len)) {
+ rspamd_milter_send_action(session,
+ RSPAMD_MILTER_CHGHEADER,
+ ar->len + nhdr + 1, hname, hvalue);
+ priv->cur_hdr--;
+ }
+ }
+
+ g_string_free(hname, TRUE);
+ g_string_free(hvalue, TRUE);
+
+ if (priv->cur_hdr < 0) {
+ msg_err_milter("negative header count after removing %s", key);
+ priv->cur_hdr = 0;
+ }
+ }
+}
+
+static void
+rspamd_milter_extract_single_header(struct rspamd_milter_session *session,
+ const gchar *hdr, const ucl_object_t *obj)
+{
+ GString *hname, *hvalue;
+ struct rspamd_milter_private *priv = session->priv;
+ gint idx = -1;
+ const ucl_object_t *val;
+
+ val = ucl_object_lookup(obj, "value");
+
+ if (val && ucl_object_type(val) == UCL_STRING) {
+ const ucl_object_t *idx_obj;
+ gboolean has_idx = FALSE;
+
+ idx_obj = ucl_object_lookup_any(obj, "order",
+ "index", NULL);
+
+ if (idx_obj && (ucl_object_type(idx_obj) == UCL_INT || ucl_object_type(idx_obj) == UCL_FLOAT)) {
+ idx = ucl_object_toint(idx_obj);
+ has_idx = TRUE;
+ }
+
+ hname = g_string_new(hdr);
+ hvalue = g_string_new(ucl_object_tostring(val));
+
+ if (has_idx) {
+ if (idx >= 0) {
+ rspamd_milter_send_action(session,
+ RSPAMD_MILTER_INSHEADER,
+ idx,
+ hname, hvalue);
+ }
+ else {
+ /* Calculate negative offset */
+
+ if (idx == -1) {
+ rspamd_milter_send_action(session,
+ RSPAMD_MILTER_ADDHEADER,
+ hname, hvalue);
+ }
+ else if (-idx <= priv->cur_hdr) {
+ /*
+ * Note: We should account MTA's own "Received:" field
+ * which wasn't passed by Milter's header command.
+ */
+ rspamd_milter_send_action(session,
+ RSPAMD_MILTER_INSHEADER,
+ priv->cur_hdr + idx + 2,
+ hname, hvalue);
+ }
+ else {
+ rspamd_milter_send_action(session,
+ RSPAMD_MILTER_INSHEADER,
+ 0,
+ hname, hvalue);
+ }
+ }
+ }
+ else {
+ rspamd_milter_send_action(session,
+ RSPAMD_MILTER_ADDHEADER,
+ hname, hvalue);
+ }
+
+ priv->cur_hdr++;
+
+ g_string_free(hname, TRUE);
+ g_string_free(hvalue, TRUE);
+ }
+}
+
+/*
+ * Returns `TRUE` if action has been processed internally by this function
+ */
+static gboolean
+rspamd_milter_process_milter_block(struct rspamd_milter_session *session,
+ const ucl_object_t *obj, struct rspamd_action *action)
+{
+ const ucl_object_t *elt, *cur;
+ ucl_object_iter_t it;
+ struct rspamd_milter_private *priv = session->priv;
+ GString *hname, *hvalue;
+
+ if (obj && ucl_object_type(obj) == UCL_OBJECT) {
+ elt = ucl_object_lookup(obj, "remove_headers");
+ /*
+ * remove_headers: {"name": 1, ... }
+ * where number is the header's position starting from '1'
+ */
+ if (elt && ucl_object_type(elt) == UCL_OBJECT) {
+ it = NULL;
+
+ while ((cur = ucl_object_iterate(elt, &it, true)) != NULL) {
+ if (ucl_object_type(cur) == UCL_INT) {
+ rspamd_milter_remove_header_safe(session,
+ ucl_object_key(cur),
+ ucl_object_toint(cur));
+ }
+ }
+ }
+
+ elt = ucl_object_lookup(obj, "add_headers");
+ /*
+ * add_headers: {"name": "value", ... }
+ * name could have multiple values
+ * -or- (since 1.7)
+ * {"name": {"value": "val", "order": 0}, ... }
+ */
+ if (elt && ucl_object_type(elt) == UCL_OBJECT) {
+ it = NULL;
+
+ while ((cur = ucl_object_iterate(elt, &it, true)) != NULL) {
+
+ const char *key_name = ucl_object_key(cur);
+
+ if (ucl_object_type(cur) == UCL_STRING) {
+ /*
+ * Legacy support of {"name": "value", ... } with
+ * multiple names under the same name
+ */
+ ucl_object_iter_t *elt_it;
+ const ucl_object_t *cur_elt;
+
+ elt_it = ucl_object_iterate_new(cur);
+ while ((cur_elt = ucl_object_iterate_safe(elt_it, false)) != NULL) {
+ if (ucl_object_type(cur_elt) == UCL_STRING) {
+ hname = g_string_new(key_name);
+ hvalue = g_string_new(ucl_object_tostring(cur_elt));
+
+ rspamd_milter_send_action(session,
+ RSPAMD_MILTER_ADDHEADER,
+ hname, hvalue);
+ g_string_free(hname, TRUE);
+ g_string_free(hvalue, TRUE);
+ }
+ else {
+ msg_warn_milter("legacy header with name %s, that has not a string value: %s",
+ key_name, ucl_object_type_to_string(cur_elt->type));
+ }
+ }
+ ucl_object_iterate_free(elt_it);
+ }
+ else {
+ if (ucl_object_type(cur) == UCL_OBJECT) {
+ rspamd_milter_extract_single_header(session,
+ key_name, cur);
+ }
+ else if (ucl_object_type(cur) == UCL_ARRAY) {
+ /* Multiple values for the same key */
+ ucl_object_iter_t *array_it;
+ const ucl_object_t *array_elt;
+
+ array_it = ucl_object_iterate_new(cur);
+
+ while ((array_elt = ucl_object_iterate_safe(array_it,
+ true)) != NULL) {
+ rspamd_milter_extract_single_header(session,
+ key_name, array_elt);
+ }
+
+ ucl_object_iterate_free(array_it);
+ }
+ else {
+ msg_warn_milter("non-legacy header with name %s, that has unsupported value type: %s",
+ key_name, ucl_object_type_to_string(cur->type));
+ }
+ }
+ }
+ }
+
+ elt = ucl_object_lookup(obj, "change_from");
+
+ if (elt && ucl_object_type(elt) == UCL_STRING) {
+ hvalue = g_string_new(ucl_object_tostring(elt));
+ rspamd_milter_send_action(session,
+ RSPAMD_MILTER_CHGFROM,
+ hvalue);
+ g_string_free(hvalue, TRUE);
+ }
+
+ elt = ucl_object_lookup(obj, "add_rcpt");
+
+ if (elt && ucl_object_type(elt) == UCL_ARRAY) {
+ it = NULL;
+
+ while ((cur = ucl_object_iterate(elt, &it, true)) != NULL) {
+ hvalue = g_string_new(ucl_object_tostring(cur));
+ rspamd_milter_send_action(session,
+ RSPAMD_MILTER_ADDRCPT,
+ hvalue);
+ g_string_free(hvalue, TRUE);
+ }
+ }
+
+ elt = ucl_object_lookup(obj, "del_rcpt");
+
+ if (elt && ucl_object_type(elt) == UCL_ARRAY) {
+ it = NULL;
+
+ while ((cur = ucl_object_iterate(elt, &it, true)) != NULL) {
+ hvalue = g_string_new(ucl_object_tostring(cur));
+ rspamd_milter_send_action(session,
+ RSPAMD_MILTER_DELRCPT,
+ hvalue);
+ g_string_free(hvalue, TRUE);
+ }
+ }
+
+ elt = ucl_object_lookup(obj, "reject");
+
+ if (elt && ucl_object_type(elt) == UCL_STRING) {
+ if (strcmp(ucl_object_tostring(elt), "discard") == 0) {
+ priv->discard_on_reject = TRUE;
+ msg_info_milter("discard message instead of rejection");
+ }
+ else if (strcmp(ucl_object_tostring(elt), "quarantine") == 0) {
+ priv->quarantine_on_reject = TRUE;
+ msg_info_milter("quarantine message instead of rejection");
+ }
+ else {
+ priv->discard_on_reject = FALSE;
+ priv->quarantine_on_reject = FALSE;
+ }
+ }
+
+ elt = ucl_object_lookup(obj, "no_action");
+
+ if (elt && ucl_object_type(elt) == UCL_BOOLEAN) {
+ priv->no_action = ucl_object_toboolean(elt);
+ }
+ }
+
+ if (action->action_type == METRIC_ACTION_ADD_HEADER) {
+ elt = ucl_object_lookup(obj, "spam_header");
+
+ if (elt) {
+ if (ucl_object_type(elt) == UCL_STRING) {
+ rspamd_milter_remove_header_safe(session,
+ milter_ctx->spam_header,
+ 0);
+
+ hname = g_string_new(milter_ctx->spam_header);
+ hvalue = g_string_new(ucl_object_tostring(elt));
+ rspamd_milter_send_action(session, RSPAMD_MILTER_CHGHEADER,
+ (guint32) 1, hname, hvalue);
+ g_string_free(hname, TRUE);
+ g_string_free(hvalue, TRUE);
+ rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);
+
+ return TRUE;
+ }
+ else if (ucl_object_type(elt) == UCL_OBJECT) {
+ it = NULL;
+
+ while ((cur = ucl_object_iterate(elt, &it, true)) != NULL) {
+ rspamd_milter_remove_header_safe(session,
+ ucl_object_key(cur),
+ 0);
+
+ hname = g_string_new(ucl_object_key(cur));
+ hvalue = g_string_new(ucl_object_tostring(cur));
+ rspamd_milter_send_action(session, RSPAMD_MILTER_CHGHEADER,
+ (guint32) 1, hname, hvalue);
+ g_string_free(hname, TRUE);
+ g_string_free(hvalue, TRUE);
+ }
+
+ rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);
+
+ return TRUE;
+ }
+ }
+ }
+
+ return FALSE;
+}
+
+void rspamd_milter_send_task_results(struct rspamd_milter_session *session,
+ const ucl_object_t *results,
+ const gchar *new_body,
+ gsize bodylen)
+{
+ const ucl_object_t *elt;
+ struct rspamd_milter_private *priv = session->priv;
+ const gchar *str_action;
+ struct rspamd_action *action;
+ rspamd_fstring_t *xcode = NULL, *rcode = NULL, *reply = NULL;
+ GString *hname, *hvalue;
+ gboolean processed = FALSE;
+
+ if (results == NULL) {
+ msg_err_milter("cannot find scan results, tempfail");
+ rspamd_milter_send_action(session, RSPAMD_MILTER_TEMPFAIL);
+
+ goto cleanup;
+ }
+
+ elt = ucl_object_lookup(results, "action");
+
+ if (!elt) {
+ msg_err_milter("cannot find action in results, tempfail");
+ rspamd_milter_send_action(session, RSPAMD_MILTER_TEMPFAIL);
+
+ goto cleanup;
+ }
+
+ str_action = ucl_object_tostring(elt);
+ action = rspamd_config_get_action(milter_ctx->cfg, str_action);
+
+ if (action == NULL) {
+ msg_err_milter("action %s has not been registered", str_action);
+ rspamd_milter_send_action(session, RSPAMD_MILTER_TEMPFAIL);
+
+ goto cleanup;
+ }
+
+ elt = ucl_object_lookup(results, "messages");
+ if (elt) {
+ const ucl_object_t *smtp_res;
+ const gchar *msg;
+ gsize len = 0;
+
+ smtp_res = ucl_object_lookup(elt, "smtp_message");
+
+ if (smtp_res) {
+ msg = ucl_object_tolstring(smtp_res, &len);
+ reply = rspamd_fstring_new_init(msg, len);
+ }
+ }
+
+ /* Deal with milter headers */
+ elt = ucl_object_lookup(results, "milter");
+
+ if (elt) {
+ processed = rspamd_milter_process_milter_block(session, elt, action);
+ }
+
+ /* DKIM-Signature */
+ elt = ucl_object_lookup(results, "dkim-signature");
+
+ if (elt) {
+ hname = g_string_new(RSPAMD_MILTER_DKIM_HEADER);
+
+ if (ucl_object_type(elt) == UCL_STRING) {
+ hvalue = g_string_new(ucl_object_tostring(elt));
+
+ rspamd_milter_send_action(session, RSPAMD_MILTER_INSHEADER,
+ 1, hname, hvalue);
+
+ g_string_free(hvalue, TRUE);
+ }
+ else {
+ ucl_object_iter_t it;
+ const ucl_object_t *cur;
+ int i = 1;
+
+ it = ucl_object_iterate_new(elt);
+
+ while ((cur = ucl_object_iterate_safe(it, true)) != NULL) {
+ hvalue = g_string_new(ucl_object_tostring(cur));
+
+ rspamd_milter_send_action(session, RSPAMD_MILTER_INSHEADER,
+ i++, hname, hvalue);
+
+ g_string_free(hvalue, TRUE);
+ }
+
+ ucl_object_iterate_free(it);
+ }
+
+ g_string_free(hname, TRUE);
+ }
+
+ if (processed) {
+ goto cleanup;
+ }
+
+ if (new_body) {
+ rspamd_milter_send_action(session, RSPAMD_MILTER_REPLBODY,
+ bodylen, new_body);
+ }
+
+ if (priv->no_action) {
+ msg_info_milter("do not apply action %s, no_action is set",
+ str_action);
+ hname = g_string_new(RSPAMD_MILTER_ACTION_HEADER);
+ hvalue = g_string_new(str_action);
+
+ rspamd_milter_send_action(session, RSPAMD_MILTER_ADDHEADER,
+ hname, hvalue);
+ g_string_free(hname, TRUE);
+ g_string_free(hvalue, TRUE);
+ rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);
+
+ goto cleanup;
+ }
+
+ switch (action->action_type) {
+ case METRIC_ACTION_REJECT:
+ if (priv->discard_on_reject) {
+ rspamd_milter_send_action(session, RSPAMD_MILTER_DISCARD);
+ }
+ else if (priv->quarantine_on_reject) {
+ /* TODO: be more flexible about SMTP messages */
+ rspamd_milter_send_action(session, RSPAMD_MILTER_QUARANTINE,
+ RSPAMD_MILTER_QUARANTINE_MESSAGE);
+
+ /* Quarantine also requires accept action, all hail Sendmail */
+ rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);
+ }
+ else {
+ rcode = rspamd_fstring_new_init(RSPAMD_MILTER_RCODE_REJECT,
+ sizeof(RSPAMD_MILTER_RCODE_REJECT) - 1);
+ xcode = rspamd_fstring_new_init(RSPAMD_MILTER_XCODE_REJECT,
+ sizeof(RSPAMD_MILTER_XCODE_REJECT) - 1);
+
+ if (!reply) {
+ if (milter_ctx->reject_message == NULL) {
+ reply = rspamd_fstring_new_init(
+ RSPAMD_MILTER_REJECT_MESSAGE,
+ sizeof(RSPAMD_MILTER_REJECT_MESSAGE) - 1);
+ }
+ else {
+ reply = rspamd_fstring_new_init(milter_ctx->reject_message,
+ strlen(milter_ctx->reject_message));
+ }
+ }
+
+ rspamd_milter_set_reply(session, rcode, xcode, reply);
+ }
+ break;
+ case METRIC_ACTION_SOFT_REJECT:
+ rcode = rspamd_fstring_new_init(RSPAMD_MILTER_RCODE_TEMPFAIL,
+ sizeof(RSPAMD_MILTER_RCODE_TEMPFAIL) - 1);
+ xcode = rspamd_fstring_new_init(RSPAMD_MILTER_XCODE_TEMPFAIL,
+ sizeof(RSPAMD_MILTER_XCODE_TEMPFAIL) - 1);
+
+ if (!reply) {
+ reply = rspamd_fstring_new_init(RSPAMD_MILTER_TEMPFAIL_MESSAGE,
+ sizeof(RSPAMD_MILTER_TEMPFAIL_MESSAGE) - 1);
+ }
+
+ rspamd_milter_set_reply(session, rcode, xcode, reply);
+ break;
+
+ case METRIC_ACTION_REWRITE_SUBJECT:
+ elt = ucl_object_lookup(results, "subject");
+
+ if (elt) {
+ hname = g_string_new("Subject");
+ hvalue = g_string_new(ucl_object_tostring(elt));
+
+ rspamd_milter_send_action(session, RSPAMD_MILTER_CHGHEADER,
+ (guint32) 1, hname, hvalue);
+ g_string_free(hname, TRUE);
+ g_string_free(hvalue, TRUE);
+ }
+
+ rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);
+ break;
+
+ case METRIC_ACTION_ADD_HEADER:
+ /* Remove existing headers */
+ rspamd_milter_remove_header_safe(session,
+ milter_ctx->spam_header,
+ 0);
+
+ hname = g_string_new(milter_ctx->spam_header);
+ hvalue = g_string_new("Yes");
+ rspamd_milter_send_action(session, RSPAMD_MILTER_CHGHEADER,
+ (guint32) 1, hname, hvalue);
+ g_string_free(hname, TRUE);
+ g_string_free(hvalue, TRUE);
+ rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);
+ break;
+
+ case METRIC_ACTION_QUARANTINE:
+ /* TODO: be more flexible about SMTP messages */
+ rspamd_milter_send_action(session, RSPAMD_MILTER_QUARANTINE,
+ RSPAMD_MILTER_QUARANTINE_MESSAGE);
+
+ /* Quarantine also requires accept action, all hail Sendmail */
+ rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);
+ break;
+ case METRIC_ACTION_DISCARD:
+ rspamd_milter_send_action(session, RSPAMD_MILTER_DISCARD);
+ break;
+ case METRIC_ACTION_GREYLIST:
+ case METRIC_ACTION_NOACTION:
+ default:
+ rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);
+ break;
+ }
+
+cleanup:
+ rspamd_fstring_free(rcode);
+ rspamd_fstring_free(xcode);
+ rspamd_fstring_free(reply);
+
+ rspamd_milter_session_reset(session, RSPAMD_MILTER_RESET_ABORT);
+}
+
+void rspamd_milter_init_library(const struct rspamd_milter_context *ctx)
+{
+ milter_ctx = ctx;
+}
+
+rspamd_mempool_t *
+rspamd_milter_get_session_pool(struct rspamd_milter_session *session)
+{
+ struct rspamd_milter_private *priv = session->priv;
+
+ return priv->pool;
+}
diff --git a/src/libserver/milter.h b/src/libserver/milter.h
new file mode 100644
index 0000000..096cda8
--- /dev/null
+++ b/src/libserver/milter.h
@@ -0,0 +1,188 @@
+/*-
+ * Copyright 2017 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_MILTER_H
+#define RSPAMD_MILTER_H
+
+#include "config.h"
+#include "fstring.h"
+#include "addr.h"
+#include "contrib/libucl/ucl.h"
+#include "contrib/libev/ev.h"
+#include "ref.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum rspamd_milter_reply {
+ RSPAMD_MILTER_ADDRCPT = '+',
+ RSPAMD_MILTER_DELRCPT = '-',
+ RSPAMD_MILTER_ACCEPT = 'a',
+ RSPAMD_MILTER_CONTINUE = 'c',
+ RSPAMD_MILTER_DISCARD = 'd',
+ RSPAMD_MILTER_CHGFROM = 'e',
+ RSPAMD_MILTER_ADDHEADER = 'h',
+ RSPAMD_MILTER_CHGHEADER = 'm',
+ RSPAMD_MILTER_INSHEADER = 'i',
+ RSPAMD_MILTER_REPLBODY = 'b',
+ RSPAMD_MILTER_REJECT = 'r',
+ RSPAMD_MILTER_TEMPFAIL = 't',
+ RSPAMD_MILTER_REPLYCODE = 'y',
+ RSPAMD_MILTER_OPTNEG = 'O',
+ RSPAMD_MILTER_PROGRESS = 'p',
+ RSPAMD_MILTER_QUARANTINE = 'q',
+};
+
+struct rspamd_email_address;
+struct ev_loop;
+struct rspamd_http_message;
+struct rspamd_config;
+
+struct rspamd_milter_context {
+ const gchar *spam_header;
+ const gchar *client_ca_name;
+ const gchar *reject_message;
+ void *sessions_cache;
+ struct rspamd_config *cfg;
+ gboolean discard_on_reject;
+ gboolean quarantine_on_reject;
+};
+
+struct rspamd_milter_session {
+ GHashTable *macros;
+ rspamd_inet_addr_t *addr;
+ struct rspamd_email_address *from;
+ GPtrArray *rcpts;
+ rspamd_fstring_t *helo;
+ rspamd_fstring_t *hostname;
+ rspamd_fstring_t *message;
+ void *priv;
+ ref_entry_t ref;
+};
+
+typedef void (*rspamd_milter_finish)(gint fd,
+ struct rspamd_milter_session *session, void *ud);
+
+typedef void (*rspamd_milter_error)(gint fd,
+ struct rspamd_milter_session *session,
+ void *ud, GError *err);
+
+/**
+ * Handles socket with milter protocol
+ * @param fd
+ * @param finish_cb
+ * @param error_cb
+ * @param ud
+ * @return
+ */
+gboolean rspamd_milter_handle_socket(gint fd, ev_tstamp timeout,
+ rspamd_mempool_t *pool,
+ struct ev_loop *ev_base, rspamd_milter_finish finish_cb,
+ rspamd_milter_error error_cb, void *ud);
+
+/**
+ * Updates userdata for a session, returns previous userdata
+ * @param session
+ * @param ud
+ * @return
+ */
+void *rspamd_milter_update_userdata(struct rspamd_milter_session *session,
+ void *ud);
+
+/**
+ * Sets SMTP reply string
+ * @param session
+ * @param rcode
+ * @param xcode
+ * @param reply
+ * @return
+ */
+gboolean rspamd_milter_set_reply(struct rspamd_milter_session *session,
+ rspamd_fstring_t *rcode,
+ rspamd_fstring_t *xcode,
+ rspamd_fstring_t *reply);
+
+/**
+ * Send some action to the MTA
+ * @param fd
+ * @param session
+ * @param act
+ * @return
+ */
+gboolean rspamd_milter_send_action(struct rspamd_milter_session *session,
+ enum rspamd_milter_reply act, ...);
+
+/**
+ * Adds some header
+ * @param session
+ * @param name
+ * @param value
+ * @return
+ */
+gboolean rspamd_milter_add_header(struct rspamd_milter_session *session,
+ GString *name, GString *value);
+
+/**
+ * Removes some header
+ * @param session
+ * @param name
+ * @return
+ */
+gboolean rspamd_milter_del_header(struct rspamd_milter_session *session,
+ GString *name);
+
+void rspamd_milter_session_unref(struct rspamd_milter_session *session);
+
+struct rspamd_milter_session *rspamd_milter_session_ref(
+ struct rspamd_milter_session *session);
+
+/**
+ * Converts milter session to HTTP session that is suitable for Rspamd
+ * @param session
+ * @return
+ */
+struct rspamd_http_message *rspamd_milter_to_http(
+ struct rspamd_milter_session *session);
+
+/**
+ * Sends task results to the
+ * @param session
+ * @param results
+ */
+void rspamd_milter_send_task_results(struct rspamd_milter_session *session,
+ const ucl_object_t *results,
+ const gchar *new_body,
+ gsize bodylen);
+
+/**
+ * Init internal milter context
+ * @param spam_header spam header name (must NOT be NULL)
+ */
+void rspamd_milter_init_library(const struct rspamd_milter_context *ctx);
+
+/**
+ * Returns pool for a session
+ * @param session
+ * @return
+ */
+rspamd_mempool_t *rspamd_milter_get_session_pool(
+ struct rspamd_milter_session *session);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/milter_internal.h b/src/libserver/milter_internal.h
new file mode 100644
index 0000000..bc292d3
--- /dev/null
+++ b/src/libserver/milter_internal.h
@@ -0,0 +1,176 @@
+/*-
+ * Copyright 2017 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_MILTER_INTERNAL_H
+#define RSPAMD_MILTER_INTERNAL_H
+
+#include "config.h"
+#include "libutil/mem_pool.h"
+#include "contrib/libev/ev.h"
+#include "khash.h"
+#include "libutil/str_util.h"
+#include "libutil/libev_helper.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum rspamd_milter_state {
+ st_len_1 = 0,
+ st_len_2,
+ st_len_3,
+ st_len_4,
+ st_read_cmd,
+ st_read_data
+};
+
+struct rspamd_milter_parser {
+ rspamd_fstring_t *buf;
+ goffset pos;
+ goffset cmd_start;
+ gsize datalen;
+ enum rspamd_milter_state state;
+ gchar cur_cmd;
+};
+
+struct rspamd_milter_outbuf {
+ rspamd_fstring_t *buf;
+ goffset pos;
+ struct rspamd_milter_outbuf *next, *prev;
+};
+
+enum rspamd_milter_io_state {
+ RSPAMD_MILTER_READ_MORE,
+ RSPAMD_MILTER_WRITE_REPLY,
+ RSPAMD_MILTER_WANNA_DIE,
+ RSPAMD_MILTER_WRITE_AND_DIE,
+ RSPAMD_MILTER_PONG_AND_DIE,
+};
+
+KHASH_INIT(milter_headers_hash_t, char *, GArray *, true,
+ rspamd_strcase_hash, rspamd_strcase_equal);
+
+struct rspamd_milter_private {
+ struct rspamd_milter_parser parser;
+ struct rspamd_io_ev ev;
+ struct rspamd_milter_outbuf *out_chain;
+ struct ev_loop *event_loop;
+ rspamd_mempool_t *pool;
+ khash_t(milter_headers_hash_t) * headers;
+ gint cur_hdr;
+ rspamd_milter_finish fin_cb;
+ rspamd_milter_error err_cb;
+ void *ud;
+ enum rspamd_milter_io_state state;
+ int fd;
+ gboolean discard_on_reject;
+ gboolean quarantine_on_reject;
+ gboolean no_action;
+};
+
+enum rspamd_milter_io_cmd {
+ RSPAMD_MILTER_CMD_ABORT = 'A', /* Abort */
+ RSPAMD_MILTER_CMD_BODY = 'B', /* Body chunk */
+ RSPAMD_MILTER_CMD_CONNECT = 'C', /* Connection information */
+ RSPAMD_MILTER_CMD_MACRO = 'D', /* Define macro */
+ RSPAMD_MILTER_CMD_BODYEOB = 'E', /* final body chunk (end of message) */
+ RSPAMD_MILTER_CMD_HELO = 'H', /* HELO/EHLO */
+ RSPAMD_MILTER_CMD_QUIT_NC = 'K', /* QUIT but new connection follows */
+ RSPAMD_MILTER_CMD_HEADER = 'L', /* Header */
+ RSPAMD_MILTER_CMD_MAIL = 'M', /* MAIL from */
+ RSPAMD_MILTER_CMD_EOH = 'N', /* EOH */
+ RSPAMD_MILTER_CMD_OPTNEG = 'O', /* Option negotiation */
+ RSPAMD_MILTER_CMD_QUIT = 'Q', /* QUIT */
+ RSPAMD_MILTER_CMD_RCPT = 'R', /* RCPT to */
+ RSPAMD_MILTER_CMD_DATA = 'T', /* DATA */
+ RSPAMD_MILTER_CMD_UNKNOWN = 'U' /* Any unknown command */
+};
+
+/*
+ * Protocol flags
+ */
+#define RSPAMD_MILTER_FLAG_NOUNKNOWN (1L << 8) /* filter does not want unknown cmd */
+#define RSPAMD_MILTER_FLAG_NODATA (1L << 9) /* filter does not want DATA */
+#define RSPAMD_MILTER_FLAG_NR_HDR (1L << 7) /* filter won't reply for header */
+#define RSPAMD_MILTER_FLAG_SKIP (1L << 10) /* MTA supports SMFIR_SKIP */
+#define RSPAMD_MILTER_FLAG_RCPT_REJ (1L << 11) /* filter wants rejected RCPTs */
+#define RSPAMD_MILTER_FLAG_NR_CONN (1L << 12) /* filter won't reply for connect */
+#define RSPAMD_MILTER_FLAG_NR_HELO (1L << 13) /* filter won't reply for HELO */
+#define RSPAMD_MILTER_FLAG_NR_MAIL (1L << 14) /* filter won't reply for MAIL */
+#define RSPAMD_MILTER_FLAG_NR_RCPT (1L << 15) /* filter won't reply for RCPT */
+#define RSPAMD_MILTER_FLAG_NR_DATA (1L << 16) /* filter won't reply for DATA */
+#define RSPAMD_MILTER_FLAG_NR_UNKN (1L << 17) /* filter won't reply for UNKNOWN */
+#define RSPAMD_MILTER_FLAG_NR_EOH (1L << 18) /* filter won't reply for eoh */
+#define RSPAMD_MILTER_FLAG_NR_BODY (1L << 19) /* filter won't reply for body chunk */
+
+/*
+ * For now, we specify that we want to reply just after EOM
+ */
+#define RSPAMD_MILTER_FLAG_NOREPLY_MASK \
+ (RSPAMD_MILTER_FLAG_NR_CONN | RSPAMD_MILTER_FLAG_NR_HELO | \
+ RSPAMD_MILTER_FLAG_NR_MAIL | RSPAMD_MILTER_FLAG_NR_RCPT | \
+ RSPAMD_MILTER_FLAG_NR_DATA | RSPAMD_MILTER_FLAG_NR_UNKN | \
+ RSPAMD_MILTER_FLAG_NR_HDR | RSPAMD_MILTER_FLAG_NR_EOH | \
+ RSPAMD_MILTER_FLAG_NR_BODY)
+
+/*
+ * Options that the filter may send at initial handshake time, and message
+ * modifications that the filter may request at the end of the message body.
+ */
+#define RSPAMD_MILTER_FLAG_ADDHDRS (1L << 0) /* filter may add headers */
+#define RSPAMD_MILTER_FLAG_CHGBODY (1L << 1) /* filter may replace body */
+#define RSPAMD_MILTER_FLAG_ADDRCPT (1L << 2) /* filter may add recipients */
+#define RSPAMD_MILTER_FLAG_DELRCPT (1L << 3) /* filter may delete recipients */
+#define RSPAMD_MILTER_FLAG_CHGHDRS (1L << 4) /* filter may change/delete headers */
+#define RSPAMD_MILTER_FLAG_QUARANTINE (1L << 5) /* filter may request quarantine */
+
+#define RSPAMD_MILTER_ACTIONS_MASK \
+ (RSPAMD_MILTER_FLAG_ADDHDRS | RSPAMD_MILTER_FLAG_ADDRCPT | \
+ RSPAMD_MILTER_FLAG_DELRCPT | RSPAMD_MILTER_FLAG_CHGHDRS | \
+ RSPAMD_MILTER_FLAG_CHGBODY | RSPAMD_MILTER_FLAG_QUARANTINE)
+
+enum rspamd_milter_connect_proto {
+ RSPAMD_MILTER_CONN_UNKNOWN = 'U',
+ RSPAMD_MILTER_CONN_UNIX = 'L',
+ RSPAMD_MILTER_CONN_INET = '4',
+ RSPAMD_MILTER_CONN_INET6 = '6',
+};
+
+/*
+ * Rspamd supports just version 6 of the protocol, failing all versions below
+ * this one
+ */
+#define RSPAMD_MILTER_PROTO_VER 6
+
+#define RSPAMD_MILTER_MESSAGE_CHUNK 65536
+
+#define RSPAMD_MILTER_RCODE_REJECT "554"
+#define RSPAMD_MILTER_RCODE_TEMPFAIL "451"
+#define RSPAMD_MILTER_RCODE_LATER "452"
+#define RSPAMD_MILTER_XCODE_REJECT "5.7.1"
+#define RSPAMD_MILTER_XCODE_TEMPFAIL "4.7.1"
+#define RSPAMD_MILTER_REJECT_MESSAGE "Spam message rejected"
+#define RSPAMD_MILTER_QUARANTINE_MESSAGE "Spam message quarantined"
+#define RSPAMD_MILTER_TEMPFAIL_MESSAGE "Try again later"
+#define RSPAMD_MILTER_SPAM_HEADER "X-Spam"
+#define RSPAMD_MILTER_DKIM_HEADER "DKIM-Signature"
+#define RSPAMD_MILTER_ACTION_HEADER "X-Rspamd-Action"
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif \ No newline at end of file
diff --git a/src/libserver/monitored.c b/src/libserver/monitored.c
new file mode 100644
index 0000000..3aebaf6
--- /dev/null
+++ b/src/libserver/monitored.c
@@ -0,0 +1,735 @@
+/*-
+ * 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 <contrib/librdns/rdns.h>
+#include "rdns.h"
+#include "mem_pool.h"
+#include "cfg_file.h"
+#include "cryptobox.h"
+#include "logger.h"
+#include "contrib/uthash/utlist.h"
+
+static const gdouble default_monitoring_interval = 60.0;
+static const guint default_max_errors = 2;
+static const gdouble default_max_monitored_mult = 32;
+static const gdouble default_min_monitored_mult = 0.1;
+static const gdouble default_initial_monitored_mult = default_min_monitored_mult;
+static const gdouble default_offline_monitored_mult = 8.0;
+
+struct rspamd_monitored_methods {
+ void *(*monitored_config)(struct rspamd_monitored *m,
+ struct rspamd_monitored_ctx *ctx,
+ const ucl_object_t *opts);
+ gboolean (*monitored_update)(struct rspamd_monitored *m,
+ struct rspamd_monitored_ctx *ctx, gpointer ud);
+ void (*monitored_dtor)(struct rspamd_monitored *m,
+ struct rspamd_monitored_ctx *ctx, gpointer ud);
+ gpointer ud;
+};
+
+struct rspamd_monitored_ctx {
+ struct rspamd_config *cfg;
+ struct rdns_resolver *resolver;
+ struct ev_loop *event_loop;
+ GPtrArray *elts;
+ GHashTable *helts;
+ mon_change_cb change_cb;
+ gpointer ud;
+ gdouble monitoring_interval;
+ gdouble max_monitored_mult;
+ gdouble min_monitored_mult;
+ gdouble initial_monitored_mult;
+ gdouble offline_monitored_mult;
+ guint max_errors;
+ gboolean initialized;
+};
+
+struct rspamd_monitored {
+ gchar *url;
+ gdouble monitoring_mult;
+ gdouble offline_time;
+ gdouble total_offline_time;
+ gdouble latency;
+ guint nchecks;
+ guint max_errors;
+ guint cur_errors;
+ gboolean alive;
+ enum rspamd_monitored_type type;
+ enum rspamd_monitored_flags flags;
+ struct rspamd_monitored_ctx *ctx;
+ struct rspamd_monitored_methods proc;
+ ev_timer periodic;
+ gchar tag[RSPAMD_MONITORED_TAG_LEN];
+};
+
+#define msg_err_mon(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "monitored", m->tag, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_mon(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ "monitored", m->tag, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_mon(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ "monitored", m->tag, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_notice_mon(...) rspamd_default_log_function(G_LOG_LEVEL_MESSAGE, \
+ "monitored", m->tag, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_mon(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_monitored_log_id, "monitored", m->tag, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+INIT_LOG_MODULE(monitored)
+
+static inline void
+rspamd_monitored_propagate_error(struct rspamd_monitored *m,
+ const gchar *error)
+{
+ if (m->alive) {
+ if (m->cur_errors < m->max_errors) {
+
+ m->cur_errors++;
+ /* Reduce timeout */
+ rspamd_monitored_stop(m);
+
+ if (m->monitoring_mult > m->ctx->min_monitored_mult) {
+ if (m->monitoring_mult < 1.0) {
+ m->monitoring_mult = 1.0;
+ }
+ else {
+ m->monitoring_mult /= 2.0;
+ }
+ }
+
+ msg_debug_mon("%s on resolving %s, %d retries left; next check in %.2f",
+ error, m->url, m->max_errors - m->cur_errors,
+ m->ctx->monitoring_interval * m->monitoring_mult);
+
+ rspamd_monitored_start(m);
+ }
+ else {
+ msg_notice_mon("%s on resolving %s, disable object",
+ error, m->url);
+ m->alive = FALSE;
+ m->offline_time = rspamd_get_calendar_ticks();
+ rspamd_monitored_stop(m);
+ m->monitoring_mult = 2.0;
+ rspamd_monitored_start(m);
+
+ if (m->ctx->change_cb) {
+ m->ctx->change_cb(m->ctx, m, FALSE, m->ctx->ud);
+ }
+ }
+ }
+ else {
+ if (m->monitoring_mult < m->ctx->offline_monitored_mult) {
+ /* Increase timeout */
+ rspamd_monitored_stop(m);
+ m->monitoring_mult *= 2.0;
+ rspamd_monitored_start(m);
+ }
+ else {
+ rspamd_monitored_stop(m);
+ m->monitoring_mult = m->ctx->offline_monitored_mult;
+ rspamd_monitored_start(m);
+ }
+ }
+}
+
+static inline void
+rspamd_monitored_propagate_success(struct rspamd_monitored *m, gdouble lat)
+{
+ gdouble t;
+
+ m->cur_errors = 0;
+
+ if (!m->alive) {
+ m->monitoring_mult = 1.0;
+ t = rspamd_get_calendar_ticks();
+ m->total_offline_time += t - m->offline_time;
+ m->alive = TRUE;
+ msg_notice_mon("restoring %s after %.1f seconds of downtime, "
+ "total downtime: %.1f",
+ m->url, t - m->offline_time, m->total_offline_time);
+ m->offline_time = 0;
+ m->nchecks = 1;
+ m->latency = lat;
+ rspamd_monitored_stop(m);
+ rspamd_monitored_start(m);
+
+ if (m->ctx->change_cb) {
+ m->ctx->change_cb(m->ctx, m, TRUE, m->ctx->ud);
+ }
+ }
+ else {
+ /* Increase monitored interval */
+ if (m->monitoring_mult < m->ctx->max_monitored_mult) {
+ if (m->monitoring_mult < 1.0) {
+ /* Upgrade fast from the initial mult */
+ m->monitoring_mult = 1.0;
+ }
+ else {
+ m->monitoring_mult *= 2.0;
+ }
+ }
+ else {
+ m->monitoring_mult = m->ctx->max_monitored_mult;
+ }
+ m->latency = (lat + m->latency * m->nchecks) / (m->nchecks + 1);
+ m->nchecks++;
+ }
+}
+
+static void
+rspamd_monitored_periodic(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_monitored *m = (struct rspamd_monitored *) w->data;
+ gdouble jittered;
+ gboolean ret = FALSE;
+
+ if (m->proc.monitored_update) {
+ ret = m->proc.monitored_update(m, m->ctx, m->proc.ud);
+ }
+
+ jittered = rspamd_time_jitter(m->ctx->monitoring_interval * m->monitoring_mult,
+ 0.0);
+
+ if (ret) {
+ m->periodic.repeat = jittered;
+ ev_timer_again(EV_A_ & m->periodic);
+ }
+}
+
+struct rspamd_dns_monitored_conf {
+ enum rdns_request_type rt;
+ GString *request;
+ radix_compressed_t *expected;
+ struct rspamd_monitored *m;
+ gint expected_code;
+ gdouble check_tm;
+};
+
+static void
+rspamd_monitored_dns_random(struct rspamd_monitored *m,
+ struct rspamd_dns_monitored_conf *conf)
+{
+ gchar random_prefix[32];
+ const gchar dns_chars[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
+ gint len;
+
+ len = rspamd_random_uint64_fast() % sizeof(random_prefix);
+
+ if (len < 8) {
+ len = 8;
+ }
+
+ for (guint i = 0; i < len; i++) {
+ guint idx = rspamd_random_uint64_fast() % (sizeof(dns_chars) - 1);
+ random_prefix[i] = dns_chars[idx];
+ }
+
+ conf->request->len = 0;
+ rspamd_printf_gstring(conf->request, "%*.s.%s", len, random_prefix,
+ m->url);
+}
+
+static void *
+rspamd_monitored_dns_conf(struct rspamd_monitored *m,
+ struct rspamd_monitored_ctx *ctx,
+ const ucl_object_t *opts)
+{
+ struct rspamd_dns_monitored_conf *conf;
+ const ucl_object_t *elt;
+ gint rt;
+ GString *req = g_string_sized_new(127);
+
+ conf = g_malloc0(sizeof(*conf));
+ conf->rt = RDNS_REQUEST_A;
+ conf->m = m;
+ conf->expected_code = -1;
+
+ if (opts) {
+ elt = ucl_object_lookup(opts, "type");
+
+ if (elt) {
+ rt = rdns_type_fromstr(ucl_object_tostring(elt));
+
+ if (rt != -1) {
+ conf->rt = rt;
+ }
+ else {
+ msg_err_mon("invalid resolve type: %s",
+ ucl_object_tostring(elt));
+ }
+ }
+
+ if (!(m->flags & RSPAMD_MONITORED_RANDOM)) {
+ /* Prefix is useless for random monitored */
+ elt = ucl_object_lookup(opts, "prefix");
+
+ if (elt && ucl_object_type(elt) == UCL_STRING) {
+ rspamd_printf_gstring(req, "%s.", ucl_object_tostring(elt));
+ }
+ }
+
+ elt = ucl_object_lookup(opts, "ipnet");
+
+ if (elt) {
+ if (ucl_object_type(elt) == UCL_STRING) {
+ radix_add_generic_iplist(ucl_object_tostring(elt),
+ &conf->expected, FALSE, NULL);
+ }
+ else if (ucl_object_type(elt) == UCL_ARRAY) {
+ const ucl_object_t *cur;
+ ucl_object_iter_t it = NULL;
+
+ while ((cur = ucl_object_iterate(elt, &it, true)) != NULL) {
+ radix_add_generic_iplist(ucl_object_tostring(elt),
+ &conf->expected, FALSE, NULL);
+ }
+ }
+ }
+
+ elt = ucl_object_lookup(opts, "rcode");
+ if (elt) {
+ rt = rdns_rcode_fromstr(ucl_object_tostring(elt));
+
+ if (rt != -1) {
+ conf->expected_code = rt;
+ }
+ else {
+ msg_err_mon("invalid resolve rcode: %s",
+ ucl_object_tostring(elt));
+ }
+ }
+ }
+
+ if (!(m->flags & RSPAMD_MONITORED_RANDOM)) {
+ rspamd_printf_gstring(req, "%s", m->url);
+ }
+
+ conf->request = req;
+
+ return conf;
+}
+
+static void
+rspamd_monitored_dns_cb(struct rdns_reply *reply, void *arg)
+{
+ struct rspamd_dns_monitored_conf *conf = arg;
+ struct rspamd_monitored *m;
+ struct rdns_reply_entry *cur;
+ gboolean is_special_reply = FALSE;
+ gdouble lat;
+
+ m = conf->m;
+ lat = rspamd_get_calendar_ticks() - conf->check_tm;
+ conf->check_tm = 0;
+ msg_debug_mon("dns callback for %s in %.2f: %s", m->url, lat,
+ rdns_strerror(reply->code));
+
+ if (reply->code == RDNS_RC_TIMEOUT) {
+ rspamd_monitored_propagate_error(m, "timeout");
+ }
+ else if (reply->code == RDNS_RC_SERVFAIL) {
+ rspamd_monitored_propagate_error(m, "servfail");
+ }
+ else if (reply->code == RDNS_RC_REFUSED) {
+ rspamd_monitored_propagate_error(m, "refused");
+ }
+ else {
+ if (conf->expected_code != -1) {
+ if (reply->code != conf->expected_code) {
+ if (reply->code == RDNS_RC_NOREC &&
+ conf->expected_code == RDNS_RC_NXDOMAIN) {
+ rspamd_monitored_propagate_success(m, lat);
+ }
+ else {
+ LL_FOREACH(reply->entries, cur)
+ {
+ if (cur->type == RDNS_REQUEST_A) {
+ if ((guint32) cur->content.a.addr.s_addr ==
+ htonl(INADDR_LOOPBACK)) {
+ is_special_reply = TRUE;
+ }
+ }
+ }
+
+ if (is_special_reply) {
+ msg_notice_mon("DNS query blocked on %s "
+ "(127.0.0.1 returned), "
+ "possibly due to high volume",
+ m->url);
+ }
+ else {
+ msg_notice_mon("DNS reply returned '%s' for %s while '%s' "
+ "was expected when querying for '%s'"
+ "(likely DNS spoofing or BL internal issues)",
+ rdns_strerror(reply->code),
+ m->url,
+ rdns_strerror(conf->expected_code),
+ conf->request->str);
+ }
+
+ rspamd_monitored_propagate_error(m, "invalid return");
+ }
+ }
+ else {
+ rspamd_monitored_propagate_success(m, lat);
+ }
+ }
+ else if (conf->expected) {
+ /* We also need to check IP */
+ if (reply->code != RDNS_RC_NOERROR) {
+ rspamd_monitored_propagate_error(m, "no record");
+ }
+ else {
+ rspamd_inet_addr_t *addr;
+
+ addr = rspamd_inet_address_from_rnds(reply->entries);
+
+ if (!addr) {
+ rspamd_monitored_propagate_error(m,
+ "unreadable address");
+ }
+ else if (radix_find_compressed_addr(conf->expected, addr)) {
+ msg_notice_mon("bad address %s is returned when monitoring %s",
+ rspamd_inet_address_to_string(addr),
+ conf->request->str);
+ rspamd_monitored_propagate_error(m,
+ "invalid address");
+
+ rspamd_inet_address_free(addr);
+ }
+ else {
+ rspamd_monitored_propagate_success(m, lat);
+ rspamd_inet_address_free(addr);
+ }
+ }
+ }
+ else {
+ rspamd_monitored_propagate_success(m, lat);
+ }
+ }
+}
+
+static gboolean
+rspamd_monitored_dns_mon(struct rspamd_monitored *m,
+ struct rspamd_monitored_ctx *ctx, gpointer ud)
+{
+ struct rspamd_dns_monitored_conf *conf = ud;
+
+ if (m->flags & RSPAMD_MONITORED_RANDOM) {
+ rspamd_monitored_dns_random(m, conf);
+ }
+
+ if (!rdns_make_request_full(ctx->resolver, rspamd_monitored_dns_cb,
+ conf, ctx->cfg->dns_timeout, ctx->cfg->dns_retransmits,
+ 1, conf->request->str, conf->rt)) {
+ msg_notice_mon("cannot make request to resolve %s (%s monitored url)",
+ conf->request->str, conf->m->url);
+
+ m->cur_errors++;
+ rspamd_monitored_propagate_error(m, "failed to make DNS request");
+
+ return FALSE;
+ }
+ else {
+ conf->check_tm = rspamd_get_calendar_ticks();
+ }
+
+ return TRUE;
+}
+
+void rspamd_monitored_dns_dtor(struct rspamd_monitored *m,
+ struct rspamd_monitored_ctx *ctx, gpointer ud)
+{
+ struct rspamd_dns_monitored_conf *conf = ud;
+
+ g_string_free(conf->request, TRUE);
+
+ if (conf->expected) {
+ radix_destroy_compressed(conf->expected);
+ }
+
+ g_free(conf);
+}
+
+struct rspamd_monitored_ctx *
+rspamd_monitored_ctx_init(void)
+{
+ struct rspamd_monitored_ctx *ctx;
+
+ ctx = g_malloc0(sizeof(*ctx));
+ ctx->monitoring_interval = default_monitoring_interval;
+ ctx->max_errors = default_max_errors;
+ ctx->offline_monitored_mult = default_offline_monitored_mult;
+ ctx->initial_monitored_mult = default_initial_monitored_mult;
+ ctx->max_monitored_mult = default_max_monitored_mult;
+ ctx->min_monitored_mult = default_min_monitored_mult;
+ ctx->elts = g_ptr_array_new();
+ ctx->helts = g_hash_table_new(g_str_hash, g_str_equal);
+
+ return ctx;
+}
+
+
+void rspamd_monitored_ctx_config(struct rspamd_monitored_ctx *ctx,
+ struct rspamd_config *cfg,
+ struct ev_loop *ev_base,
+ struct rdns_resolver *resolver,
+ mon_change_cb change_cb,
+ gpointer ud)
+{
+ struct rspamd_monitored *m;
+ guint i;
+
+ g_assert(ctx != NULL);
+ ctx->event_loop = ev_base;
+ ctx->resolver = resolver;
+ ctx->cfg = cfg;
+ ctx->initialized = TRUE;
+ ctx->change_cb = change_cb;
+ ctx->ud = ud;
+
+ if (cfg->monitored_interval != 0) {
+ ctx->monitoring_interval = cfg->monitored_interval;
+ }
+
+ /* Start all events */
+ for (i = 0; i < ctx->elts->len; i++) {
+ m = g_ptr_array_index(ctx->elts, i);
+ m->monitoring_mult = ctx->initial_monitored_mult;
+ rspamd_monitored_start(m);
+ m->monitoring_mult = 1.0;
+ }
+}
+
+
+struct ev_loop *
+rspamd_monitored_ctx_get_ev_base(struct rspamd_monitored_ctx *ctx)
+{
+ return ctx->event_loop;
+}
+
+
+struct rspamd_monitored *
+rspamd_monitored_create_(struct rspamd_monitored_ctx *ctx,
+ const gchar *line,
+ enum rspamd_monitored_type type,
+ enum rspamd_monitored_flags flags,
+ const ucl_object_t *opts,
+ const gchar *loc)
+{
+ struct rspamd_monitored *m;
+ rspamd_cryptobox_hash_state_t st;
+ gchar *cksum_encoded, cksum[rspamd_cryptobox_HASHBYTES];
+
+ g_assert(ctx != NULL);
+
+ m = g_malloc0(sizeof(*m));
+ m->type = type;
+ m->flags = flags;
+
+ m->url = g_strdup(line);
+ m->ctx = ctx;
+ m->monitoring_mult = ctx->initial_monitored_mult;
+ m->max_errors = ctx->max_errors;
+ m->alive = TRUE;
+
+ if (type == RSPAMD_MONITORED_DNS) {
+ m->proc.monitored_update = rspamd_monitored_dns_mon;
+ m->proc.monitored_config = rspamd_monitored_dns_conf;
+ m->proc.monitored_dtor = rspamd_monitored_dns_dtor;
+ }
+ else {
+ g_free(m);
+
+ return NULL;
+ }
+
+ if (opts) {
+ const ucl_object_t *rnd_obj;
+
+ rnd_obj = ucl_object_lookup(opts, "random");
+
+ if (rnd_obj && ucl_object_type(rnd_obj) == UCL_BOOLEAN) {
+ if (ucl_object_toboolean(rnd_obj)) {
+ m->flags |= RSPAMD_MONITORED_RANDOM;
+ }
+ }
+ }
+
+ m->proc.ud = m->proc.monitored_config(m, ctx, opts);
+
+ if (m->proc.ud == NULL) {
+ g_free(m);
+
+ return NULL;
+ }
+
+ /* Create a persistent tag */
+ rspamd_cryptobox_hash_init(&st, NULL, 0);
+ rspamd_cryptobox_hash_update(&st, m->url, strlen(m->url));
+ rspamd_cryptobox_hash_update(&st, loc, strlen(loc));
+ rspamd_cryptobox_hash_final(&st, cksum);
+ cksum_encoded = rspamd_encode_base32(cksum, sizeof(cksum), RSPAMD_BASE32_DEFAULT);
+ rspamd_strlcpy(m->tag, cksum_encoded, sizeof(m->tag));
+
+ if (g_hash_table_lookup(ctx->helts, m->tag) != NULL) {
+ msg_err("monitored error: tag collision detected for %s; "
+ "url: %s",
+ m->tag, m->url);
+ }
+ else {
+ g_hash_table_insert(ctx->helts, m->tag, m);
+ }
+
+ g_free(cksum_encoded);
+
+ g_ptr_array_add(ctx->elts, m);
+
+ if (ctx->event_loop) {
+ rspamd_monitored_start(m);
+ }
+
+ return m;
+}
+
+gboolean
+rspamd_monitored_alive(struct rspamd_monitored *m)
+{
+ g_assert(m != NULL);
+
+ return m->alive;
+}
+
+gboolean
+rspamd_monitored_set_alive(struct rspamd_monitored *m, gboolean alive)
+{
+ gboolean st;
+
+ g_assert(m != NULL);
+ st = m->alive;
+ m->alive = alive;
+
+ return st;
+}
+
+gdouble
+rspamd_monitored_offline_time(struct rspamd_monitored *m)
+{
+ g_assert(m != NULL);
+
+ if (m->offline_time > 0) {
+ return rspamd_get_calendar_ticks() - m->offline_time;
+ }
+
+ return 0;
+}
+
+gdouble
+rspamd_monitored_total_offline_time(struct rspamd_monitored *m)
+{
+ g_assert(m != NULL);
+
+ if (m->offline_time > 0) {
+ return rspamd_get_calendar_ticks() - m->offline_time + m->total_offline_time;
+ }
+
+
+ return m->total_offline_time;
+}
+
+gdouble
+rspamd_monitored_latency(struct rspamd_monitored *m)
+{
+ g_assert(m != NULL);
+
+ return m->latency;
+}
+
+void rspamd_monitored_stop(struct rspamd_monitored *m)
+{
+ g_assert(m != NULL);
+
+ ev_timer_stop(m->ctx->event_loop, &m->periodic);
+}
+
+void rspamd_monitored_start(struct rspamd_monitored *m)
+{
+ gdouble jittered;
+
+ g_assert(m != NULL);
+ jittered = rspamd_time_jitter(m->ctx->monitoring_interval * m->monitoring_mult,
+ 0.0);
+
+ msg_debug_mon("started monitored object %s in %.2f seconds", m->url, jittered);
+
+ if (ev_can_stop(&m->periodic)) {
+ ev_timer_stop(m->ctx->event_loop, &m->periodic);
+ }
+
+ m->periodic.data = m;
+ ev_timer_init(&m->periodic, rspamd_monitored_periodic, jittered, 0.0);
+ ev_timer_start(m->ctx->event_loop, &m->periodic);
+}
+
+void rspamd_monitored_ctx_destroy(struct rspamd_monitored_ctx *ctx)
+{
+ struct rspamd_monitored *m;
+ guint i;
+
+ g_assert(ctx != NULL);
+
+ for (i = 0; i < ctx->elts->len; i++) {
+ m = g_ptr_array_index(ctx->elts, i);
+ rspamd_monitored_stop(m);
+ m->proc.monitored_dtor(m, m->ctx, m->proc.ud);
+ g_free(m->url);
+ g_free(m);
+ }
+
+ g_ptr_array_free(ctx->elts, TRUE);
+ g_hash_table_unref(ctx->helts);
+ g_free(ctx);
+}
+
+struct rspamd_monitored *
+rspamd_monitored_by_tag(struct rspamd_monitored_ctx *ctx,
+ guchar tag[RSPAMD_MONITORED_TAG_LEN])
+{
+ struct rspamd_monitored *res;
+ gchar rtag[RSPAMD_MONITORED_TAG_LEN];
+
+ rspamd_strlcpy(rtag, tag, sizeof(rtag));
+ res = g_hash_table_lookup(ctx->helts, rtag);
+
+ return res;
+}
+
+
+void rspamd_monitored_get_tag(struct rspamd_monitored *m,
+ guchar tag_out[RSPAMD_MONITORED_TAG_LEN])
+{
+ g_assert(m != NULL);
+
+ rspamd_strlcpy(tag_out, m->tag, RSPAMD_MONITORED_TAG_LEN);
+} \ No newline at end of file
diff --git a/src/libserver/monitored.h b/src/libserver/monitored.h
new file mode 100644
index 0000000..01f050a
--- /dev/null
+++ b/src/libserver/monitored.h
@@ -0,0 +1,161 @@
+/*-
+ * 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 SRC_LIBSERVER_MONITORED_H_
+#define SRC_LIBSERVER_MONITORED_H_
+
+#include "config.h"
+#include "rdns.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_monitored;
+struct rspamd_monitored_ctx;
+struct rspamd_config;
+
+#define RSPAMD_MONITORED_TAG_LEN 32
+
+enum rspamd_monitored_type {
+ RSPAMD_MONITORED_DNS = 0,
+};
+
+enum rspamd_monitored_flags {
+ RSPAMD_MONITORED_DEFAULT = 0u,
+ RSPAMD_MONITORED_RBL = (1u << 0u),
+ RSPAMD_MONITORED_RANDOM = (1u << 1u)
+};
+
+/**
+ * Initialize new monitored context
+ * @return opaque context pointer (should be configured)
+ */
+struct rspamd_monitored_ctx *rspamd_monitored_ctx_init(void);
+
+typedef void (*mon_change_cb)(struct rspamd_monitored_ctx *ctx,
+ struct rspamd_monitored *m, gboolean alive,
+ void *ud);
+
+/**
+ * Configure context for monitored objects
+ * @param ctx context
+ * @param cfg configuration
+ * @param ev_base events base
+ * @param resolver resolver object
+ */
+void rspamd_monitored_ctx_config(struct rspamd_monitored_ctx *ctx,
+ struct rspamd_config *cfg,
+ struct ev_loop *ev_base,
+ struct rdns_resolver *resolver,
+ mon_change_cb change_cb,
+ gpointer ud);
+
+struct ev_loop *rspamd_monitored_ctx_get_ev_base(struct rspamd_monitored_ctx *ctx);
+
+/**
+ * Create monitored object
+ * @param ctx context
+ * @param line string definition (e.g. hostname)
+ * @param type type of monitoring
+ * @param flags specific flags for monitoring
+ * @return new monitored object
+ */
+struct rspamd_monitored *rspamd_monitored_create_(
+ struct rspamd_monitored_ctx *ctx,
+ const gchar *line,
+ enum rspamd_monitored_type type,
+ enum rspamd_monitored_flags flags,
+ const ucl_object_t *opts,
+ const gchar *loc);
+
+#define rspamd_monitored_create(ctx, line, type, flags, opts) \
+ rspamd_monitored_create_(ctx, line, type, flags, opts, G_STRFUNC)
+
+/**
+ * Return monitored by its tag
+ * @param ctx
+ * @param tag
+ * @return
+ */
+struct rspamd_monitored *rspamd_monitored_by_tag(struct rspamd_monitored_ctx *ctx,
+ guchar tag[RSPAMD_MONITORED_TAG_LEN]);
+
+/**
+ * Sets `tag_out` to the monitored tag
+ * @param m
+ * @param tag_out
+ */
+void rspamd_monitored_get_tag(struct rspamd_monitored *m,
+ guchar tag_out[RSPAMD_MONITORED_TAG_LEN]);
+
+/**
+ * Return TRUE if monitored object is alive
+ * @param m monitored object
+ * @return TRUE or FALSE
+ */
+gboolean rspamd_monitored_alive(struct rspamd_monitored *m);
+
+/**
+ * Force alive flag for a monitored object
+ * @param m monitored object
+ * @return TRUE or FALSE
+ */
+gboolean rspamd_monitored_set_alive(struct rspamd_monitored *m, gboolean alive);
+
+/**
+ * Returns the current offline time for a monitored object
+ * @param m
+ * @return
+ */
+gdouble rspamd_monitored_offline_time(struct rspamd_monitored *m);
+
+/**
+ * Returns the total offline time for a monitored object
+ * @param m
+ * @return
+ */
+gdouble rspamd_monitored_total_offline_time(struct rspamd_monitored *m);
+
+/**
+ * Returns the latency for monitored object (in seconds)
+ * @param m
+ * @return
+ */
+gdouble rspamd_monitored_latency(struct rspamd_monitored *m);
+
+/**
+ * Explicitly disable monitored object
+ * @param m
+ */
+void rspamd_monitored_stop(struct rspamd_monitored *m);
+
+/**
+ * Explicitly enable monitored object
+ * @param m
+ */
+void rspamd_monitored_start(struct rspamd_monitored *m);
+
+/**
+ * Destroy monitored context and all monitored objects inside
+ * @param ctx
+ */
+void rspamd_monitored_ctx_destroy(struct rspamd_monitored_ctx *ctx);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SRC_LIBSERVER_MONITORED_H_ */
diff --git a/src/libserver/protocol.c b/src/libserver/protocol.c
new file mode 100644
index 0000000..8674557
--- /dev/null
+++ b/src/libserver/protocol.c
@@ -0,0 +1,2185 @@
+/*
+ * 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 "rspamd.h"
+#include "message.h"
+#include "utlist.h"
+#include "libserver/http/http_private.h"
+#include "worker_private.h"
+#include "libserver/cfg_file_private.h"
+#include "libmime/scan_result_private.h"
+#include "lua/lua_common.h"
+#include "unix-std.h"
+#include "protocol_internal.h"
+#include "libserver/mempool_vars_internal.h"
+#include "contrib/fastutf8/fastutf8.h"
+#include "task.h"
+#include <math.h>
+
+#ifdef SYS_ZSTD
+#include "zstd.h"
+#else
+#include "contrib/zstd/zstd.h"
+#endif
+
+INIT_LOG_MODULE(protocol)
+
+#define msg_err_protocol(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "protocol", task->task_pool->tag.uid, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+#define msg_warn_protocol(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ "protocol", task->task_pool->tag.uid, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+#define msg_info_protocol(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ "protocol", task->task_pool->tag.uid, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+#define msg_debug_protocol(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_protocol_log_id, "protocol", task->task_pool->tag.uid, \
+ G_STRFUNC, \
+ __VA_ARGS__)
+
+static GQuark
+rspamd_protocol_quark(void)
+{
+ return g_quark_from_static_string("protocol-error");
+}
+
+/*
+ * Remove <> from the fixed string and copy it to the pool
+ */
+static gchar *
+rspamd_protocol_escape_braces(struct rspamd_task *task, rspamd_ftok_t *in)
+{
+ guint nchars = 0;
+ const gchar *p;
+ rspamd_ftok_t tok;
+ gboolean has_obrace = FALSE;
+
+ g_assert(in != NULL);
+ g_assert(in->len > 0);
+
+ p = in->begin;
+
+ while ((g_ascii_isspace(*p) || *p == '<') && nchars < in->len) {
+ if (*p == '<') {
+ has_obrace = TRUE;
+ }
+
+ p++;
+ nchars++;
+ }
+
+ tok.begin = p;
+
+ p = in->begin + in->len - 1;
+ tok.len = in->len - nchars;
+
+ while (g_ascii_isspace(*p) && tok.len > 0) {
+ p--;
+ tok.len--;
+ }
+
+ if (has_obrace && *p == '>') {
+ tok.len--;
+ }
+
+ return rspamd_mempool_ftokdup(task->task_pool, &tok);
+}
+
+#define COMPARE_CMD(str, cmd, len) (sizeof(cmd) - 1 == (len) && rspamd_lc_cmp((str), (cmd), (len)) == 0)
+
+static gboolean
+rspamd_protocol_handle_url(struct rspamd_task *task,
+ struct rspamd_http_message *msg)
+{
+ GHashTable *query_args;
+ GHashTableIter it;
+ struct http_parser_url u;
+ const gchar *p;
+ gsize pathlen;
+ rspamd_ftok_t *key, *value;
+ gpointer k, v;
+
+ if (msg->url == NULL || msg->url->len == 0) {
+ g_set_error(&task->err, rspamd_protocol_quark(), 400, "missing command");
+ return FALSE;
+ }
+
+ if (http_parser_parse_url(msg->url->str, msg->url->len, 0, &u) != 0) {
+ g_set_error(&task->err, rspamd_protocol_quark(), 400, "bad request URL");
+
+ return FALSE;
+ }
+
+ if (!(u.field_set & (1 << UF_PATH))) {
+ g_set_error(&task->err, rspamd_protocol_quark(), 400,
+ "bad request URL: missing path");
+
+ return FALSE;
+ }
+
+ p = msg->url->str + u.field_data[UF_PATH].off;
+ pathlen = u.field_data[UF_PATH].len;
+
+ if (*p == '/') {
+ p++;
+ pathlen--;
+ }
+
+ switch (*p) {
+ case 'c':
+ case 'C':
+ /* check */
+ if (COMPARE_CMD(p, MSG_CMD_CHECK_V2, pathlen)) {
+ task->cmd = CMD_CHECK_V2;
+ msg_debug_protocol("got checkv2 command");
+ }
+ else if (COMPARE_CMD(p, MSG_CMD_CHECK, pathlen)) {
+ task->cmd = CMD_CHECK;
+ msg_debug_protocol("got check command");
+ }
+ else {
+ goto err;
+ }
+ break;
+ case 's':
+ case 'S':
+ /* symbols, skip */
+ if (COMPARE_CMD(p, MSG_CMD_SYMBOLS, pathlen)) {
+ task->cmd = CMD_CHECK;
+ msg_debug_protocol("got symbols -> old check command");
+ }
+ else if (COMPARE_CMD(p, MSG_CMD_SCAN, pathlen)) {
+ task->cmd = CMD_CHECK;
+ msg_debug_protocol("got scan -> old check command");
+ }
+ else if (COMPARE_CMD(p, MSG_CMD_SKIP, pathlen)) {
+ msg_debug_protocol("got skip command");
+ task->cmd = CMD_SKIP;
+ }
+ else {
+ goto err;
+ }
+ break;
+ case 'p':
+ case 'P':
+ /* ping, process */
+ if (COMPARE_CMD(p, MSG_CMD_PING, pathlen)) {
+ msg_debug_protocol("got ping command");
+ task->cmd = CMD_PING;
+ task->flags |= RSPAMD_TASK_FLAG_SKIP;
+ task->processed_stages |= RSPAMD_TASK_STAGE_DONE; /* Skip all */
+ }
+ else if (COMPARE_CMD(p, MSG_CMD_PROCESS, pathlen)) {
+ msg_debug_protocol("got process -> old check command");
+ task->cmd = CMD_CHECK;
+ }
+ else {
+ goto err;
+ }
+ break;
+ case 'r':
+ case 'R':
+ /* report, report_ifspam */
+ if (COMPARE_CMD(p, MSG_CMD_REPORT, pathlen)) {
+ msg_debug_protocol("got report -> old check command");
+ task->cmd = CMD_CHECK;
+ }
+ else if (COMPARE_CMD(p, MSG_CMD_REPORT_IFSPAM, pathlen)) {
+ msg_debug_protocol("got reportifspam -> old check command");
+ task->cmd = CMD_CHECK;
+ }
+ else {
+ goto err;
+ }
+ break;
+ default:
+ goto err;
+ }
+
+ if (u.field_set & (1u << UF_QUERY)) {
+ /* In case if we have a query, we need to store it somewhere */
+ query_args = rspamd_http_message_parse_query(msg);
+
+ /* Insert the rest of query params as HTTP headers */
+ g_hash_table_iter_init(&it, query_args);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ gchar *key_cpy;
+ key = k;
+ value = v;
+
+ key_cpy = rspamd_mempool_ftokdup(task->task_pool, key);
+
+ rspamd_http_message_add_header_len(msg, key_cpy,
+ value->begin, value->len);
+ msg_debug_protocol("added header \"%T\" -> \"%T\" from HTTP query",
+ key, value);
+ }
+
+ g_hash_table_unref(query_args);
+ }
+
+ return TRUE;
+
+err:
+ g_set_error(&task->err, rspamd_protocol_quark(), 400, "invalid command");
+
+ return FALSE;
+}
+
+static void
+rspamd_protocol_process_recipients(struct rspamd_task *task,
+ const rspamd_ftok_t *hdr)
+{
+ enum {
+ skip_spaces,
+ quoted_string,
+ normal_string,
+ } state = skip_spaces;
+ const gchar *p, *end, *start_addr;
+ struct rspamd_email_address *addr;
+
+ p = hdr->begin;
+ end = hdr->begin + hdr->len;
+ start_addr = NULL;
+
+ while (p < end) {
+ switch (state) {
+ case skip_spaces:
+ if (g_ascii_isspace(*p)) {
+ p++;
+ }
+ else if (*p == '"') {
+ start_addr = p;
+ p++;
+ state = quoted_string;
+ }
+ else {
+ state = normal_string;
+ start_addr = p;
+ }
+ break;
+ case quoted_string:
+ if (*p == '"') {
+ state = normal_string;
+ p++;
+ }
+ else if (*p == '\\') {
+ /* Quoted pair */
+ p += 2;
+ }
+ else {
+ p++;
+ }
+ break;
+ case normal_string:
+ if (*p == '"') {
+ state = quoted_string;
+ p++;
+ }
+ else if (*p == ',' && start_addr != NULL && p > start_addr) {
+ /* We have finished address, check what we have */
+ addr = rspamd_email_address_from_smtp(start_addr,
+ p - start_addr);
+
+ if (addr) {
+ if (task->rcpt_envelope == NULL) {
+ task->rcpt_envelope = g_ptr_array_sized_new(
+ 2);
+ }
+
+ g_ptr_array_add(task->rcpt_envelope, addr);
+ }
+ else {
+ msg_err_protocol("bad rcpt address: '%*s'",
+ (int) (p - start_addr), start_addr);
+ task->flags |= RSPAMD_TASK_FLAG_BROKEN_HEADERS;
+ }
+ start_addr = NULL;
+ p++;
+ state = skip_spaces;
+ }
+ else {
+ p++;
+ }
+ break;
+ }
+ }
+
+ /* Check remainder */
+ if (start_addr && p > start_addr) {
+ switch (state) {
+ case normal_string:
+ addr = rspamd_email_address_from_smtp(start_addr, end - start_addr);
+
+ if (addr) {
+ if (task->rcpt_envelope == NULL) {
+ task->rcpt_envelope = g_ptr_array_sized_new(
+ 2);
+ }
+
+ g_ptr_array_add(task->rcpt_envelope, addr);
+ }
+ else {
+ msg_err_protocol("bad rcpt address: '%*s'",
+ (int) (end - start_addr), start_addr);
+ task->flags |= RSPAMD_TASK_FLAG_BROKEN_HEADERS;
+ }
+ break;
+ case skip_spaces:
+ /* Do nothing */
+ break;
+ case quoted_string:
+ default:
+ msg_err_protocol("bad state when parsing rcpt address: '%*s'",
+ (int) (end - start_addr), start_addr);
+ task->flags |= RSPAMD_TASK_FLAG_BROKEN_HEADERS;
+ }
+ }
+}
+
+#define COMPARE_FLAG_LIT(lit) (len == sizeof(lit) - 1 && memcmp((lit), str, len) == 0)
+#define CHECK_PROTOCOL_FLAG(lit, fl) \
+ do { \
+ if (!known && COMPARE_FLAG_LIT(lit)) { \
+ task->protocol_flags |= (fl); \
+ known = TRUE; \
+ msg_debug_protocol("add protocol flag %s", lit); \
+ } \
+ } while (0)
+#define CHECK_TASK_FLAG(lit, fl) \
+ do { \
+ if (!known && COMPARE_FLAG_LIT(lit)) { \
+ task->flags |= (fl); \
+ known = TRUE; \
+ msg_debug_protocol("add task flag %s", lit); \
+ } \
+ } while (0)
+
+static void
+rspamd_protocol_handle_flag(struct rspamd_task *task, const gchar *str,
+ gsize len)
+{
+ gboolean known = FALSE;
+
+ CHECK_TASK_FLAG("pass_all", RSPAMD_TASK_FLAG_PASS_ALL);
+ CHECK_TASK_FLAG("no_log", RSPAMD_TASK_FLAG_NO_LOG);
+ CHECK_TASK_FLAG("skip", RSPAMD_TASK_FLAG_SKIP);
+ CHECK_TASK_FLAG("skip_process", RSPAMD_TASK_FLAG_SKIP_PROCESS);
+ CHECK_TASK_FLAG("no_stat", RSPAMD_TASK_FLAG_NO_STAT);
+ CHECK_TASK_FLAG("ssl", RSPAMD_TASK_FLAG_SSL);
+ CHECK_TASK_FLAG("profile", RSPAMD_TASK_FLAG_PROFILE);
+
+ CHECK_PROTOCOL_FLAG("milter", RSPAMD_TASK_PROTOCOL_FLAG_MILTER);
+ CHECK_PROTOCOL_FLAG("zstd", RSPAMD_TASK_PROTOCOL_FLAG_COMPRESSED);
+ CHECK_PROTOCOL_FLAG("ext_urls", RSPAMD_TASK_PROTOCOL_FLAG_EXT_URLS);
+ CHECK_PROTOCOL_FLAG("body_block", RSPAMD_TASK_PROTOCOL_FLAG_BODY_BLOCK);
+ CHECK_PROTOCOL_FLAG("groups", RSPAMD_TASK_PROTOCOL_FLAG_GROUPS);
+
+ if (!known) {
+ msg_warn_protocol("unknown flag: %*s", (gint) len, str);
+ }
+}
+
+#undef COMPARE_FLAG
+#undef CHECK_PROTOCOL_FLAG
+
+static void
+rspamd_protocol_process_flags(struct rspamd_task *task, const rspamd_ftok_t *hdr)
+{
+ enum {
+ skip_spaces,
+ read_flag,
+ } state = skip_spaces;
+ const gchar *p, *end, *start;
+
+ p = hdr->begin;
+ end = hdr->begin + hdr->len;
+ start = NULL;
+
+ while (p < end) {
+ switch (state) {
+ case skip_spaces:
+ if (g_ascii_isspace(*p)) {
+ p++;
+ }
+ else {
+ state = read_flag;
+ start = p;
+ }
+ break;
+ case read_flag:
+ if (*p == ',') {
+ if (p > start) {
+ rspamd_protocol_handle_flag(task, start, p - start);
+ }
+ start = NULL;
+ state = skip_spaces;
+ p++;
+ }
+ else {
+ p++;
+ }
+ break;
+ }
+ }
+
+ /* Check remainder */
+ if (start && end > start && state == read_flag) {
+ rspamd_protocol_handle_flag(task, start, end - start);
+ }
+}
+
+#define IF_HEADER(name) \
+ srch.begin = (name); \
+ srch.len = sizeof(name) - 1; \
+ if (rspamd_ftok_casecmp(hn_tok, &srch) == 0)
+
+gboolean
+rspamd_protocol_handle_headers(struct rspamd_task *task,
+ struct rspamd_http_message *msg)
+{
+ rspamd_ftok_t *hn_tok, *hv_tok, srch;
+ gboolean has_ip = FALSE, seen_settings_header = FALSE;
+ struct rspamd_http_header *header, *h;
+ gchar *ntok;
+
+ kh_foreach_value (msg->headers, header, {
+ DL_FOREACH (header, h) {
+ ntok = rspamd_mempool_ftokdup (task->task_pool, &h->name);
+ hn_tok = rspamd_mempool_alloc (task->task_pool, sizeof (*hn_tok));
+ hn_tok->begin = ntok;
+ hn_tok->len = h->name.len;
+
+
+ ntok = rspamd_mempool_ftokdup (task->task_pool, &h->value);
+ hv_tok = rspamd_mempool_alloc (task->task_pool, sizeof (*hv_tok));
+ hv_tok->begin = ntok;
+ hv_tok->len = h->value.len;
+
+ switch (*hn_tok->begin) {
+ case 'd':
+ case 'D':
+ IF_HEADER(DELIVER_TO_HEADER)
+ {
+ task->deliver_to = rspamd_protocol_escape_braces(task, hv_tok);
+ msg_debug_protocol("read deliver-to header, value: %s",
+ task->deliver_to);
+ }
+ else
+ {
+ msg_debug_protocol("wrong header: %T", hn_tok);
+ }
+ break;
+ case 'h':
+ case 'H':
+ IF_HEADER(HELO_HEADER)
+ {
+ task->helo = rspamd_mempool_ftokdup(task->task_pool, hv_tok);
+ msg_debug_protocol("read helo header, value: %s", task->helo);
+ }
+ IF_HEADER(HOSTNAME_HEADER)
+ {
+ task->hostname = rspamd_mempool_ftokdup(task->task_pool,
+ hv_tok);
+ msg_debug_protocol("read hostname header, value: %s", task->hostname);
+ }
+ break;
+ case 'f':
+ case 'F':
+ IF_HEADER(FROM_HEADER)
+ {
+ if (hv_tok->len == 0) {
+ /* Replace '' with '<>' to fix parsing issue */
+ RSPAMD_FTOK_ASSIGN(hv_tok, "<>");
+ }
+ task->from_envelope = rspamd_email_address_from_smtp(
+ hv_tok->begin,
+ hv_tok->len);
+ msg_debug_protocol("read from header, value: %T", hv_tok);
+
+ if (!task->from_envelope) {
+ msg_err_protocol("bad from header: '%T'", hv_tok);
+ task->flags |= RSPAMD_TASK_FLAG_BROKEN_HEADERS;
+ }
+ }
+ IF_HEADER(FILENAME_HEADER)
+ {
+ task->msg.fpath = rspamd_mempool_ftokdup(task->task_pool,
+ hv_tok);
+ msg_debug_protocol("read filename header, value: %s", task->msg.fpath);
+ }
+ IF_HEADER(FLAGS_HEADER)
+ {
+ msg_debug_protocol("read flags header, value: %T", hv_tok);
+ rspamd_protocol_process_flags(task, hv_tok);
+ }
+ break;
+ case 'q':
+ case 'Q':
+ IF_HEADER(QUEUE_ID_HEADER)
+ {
+ task->queue_id = rspamd_mempool_ftokdup(task->task_pool,
+ hv_tok);
+ msg_debug_protocol("read queue_id header, value: %s", task->queue_id);
+ }
+ else
+ {
+ msg_debug_protocol("wrong header: %T", hn_tok);
+ }
+ break;
+ case 'r':
+ case 'R':
+ IF_HEADER(RCPT_HEADER)
+ {
+ rspamd_protocol_process_recipients(task, hv_tok);
+ msg_debug_protocol("read rcpt header, value: %T", hv_tok);
+ }
+ IF_HEADER(RAW_DATA_HEADER)
+ {
+ srch.begin = "yes";
+ srch.len = 3;
+
+ msg_debug_protocol("read raw data header, value: %T", hv_tok);
+
+ if (rspamd_ftok_casecmp(hv_tok, &srch) == 0) {
+ task->flags &= ~RSPAMD_TASK_FLAG_MIME;
+ msg_debug_protocol("disable mime parsing");
+ }
+ }
+ break;
+ case 'i':
+ case 'I':
+ IF_HEADER(IP_ADDR_HEADER)
+ {
+ if (!rspamd_parse_inet_address(&task->from_addr,
+ hv_tok->begin, hv_tok->len,
+ RSPAMD_INET_ADDRESS_PARSE_DEFAULT)) {
+ msg_err_protocol("bad ip header: '%T'", hv_tok);
+ }
+ else {
+ msg_debug_protocol("read IP header, value: %T", hv_tok);
+ has_ip = TRUE;
+ }
+ }
+ else
+ {
+ msg_debug_protocol("wrong header: %T", hn_tok);
+ }
+ break;
+ case 'p':
+ case 'P':
+ IF_HEADER(PASS_HEADER)
+ {
+ srch.begin = "all";
+ srch.len = 3;
+
+ msg_debug_protocol("read pass header, value: %T", hv_tok);
+
+ if (rspamd_ftok_casecmp(hv_tok, &srch) == 0) {
+ task->flags |= RSPAMD_TASK_FLAG_PASS_ALL;
+ msg_debug_protocol("pass all filters");
+ }
+ }
+ IF_HEADER(PROFILE_HEADER)
+ {
+ msg_debug_protocol("read profile header, value: %T", hv_tok);
+ task->flags |= RSPAMD_TASK_FLAG_PROFILE;
+ }
+ break;
+ case 's':
+ case 'S':
+ IF_HEADER(SETTINGS_ID_HEADER)
+ {
+ msg_debug_protocol("read settings-id header, value: %T", hv_tok);
+ task->settings_elt = rspamd_config_find_settings_name_ref(
+ task->cfg, hv_tok->begin, hv_tok->len);
+
+ if (task->settings_elt == NULL) {
+ GString *known_ids = g_string_new(NULL);
+ struct rspamd_config_settings_elt *cur;
+
+ DL_FOREACH(task->cfg->setting_ids, cur)
+ {
+ rspamd_printf_gstring(known_ids, "%s(%ud);",
+ cur->name, cur->id);
+ }
+
+ msg_warn_protocol("unknown settings id: %T(%d); known_ids: %v",
+ hv_tok,
+ rspamd_config_name_to_id(hv_tok->begin, hv_tok->len),
+ known_ids);
+
+ g_string_free(known_ids, TRUE);
+ }
+ else {
+ msg_debug_protocol("applied settings id %T -> %ud", hv_tok,
+ task->settings_elt->id);
+ }
+ }
+ IF_HEADER(SETTINGS_HEADER)
+ {
+ msg_debug_protocol("read settings header, value: %T", hv_tok);
+ seen_settings_header = TRUE;
+ }
+ break;
+ case 'u':
+ case 'U':
+ IF_HEADER(USER_HEADER)
+ {
+ /*
+ * We must ignore User header in case of spamc, as SA has
+ * different meaning of this header
+ */
+ msg_debug_protocol("read user header, value: %T", hv_tok);
+ if (!RSPAMD_TASK_IS_SPAMC(task)) {
+ task->auth_user = rspamd_mempool_ftokdup(task->task_pool,
+ hv_tok);
+ }
+ else {
+ msg_info_protocol("ignore user header: legacy SA protocol");
+ }
+ }
+ IF_HEADER(URLS_HEADER)
+ {
+ msg_debug_protocol("read urls header, value: %T", hv_tok);
+
+ srch.begin = "extended";
+ srch.len = 8;
+
+ if (rspamd_ftok_casecmp(hv_tok, &srch) == 0) {
+ task->protocol_flags |= RSPAMD_TASK_PROTOCOL_FLAG_EXT_URLS;
+ msg_debug_protocol("extended urls information");
+ }
+
+ /* TODO: add more formats there */
+ }
+ IF_HEADER(USER_AGENT_HEADER)
+ {
+ msg_debug_protocol("read user-agent header, value: %T", hv_tok);
+
+ if (hv_tok->len == 6 &&
+ rspamd_lc_cmp(hv_tok->begin, "rspamc", 6) == 0) {
+ task->protocol_flags |= RSPAMD_TASK_PROTOCOL_FLAG_LOCAL_CLIENT;
+ }
+ }
+ break;
+ case 'l':
+ case 'L':
+ IF_HEADER(NO_LOG_HEADER)
+ {
+ msg_debug_protocol("read log header, value: %T", hv_tok);
+ srch.begin = "no";
+ srch.len = 2;
+
+ if (rspamd_ftok_casecmp(hv_tok, &srch) == 0) {
+ task->flags |= RSPAMD_TASK_FLAG_NO_LOG;
+ }
+ }
+ break;
+ case 'm':
+ case 'M':
+ IF_HEADER(MLEN_HEADER)
+ {
+ msg_debug_protocol("read message length header, value: %T",
+ hv_tok);
+ task->protocol_flags |= RSPAMD_TASK_PROTOCOL_FLAG_HAS_CONTROL;
+ }
+ IF_HEADER(MTA_TAG_HEADER)
+ {
+ gchar *mta_tag;
+ mta_tag = rspamd_mempool_ftokdup(task->task_pool, hv_tok);
+ rspamd_mempool_set_variable(task->task_pool,
+ RSPAMD_MEMPOOL_MTA_TAG,
+ mta_tag, NULL);
+ msg_debug_protocol("read MTA-Tag header, value: %s", mta_tag);
+ }
+ IF_HEADER(MTA_NAME_HEADER)
+ {
+ gchar *mta_name;
+ mta_name = rspamd_mempool_ftokdup(task->task_pool, hv_tok);
+ rspamd_mempool_set_variable(task->task_pool,
+ RSPAMD_MEMPOOL_MTA_NAME,
+ mta_name, NULL);
+ msg_debug_protocol("read MTA-Name header, value: %s", mta_name);
+ }
+ IF_HEADER(MILTER_HEADER)
+ {
+ task->protocol_flags |= RSPAMD_TASK_PROTOCOL_FLAG_MILTER;
+ msg_debug_protocol("read Milter header, value: %T", hv_tok);
+ }
+ break;
+ case 't':
+ case 'T':
+ IF_HEADER(TLS_CIPHER_HEADER)
+ {
+ task->flags |= RSPAMD_TASK_FLAG_SSL;
+ msg_debug_protocol("read TLS cipher header, value: %T", hv_tok);
+ }
+ break;
+ default:
+ msg_debug_protocol("generic header: %T", hn_tok);
+ break;
+ }
+
+ rspamd_task_add_request_header (task, hn_tok, hv_tok);
+}
+}); /* End of kh_foreach_value */
+
+if (seen_settings_header && task->settings_elt) {
+ msg_warn_task("ignore settings id %s as settings header is also presented",
+ task->settings_elt->name);
+ REF_RELEASE(task->settings_elt);
+
+ task->settings_elt = NULL;
+}
+
+if (!has_ip) {
+ task->flags |= RSPAMD_TASK_FLAG_NO_IP;
+}
+
+return TRUE;
+}
+
+#define BOOL_TO_FLAG(val, flags, flag) \
+ do { \
+ if ((val)) (flags) |= (flag); \
+ else \
+ (flags) &= ~(flag); \
+ } while (0)
+
+gboolean
+rspamd_protocol_parse_task_flags(rspamd_mempool_t *pool,
+ const ucl_object_t *obj,
+ gpointer ud,
+ struct rspamd_rcl_section *section,
+ GError **err)
+{
+ struct rspamd_rcl_struct_parser *pd = ud;
+ gint *target;
+ const gchar *key;
+ gboolean value;
+
+ target = (gint *) (((gchar *) pd->user_struct) + pd->offset);
+ key = ucl_object_key(obj);
+ value = ucl_object_toboolean(obj);
+
+ if (key != NULL) {
+ if (g_ascii_strcasecmp(key, "pass_all") == 0) {
+ BOOL_TO_FLAG(value, *target, RSPAMD_TASK_FLAG_PASS_ALL);
+ }
+ else if (g_ascii_strcasecmp(key, "no_log") == 0) {
+ BOOL_TO_FLAG(value, *target, RSPAMD_TASK_FLAG_NO_LOG);
+ }
+ }
+
+ return TRUE;
+}
+
+static struct rspamd_rcl_sections_map *control_parser = NULL;
+
+RSPAMD_CONSTRUCTOR(rspamd_protocol_control_parser_ctor)
+{
+
+ struct rspamd_rcl_section *sub = rspamd_rcl_add_section(&control_parser, NULL,
+ "*",
+ NULL,
+ NULL,
+ UCL_OBJECT,
+ FALSE,
+ TRUE);
+ /* Default handlers */
+ rspamd_rcl_add_default_handler(sub,
+ "ip",
+ rspamd_rcl_parse_struct_addr,
+ G_STRUCT_OFFSET(struct rspamd_task, from_addr),
+ 0,
+ NULL);
+ rspamd_rcl_add_default_handler(sub,
+ "from",
+ rspamd_rcl_parse_struct_mime_addr,
+ G_STRUCT_OFFSET(struct rspamd_task, from_envelope),
+ 0,
+ NULL);
+ rspamd_rcl_add_default_handler(sub,
+ "rcpt",
+ rspamd_rcl_parse_struct_mime_addr,
+ G_STRUCT_OFFSET(struct rspamd_task, rcpt_envelope),
+ 0,
+ NULL);
+ rspamd_rcl_add_default_handler(sub,
+ "helo",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_task, helo),
+ 0,
+ NULL);
+ rspamd_rcl_add_default_handler(sub,
+ "user",
+ rspamd_rcl_parse_struct_string,
+ G_STRUCT_OFFSET(struct rspamd_task, auth_user),
+ 0,
+ NULL);
+ rspamd_rcl_add_default_handler(sub,
+ "pass_all",
+ rspamd_protocol_parse_task_flags,
+ G_STRUCT_OFFSET(struct rspamd_task, flags),
+ 0,
+ NULL);
+ rspamd_rcl_add_default_handler(sub,
+ "json",
+ rspamd_protocol_parse_task_flags,
+ G_STRUCT_OFFSET(struct rspamd_task, flags),
+ 0,
+ NULL);
+}
+
+RSPAMD_DESTRUCTOR(rspamd_protocol_control_parser_dtor)
+{
+ rspamd_rcl_sections_free(control_parser);
+}
+
+gboolean
+rspamd_protocol_handle_control(struct rspamd_task *task,
+ const ucl_object_t *control)
+{
+ GError *err = NULL;
+
+ if (!rspamd_rcl_parse(control_parser, task->cfg, task, task->task_pool,
+ control, &err)) {
+ msg_warn_protocol("cannot parse control block: %e", err);
+ g_error_free(err);
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_protocol_handle_request(struct rspamd_task *task,
+ struct rspamd_http_message *msg)
+{
+ gboolean ret = TRUE;
+
+ if (msg->method == HTTP_SYMBOLS) {
+ msg_debug_protocol("got legacy SYMBOLS method, enable rspamc protocol workaround");
+ task->cmd = CMD_CHECK_RSPAMC;
+ }
+ else if (msg->method == HTTP_CHECK) {
+ msg_debug_protocol("got legacy CHECK method, enable rspamc protocol workaround");
+ task->cmd = CMD_CHECK_RSPAMC;
+ }
+ else {
+ ret = rspamd_protocol_handle_url(task, msg);
+ }
+
+ if (msg->flags & RSPAMD_HTTP_FLAG_SPAMC) {
+ msg_debug_protocol("got legacy SA input, enable spamc protocol workaround");
+ task->cmd = CMD_CHECK_SPAMC;
+ }
+
+ return ret;
+}
+
+/* Structure for writing tree data */
+struct tree_cb_data {
+ ucl_object_t *top;
+ khash_t(rspamd_url_host_hash) * seen;
+ struct rspamd_task *task;
+};
+
+static ucl_object_t *
+rspamd_protocol_extended_url(struct rspamd_task *task,
+ struct rspamd_url *url,
+ const gchar *encoded, gsize enclen)
+{
+ ucl_object_t *obj, *elt;
+
+ obj = ucl_object_typed_new(UCL_OBJECT);
+
+ elt = ucl_object_fromstring_common(encoded, enclen, 0);
+ ucl_object_insert_key(obj, elt, "url", 0, false);
+
+ if (url->tldlen > 0) {
+ elt = ucl_object_fromstring_common(rspamd_url_tld_unsafe(url),
+ url->tldlen, 0);
+ ucl_object_insert_key(obj, elt, "tld", 0, false);
+ }
+ if (url->hostlen > 0) {
+ elt = ucl_object_fromstring_common(rspamd_url_host_unsafe(url),
+ url->hostlen, 0);
+ ucl_object_insert_key(obj, elt, "host", 0, false);
+ }
+
+ ucl_object_t *flags = ucl_object_typed_new(UCL_ARRAY);
+
+ for (unsigned int i = 0; i < RSPAMD_URL_MAX_FLAG_SHIFT; i++) {
+ if (url->flags & (1u << i)) {
+ ucl_object_t *fl = ucl_object_fromstring(rspamd_url_flag_to_string(1u << i));
+ ucl_array_append(flags, fl);
+ }
+ }
+
+ ucl_object_insert_key(obj, flags, "flags", 0, false);
+
+ if (url->ext && url->ext->linked_url) {
+ encoded = rspamd_url_encode(url->ext->linked_url, &enclen, task->task_pool);
+ elt = rspamd_protocol_extended_url(task, url->ext->linked_url, encoded,
+ enclen);
+ ucl_object_insert_key(obj, elt, "linked_url", 0, false);
+ }
+
+ return obj;
+}
+
+/*
+ * Callback for writing urls
+ */
+static void
+urls_protocol_cb(struct rspamd_url *url, struct tree_cb_data *cb)
+{
+ ucl_object_t *obj;
+ struct rspamd_task *task = cb->task;
+ const gchar *user_field = "unknown", *encoded = NULL;
+ gboolean has_user = FALSE;
+ guint len = 0;
+ gsize enclen = 0;
+
+ if (!(task->protocol_flags & RSPAMD_TASK_PROTOCOL_FLAG_EXT_URLS)) {
+ if (url->hostlen > 0) {
+ if (rspamd_url_host_set_has(cb->seen, url)) {
+ return;
+ }
+
+ goffset err_offset;
+
+ if ((err_offset = rspamd_fast_utf8_validate(rspamd_url_host_unsafe(url),
+ url->hostlen)) == 0) {
+ obj = ucl_object_fromstring_common(rspamd_url_host_unsafe(url),
+ url->hostlen, 0);
+ }
+ else {
+ obj = ucl_object_fromstring_common(rspamd_url_host_unsafe(url),
+ err_offset - 1, 0);
+ }
+ }
+ else {
+ return;
+ }
+
+ rspamd_url_host_set_add(cb->seen, url);
+ }
+ else {
+ encoded = rspamd_url_encode(url, &enclen, task->task_pool);
+ obj = rspamd_protocol_extended_url(task, url, encoded, enclen);
+ }
+
+ ucl_array_append(cb->top, obj);
+
+ if (cb->task->cfg->log_urls) {
+ if (task->auth_user) {
+ user_field = task->auth_user;
+ len = strlen(task->auth_user);
+ has_user = TRUE;
+ }
+ else if (task->from_envelope) {
+ user_field = task->from_envelope->addr;
+ len = task->from_envelope->addr_len;
+ }
+
+ if (!encoded) {
+ encoded = rspamd_url_encode(url, &enclen, task->task_pool);
+ }
+
+ msg_notice_task_encrypted("<%s> %s: %*s; ip: %s; URL: %*s",
+ MESSAGE_FIELD_CHECK(task, message_id),
+ has_user ? "user" : "from",
+ len, user_field,
+ rspamd_inet_address_to_string(task->from_addr),
+ (gint) enclen, encoded);
+ }
+}
+
+static ucl_object_t *
+rspamd_urls_tree_ucl(khash_t(rspamd_url_hash) * set,
+ struct rspamd_task *task)
+{
+ struct tree_cb_data cb;
+ ucl_object_t *obj;
+ struct rspamd_url *u;
+
+ obj = ucl_object_typed_new(UCL_ARRAY);
+ cb.top = obj;
+ cb.task = task;
+ cb.seen = kh_init(rspamd_url_host_hash);
+
+ kh_foreach_key(set, u, {
+ if (!(u->protocol & PROTOCOL_MAILTO)) {
+ urls_protocol_cb(u, &cb);
+ }
+ });
+
+ kh_destroy(rspamd_url_host_hash, cb.seen);
+
+ return obj;
+}
+
+static void
+emails_protocol_cb(struct rspamd_url *url, struct tree_cb_data *cb)
+{
+ ucl_object_t *obj;
+
+ if (url->userlen > 0 && url->hostlen > 0) {
+ obj = ucl_object_fromlstring(rspamd_url_user_unsafe(url),
+ url->userlen + url->hostlen + 1);
+ ucl_array_append(cb->top, obj);
+ }
+}
+
+static ucl_object_t *
+rspamd_emails_tree_ucl(khash_t(rspamd_url_hash) * set,
+ struct rspamd_task *task)
+{
+ struct tree_cb_data cb;
+ ucl_object_t *obj;
+ struct rspamd_url *u;
+
+ obj = ucl_object_typed_new(UCL_ARRAY);
+ cb.top = obj;
+ cb.task = task;
+
+ kh_foreach_key(set, u, {
+ if ((u->protocol & PROTOCOL_MAILTO)) {
+ emails_protocol_cb(u, &cb);
+ }
+ });
+
+
+ return obj;
+}
+
+
+/* Write new subject */
+static const gchar *
+rspamd_protocol_rewrite_subject(struct rspamd_task *task)
+{
+ GString *subj_buf;
+ gchar *res;
+ const gchar *s, *c, *p;
+ gsize slen = 0;
+
+ c = rspamd_mempool_get_variable(task->task_pool, "metric_subject");
+
+ if (c == NULL) {
+ c = task->cfg->subject;
+ }
+
+ if (c == NULL) {
+ c = SPAM_SUBJECT;
+ }
+
+ p = c;
+ s = MESSAGE_FIELD_CHECK(task, subject);
+
+ if (s) {
+ slen = strlen(s);
+ }
+
+ subj_buf = g_string_sized_new(strlen(c) + slen);
+
+ while (*p) {
+ if (*p == '%') {
+ switch (p[1]) {
+ case 's':
+ g_string_append_len(subj_buf, c, p - c);
+
+ if (s) {
+ g_string_append_len(subj_buf, s, slen);
+ }
+ c = p + 2;
+ p += 2;
+ break;
+ case 'd':
+ g_string_append_len(subj_buf, c, p - c);
+ rspamd_printf_gstring(subj_buf, "%.2f", task->result->score);
+ c = p + 2;
+ p += 2;
+ break;
+ case '%':
+ g_string_append_len(subj_buf, c, p - c);
+ g_string_append_c(subj_buf, '%');
+ c = p + 2;
+ p += 2;
+ break;
+ default:
+ p++; /* Just % something unknown */
+ break;
+ }
+ }
+ else {
+ p++;
+ }
+ }
+
+ if (p > c) {
+ g_string_append_len(subj_buf, c, p - c);
+ }
+
+ res = rspamd_mime_header_encode(subj_buf->str, subj_buf->len);
+
+ rspamd_mempool_add_destructor(task->task_pool,
+ (rspamd_mempool_destruct_t) g_free,
+ res);
+ g_string_free(subj_buf, TRUE);
+
+ return res;
+}
+
+static ucl_object_t *
+rspamd_metric_symbol_ucl(struct rspamd_task *task, struct rspamd_symbol_result *sym)
+{
+ ucl_object_t *obj = NULL, *ar;
+ const gchar *description = NULL;
+ struct rspamd_symbol_option *opt;
+
+ if (sym->sym != NULL) {
+ description = sym->sym->description;
+ }
+
+ obj = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(obj, ucl_object_fromstring(sym->name), "name", 0, false);
+ ucl_object_insert_key(obj, ucl_object_fromdouble(sym->score), "score", 0, false);
+
+ if (task->cmd == CMD_CHECK_V2) {
+ if (sym->sym) {
+ ucl_object_insert_key(obj, ucl_object_fromdouble(sym->sym->score), "metric_score", 0, false);
+ }
+ else {
+ ucl_object_insert_key(obj, ucl_object_fromdouble(0.0),
+ "metric_score", 0, false);
+ }
+ }
+
+ if (description) {
+ ucl_object_insert_key(obj, ucl_object_fromstring(description),
+ "description", 0, false);
+ }
+
+ if (sym->options != NULL) {
+ ar = ucl_object_typed_new(UCL_ARRAY);
+
+ DL_FOREACH(sym->opts_head, opt)
+ {
+ ucl_array_append(ar, ucl_object_fromstring_common(opt->option,
+ opt->optlen, 0));
+ }
+
+ ucl_object_insert_key(obj, ar, "options", 0, false);
+ }
+
+ return obj;
+}
+
+static ucl_object_t *
+rspamd_metric_group_ucl(struct rspamd_task *task,
+ struct rspamd_symbols_group *gr, gdouble score)
+{
+ ucl_object_t *obj = NULL;
+
+ obj = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(obj, ucl_object_fromdouble(score),
+ "score", 0, false);
+
+ if (gr->description) {
+ ucl_object_insert_key(obj, ucl_object_fromstring(gr->description),
+ "description", 0, false);
+ }
+
+ return obj;
+}
+
+static ucl_object_t *
+rspamd_scan_result_ucl(struct rspamd_task *task,
+ struct rspamd_scan_result *mres, ucl_object_t *top)
+{
+ struct rspamd_symbol_result *sym;
+ gboolean is_spam;
+ struct rspamd_action *action;
+ ucl_object_t *obj = NULL, *sobj;
+ const gchar *subject;
+ struct rspamd_passthrough_result *pr = NULL;
+
+ action = rspamd_check_action_metric(task, &pr, NULL);
+ is_spam = !(action->flags & RSPAMD_ACTION_HAM);
+
+ if (task->cmd == CMD_CHECK) {
+ obj = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(obj,
+ ucl_object_frombool(is_spam),
+ "is_spam", 0, false);
+ }
+ else {
+ obj = top;
+ }
+
+ if (pr) {
+ if (pr->message && !(pr->flags & RSPAMD_PASSTHROUGH_NO_SMTP_MESSAGE)) {
+ /* Add smtp message if it does not exist: see #3269 for details */
+ if (ucl_object_lookup(task->messages, "smtp_message") == NULL) {
+ ucl_object_insert_key(task->messages,
+ ucl_object_fromstring_common(pr->message, 0, UCL_STRING_RAW),
+ "smtp_message", 0,
+ false);
+ }
+ }
+
+ ucl_object_insert_key(obj,
+ ucl_object_fromstring(pr->module),
+ "passthrough_module", 0, false);
+ }
+
+ ucl_object_insert_key(obj,
+ ucl_object_frombool(RSPAMD_TASK_IS_SKIPPED(task)),
+ "is_skipped", 0, false);
+
+ if (!isnan(mres->score)) {
+ ucl_object_insert_key(obj, ucl_object_fromdouble(mres->score),
+ "score", 0, false);
+ }
+ else {
+ ucl_object_insert_key(obj,
+ ucl_object_fromdouble(0.0), "score", 0, false);
+ }
+
+ ucl_object_insert_key(obj,
+ ucl_object_fromdouble(rspamd_task_get_required_score(task, mres)),
+ "required_score", 0, false);
+ ucl_object_insert_key(obj,
+ ucl_object_fromstring(action->name),
+ "action", 0, false);
+
+ if (action->action_type == METRIC_ACTION_REWRITE_SUBJECT) {
+ subject = rspamd_protocol_rewrite_subject(task);
+
+ if (subject) {
+ ucl_object_insert_key(obj, ucl_object_fromstring(subject),
+ "subject", 0, false);
+ }
+ }
+ if (action->flags & RSPAMD_ACTION_MILTER) {
+ /* Treat milter action specially */
+ if (action->action_type == METRIC_ACTION_DISCARD) {
+ ucl_object_insert_key(obj, ucl_object_fromstring("discard"),
+ "reject", 0, false);
+ }
+ else if (action->action_type == METRIC_ACTION_QUARANTINE) {
+ ucl_object_insert_key(obj, ucl_object_fromstring("quarantine"),
+ "reject", 0, false);
+ }
+ }
+
+ /* Now handle symbols */
+ if (task->cmd != CMD_CHECK) {
+ /* Insert actions thresholds */
+ ucl_object_t *actions_obj = ucl_object_typed_new(UCL_OBJECT);
+
+ for (int i = task->result->nactions - 1; i >= 0; i--) {
+ struct rspamd_action_config *action_lim = &task->result->actions_config[i];
+
+ if (!isnan(action_lim->cur_limit) &&
+ !(action_lim->action->flags & (RSPAMD_ACTION_NO_THRESHOLD | RSPAMD_ACTION_HAM))) {
+ ucl_object_insert_key(actions_obj, ucl_object_fromdouble(action_lim->cur_limit),
+ action_lim->action->name, 0, true);
+ }
+ }
+
+ ucl_object_insert_key(obj, actions_obj, "thresholds", 0, false);
+
+ /* For checkv2 we insert symbols as a separate object */
+ obj = ucl_object_typed_new(UCL_OBJECT);
+ }
+
+ kh_foreach_value(mres->symbols, sym, {
+ if (!(sym->flags & RSPAMD_SYMBOL_RESULT_IGNORED)) {
+ sobj = rspamd_metric_symbol_ucl(task, sym);
+ ucl_object_insert_key(obj, sobj, sym->name, 0, false);
+ }
+ })
+
+ if (task->cmd != CMD_CHECK)
+ {
+ /* For checkv2 we insert symbols as a separate object */
+ ucl_object_insert_key(top, obj, "symbols", 0, false);
+ }
+ else
+ {
+ /* For legacy check we just insert it as "default" all together */
+ ucl_object_insert_key(top, obj, DEFAULT_METRIC, 0, false);
+ }
+
+ /* Handle groups if needed */
+ if (task->protocol_flags & RSPAMD_TASK_PROTOCOL_FLAG_GROUPS) {
+ struct rspamd_symbols_group *gr;
+ gdouble gr_score;
+
+ obj = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_reserve(obj, kh_size(mres->sym_groups));
+
+ kh_foreach(mres->sym_groups, gr, gr_score, {
+ if (task->cfg->public_groups_only &&
+ !(gr->flags & RSPAMD_SYMBOL_GROUP_PUBLIC)) {
+ continue;
+ }
+ sobj = rspamd_metric_group_ucl(task, gr, gr_score);
+ ucl_object_insert_key(obj, sobj, gr->name, 0, false);
+ });
+
+ ucl_object_insert_key(top, obj, "groups", 0, false);
+ }
+
+ return obj;
+}
+
+void rspamd_ucl_torspamc_output(const ucl_object_t *top,
+ rspamd_fstring_t **out)
+{
+ const ucl_object_t *symbols, *score,
+ *required_score, *is_spam, *elt, *cur;
+ ucl_object_iter_t iter = NULL;
+
+ score = ucl_object_lookup(top, "score");
+ required_score = ucl_object_lookup(top, "required_score");
+ is_spam = ucl_object_lookup(top, "is_spam");
+ rspamd_printf_fstring(out,
+ "Metric: default; %s; %.2f / %.2f / 0.0\r\n",
+ ucl_object_toboolean(is_spam) ? "True" : "False",
+ ucl_object_todouble(score),
+ ucl_object_todouble(required_score));
+ elt = ucl_object_lookup(top, "action");
+ if (elt != NULL) {
+ rspamd_printf_fstring(out, "Action: %s\r\n",
+ ucl_object_tostring(elt));
+ }
+
+ elt = ucl_object_lookup(top, "subject");
+ if (elt != NULL) {
+ rspamd_printf_fstring(out, "Subject: %s\r\n",
+ ucl_object_tostring(elt));
+ }
+
+ symbols = ucl_object_lookup(top, "symbols");
+
+ if (symbols != NULL) {
+ iter = NULL;
+ while ((elt = ucl_object_iterate(symbols, &iter, true)) != NULL) {
+ if (elt->type == UCL_OBJECT) {
+ const ucl_object_t *sym_score;
+ sym_score = ucl_object_lookup(elt, "score");
+ rspamd_printf_fstring(out, "Symbol: %s(%.2f)\r\n",
+ ucl_object_key(elt),
+ ucl_object_todouble(sym_score));
+ }
+ }
+ }
+
+ elt = ucl_object_lookup(top, "messages");
+ if (elt != NULL) {
+ iter = NULL;
+ while ((cur = ucl_object_iterate(elt, &iter, true)) != NULL) {
+ if (cur->type == UCL_STRING) {
+ rspamd_printf_fstring(out, "Message: %s\r\n",
+ ucl_object_tostring(cur));
+ }
+ }
+ }
+
+ elt = ucl_object_lookup(top, "message-id");
+ if (elt != NULL) {
+ rspamd_printf_fstring(out, "Message-ID: %s\r\n",
+ ucl_object_tostring(elt));
+ }
+}
+
+void rspamd_ucl_tospamc_output(const ucl_object_t *top,
+ rspamd_fstring_t **out)
+{
+ const ucl_object_t *symbols, *score,
+ *required_score, *is_spam, *elt;
+ ucl_object_iter_t iter = NULL;
+ rspamd_fstring_t *f;
+
+ score = ucl_object_lookup(top, "score");
+ required_score = ucl_object_lookup(top, "required_score");
+ is_spam = ucl_object_lookup(top, "is_spam");
+ rspamd_printf_fstring(out,
+ "Spam: %s ; %.2f / %.2f\r\n\r\n",
+ ucl_object_toboolean(is_spam) ? "True" : "False",
+ ucl_object_todouble(score),
+ ucl_object_todouble(required_score));
+
+ symbols = ucl_object_lookup(top, "symbols");
+
+ if (symbols != NULL) {
+ while ((elt = ucl_object_iterate(symbols, &iter, true)) != NULL) {
+ if (elt->type == UCL_OBJECT) {
+ rspamd_printf_fstring(out, "%s,",
+ ucl_object_key(elt));
+ }
+ }
+ /* Ugly hack, but the whole spamc is ugly */
+ f = *out;
+ if (f->str[f->len - 1] == ',') {
+ f->len--;
+
+ *out = rspamd_fstring_append(*out, CRLF, 2);
+ }
+ }
+}
+
+static void
+rspamd_protocol_output_profiling(struct rspamd_task *task,
+ ucl_object_t *top)
+{
+ GHashTable *tbl;
+ GHashTableIter it;
+ gpointer k, v;
+ ucl_object_t *prof;
+ gdouble val;
+
+ prof = ucl_object_typed_new(UCL_OBJECT);
+ tbl = rspamd_mempool_get_variable(task->task_pool, "profile");
+
+ if (tbl) {
+ g_hash_table_iter_init(&it, tbl);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ val = *(gdouble *) v;
+ ucl_object_insert_key(prof, ucl_object_fromdouble(val),
+ (const char *) k, 0, false);
+ }
+ }
+
+ ucl_object_insert_key(top, prof, "profile", 0, false);
+}
+
+ucl_object_t *
+rspamd_protocol_write_ucl(struct rspamd_task *task,
+ enum rspamd_protocol_flags flags)
+{
+ ucl_object_t *top = NULL;
+ GString *dkim_sig;
+ GList *dkim_sigs;
+ const ucl_object_t *milter_reply;
+
+ rspamd_task_set_finish_time(task);
+ top = ucl_object_typed_new(UCL_OBJECT);
+
+ rspamd_mempool_add_destructor(task->task_pool,
+ (rspamd_mempool_destruct_t) ucl_object_unref, top);
+
+ if (flags & RSPAMD_PROTOCOL_METRICS) {
+ rspamd_scan_result_ucl(task, task->result, top);
+ }
+
+ if (flags & RSPAMD_PROTOCOL_MESSAGES) {
+ if (G_UNLIKELY(task->cfg->compat_messages)) {
+ const ucl_object_t *cur;
+ ucl_object_t *msg_object;
+ ucl_object_iter_t iter = NULL;
+
+ msg_object = ucl_object_typed_new(UCL_ARRAY);
+
+ while ((cur = ucl_object_iterate(task->messages, &iter, true)) != NULL) {
+ if (cur->type == UCL_STRING) {
+ ucl_array_append(msg_object, ucl_object_ref(cur));
+ }
+ }
+
+ ucl_object_insert_key(top, msg_object, "messages", 0, false);
+ }
+ else {
+ ucl_object_insert_key(top, ucl_object_ref(task->messages),
+ "messages", 0, false);
+ }
+ }
+
+ if (flags & RSPAMD_PROTOCOL_URLS && task->message) {
+ if (kh_size(MESSAGE_FIELD(task, urls)) > 0) {
+ ucl_object_insert_key(top,
+ rspamd_urls_tree_ucl(MESSAGE_FIELD(task, urls), task),
+ "urls", 0, false);
+ ucl_object_insert_key(top,
+ rspamd_emails_tree_ucl(MESSAGE_FIELD(task, urls), task),
+ "emails", 0, false);
+ }
+ }
+
+ if (flags & RSPAMD_PROTOCOL_EXTRA) {
+ if (G_UNLIKELY(RSPAMD_TASK_IS_PROFILING(task))) {
+ rspamd_protocol_output_profiling(task, top);
+ }
+ }
+
+ if (flags & RSPAMD_PROTOCOL_BASIC) {
+ ucl_object_insert_key(top,
+ ucl_object_fromstring(MESSAGE_FIELD_CHECK(task, message_id)),
+ "message-id", 0, false);
+ ucl_object_insert_key(top,
+ ucl_object_fromdouble(task->time_real_finish - task->task_timestamp),
+ "time_real", 0, false);
+ }
+
+ if (flags & RSPAMD_PROTOCOL_DKIM) {
+ dkim_sigs = rspamd_mempool_get_variable(task->task_pool,
+ RSPAMD_MEMPOOL_DKIM_SIGNATURE);
+
+ if (dkim_sigs) {
+ if (dkim_sigs->next) {
+ /* Multiple DKIM signatures */
+ ucl_object_t *ar = ucl_object_typed_new(UCL_ARRAY);
+
+ for (; dkim_sigs != NULL; dkim_sigs = dkim_sigs->next) {
+ GString *folded_header;
+ dkim_sig = (GString *) dkim_sigs->data;
+
+ if (task->protocol_flags & RSPAMD_TASK_PROTOCOL_FLAG_MILTER ||
+ !task->message) {
+
+ folded_header = rspamd_header_value_fold(
+ "DKIM-Signature", strlen("DKIM-Signature"),
+ dkim_sig->str, dkim_sig->len,
+ 80, RSPAMD_TASK_NEWLINES_LF, NULL);
+ }
+ else {
+ folded_header = rspamd_header_value_fold(
+ "DKIM-Signature", strlen("DKIM-Signature"),
+ dkim_sig->str, dkim_sig->len,
+ 80,
+ MESSAGE_FIELD(task, nlines_type),
+ NULL);
+ }
+
+ ucl_array_append(ar,
+ ucl_object_fromstring_common(folded_header->str,
+ folded_header->len, UCL_STRING_RAW));
+ g_string_free(folded_header, TRUE);
+ }
+
+ ucl_object_insert_key(top,
+ ar,
+ "dkim-signature", 0,
+ false);
+ }
+ else {
+ /* Single DKIM signature */
+ GString *folded_header;
+ dkim_sig = (GString *) dkim_sigs->data;
+
+ if (task->protocol_flags & RSPAMD_TASK_PROTOCOL_FLAG_MILTER) {
+ folded_header = rspamd_header_value_fold(
+ "DKIM-Signature", strlen("DKIM-Signature"),
+ dkim_sig->str, dkim_sig->len,
+ 80, RSPAMD_TASK_NEWLINES_LF, NULL);
+ }
+ else {
+ folded_header = rspamd_header_value_fold(
+ "DKIM-Signature", strlen("DKIM-Signature"),
+ dkim_sig->str, dkim_sig->len,
+ 80, MESSAGE_FIELD(task, nlines_type),
+ NULL);
+ }
+
+ ucl_object_insert_key(top,
+ ucl_object_fromstring_common(folded_header->str,
+ folded_header->len, UCL_STRING_RAW),
+ "dkim-signature", 0, false);
+ g_string_free(folded_header, TRUE);
+ }
+ }
+ }
+
+ if (flags & RSPAMD_PROTOCOL_RMILTER) {
+ milter_reply = rspamd_mempool_get_variable(task->task_pool,
+ RSPAMD_MEMPOOL_MILTER_REPLY);
+
+ if (milter_reply) {
+ if (task->cmd != CMD_CHECK) {
+ ucl_object_insert_key(top, ucl_object_ref(milter_reply),
+ "milter", 0, false);
+ }
+ else {
+ ucl_object_insert_key(top, ucl_object_ref(milter_reply),
+ "rmilter", 0, false);
+ }
+ }
+ }
+
+ return top;
+}
+
+void rspamd_protocol_http_reply(struct rspamd_http_message *msg,
+ struct rspamd_task *task, ucl_object_t **pobj)
+{
+ struct rspamd_scan_result *metric_res;
+ const struct rspamd_re_cache_stat *restat;
+
+ ucl_object_t *top = NULL;
+ rspamd_fstring_t *reply;
+ gint flags = RSPAMD_PROTOCOL_DEFAULT;
+ struct rspamd_action *action;
+
+ /* Removed in 2.0 */
+#if 0
+ GHashTableIter hiter;
+ gpointer h, v;
+ /* Write custom headers */
+ g_hash_table_iter_init (&hiter, task->reply_headers);
+ while (g_hash_table_iter_next (&hiter, &h, &v)) {
+ rspamd_ftok_t *hn = h, *hv = v;
+
+ rspamd_http_message_add_header (msg, hn->begin, hv->begin);
+ }
+#endif
+
+ flags |= RSPAMD_PROTOCOL_URLS;
+
+ top = rspamd_protocol_write_ucl(task, flags);
+
+ if (pobj) {
+ *pobj = top;
+ }
+
+ if (!(task->flags & RSPAMD_TASK_FLAG_NO_LOG)) {
+ rspamd_roll_history_update(task->worker->srv->history, task);
+ }
+ else {
+ msg_debug_protocol("skip history update due to no log flag");
+ }
+
+ rspamd_task_write_log(task);
+
+ if (task->cfg->log_flags & RSPAMD_LOG_FLAG_RE_CACHE) {
+ restat = rspamd_re_cache_get_stat(task->re_rt);
+ g_assert(restat != NULL);
+ msg_notice_task(
+ "regexp statistics: %ud pcre regexps scanned, %ud regexps matched,"
+ " %ud regexps total, %ud regexps cached,"
+ " %HL scanned using pcre, %HL scanned total",
+ restat->regexp_checked,
+ restat->regexp_matched,
+ restat->regexp_total,
+ restat->regexp_fast_cached,
+ restat->bytes_scanned_pcre,
+ restat->bytes_scanned);
+ }
+
+ reply = rspamd_fstring_sized_new(1000);
+
+ if (msg->method < HTTP_SYMBOLS && !RSPAMD_TASK_IS_SPAMC(task)) {
+ msg_debug_protocol("writing json reply");
+ rspamd_ucl_emit_fstring(top, UCL_EMIT_JSON_COMPACT, &reply);
+ }
+ else {
+ if (RSPAMD_TASK_IS_SPAMC(task)) {
+ msg_debug_protocol("writing spamc legacy reply to client");
+ rspamd_ucl_tospamc_output(top, &reply);
+ }
+ else {
+ msg_debug_protocol("writing rspamc legacy reply to client");
+ rspamd_ucl_torspamc_output(top, &reply);
+ }
+ }
+
+ if (task->protocol_flags & RSPAMD_TASK_PROTOCOL_FLAG_BODY_BLOCK) {
+ /* Check if we need to insert a body block */
+ if (task->flags & RSPAMD_TASK_FLAG_MESSAGE_REWRITE) {
+ GString *hdr_offset = g_string_sized_new(30);
+
+ rspamd_printf_gstring(hdr_offset, "%z", RSPAMD_FSTRING_LEN(reply));
+ rspamd_http_message_add_header(msg, MESSAGE_OFFSET_HEADER,
+ hdr_offset->str);
+ msg_debug_protocol("write body block at position %s",
+ hdr_offset->str);
+ g_string_free(hdr_offset, TRUE);
+
+ /* In case of milter, we append just body, otherwise - full message */
+ if (task->protocol_flags & RSPAMD_TASK_PROTOCOL_FLAG_MILTER) {
+ const gchar *start;
+ goffset len, hdr_off;
+
+ start = task->msg.begin;
+ len = task->msg.len;
+
+ hdr_off = MESSAGE_FIELD(task, raw_headers_content).len;
+
+ if (hdr_off < len) {
+ start += hdr_off;
+ len -= hdr_off;
+
+ /* The problem here is that we need not end of headers, we need
+ * start of body.
+ *
+ * Hence, we need to skip one \r\n till there is anything else in
+ * a line.
+ */
+
+ if (*start == '\r' && len > 0) {
+ start++;
+ len--;
+ }
+
+ if (*start == '\n' && len > 0) {
+ start++;
+ len--;
+ }
+
+ msg_debug_protocol("milter version of body block size %d",
+ (int) len);
+ reply = rspamd_fstring_append(reply, start, len);
+ }
+ }
+ else {
+ msg_debug_protocol("general version of body block size %d",
+ (int) task->msg.len);
+ reply = rspamd_fstring_append(reply,
+ task->msg.begin, task->msg.len);
+ }
+ }
+ }
+
+ if ((task->protocol_flags & RSPAMD_TASK_PROTOCOL_FLAG_COMPRESSED) &&
+ rspamd_libs_reset_compression(task->cfg->libs_ctx)) {
+ /* We can compress output */
+ ZSTD_inBuffer zin;
+ ZSTD_outBuffer zout;
+ ZSTD_CStream *zstream;
+ rspamd_fstring_t *compressed_reply;
+ gsize r;
+
+ zstream = task->cfg->libs_ctx->out_zstream;
+ compressed_reply = rspamd_fstring_sized_new(ZSTD_compressBound(reply->len));
+ zin.pos = 0;
+ zin.src = reply->str;
+ zin.size = reply->len;
+ zout.pos = 0;
+ zout.dst = compressed_reply->str;
+ zout.size = compressed_reply->allocated;
+
+ while (zin.pos < zin.size) {
+ r = ZSTD_compressStream(zstream, &zout, &zin);
+
+ if (ZSTD_isError(r)) {
+ msg_err_protocol("cannot compress: %s", ZSTD_getErrorName(r));
+ rspamd_fstring_free(compressed_reply);
+ rspamd_http_message_set_body_from_fstring_steal(msg, reply);
+
+ goto end;
+ }
+ }
+
+ ZSTD_flushStream(zstream, &zout);
+ r = ZSTD_endStream(zstream, &zout);
+
+ if (ZSTD_isError(r)) {
+ msg_err_protocol("cannot finalize compress: %s", ZSTD_getErrorName(r));
+ rspamd_fstring_free(compressed_reply);
+ rspamd_http_message_set_body_from_fstring_steal(msg, reply);
+
+ goto end;
+ }
+
+ msg_info_protocol("writing compressed results: %z bytes before "
+ "%z bytes after",
+ zin.pos, zout.pos);
+ compressed_reply->len = zout.pos;
+ rspamd_fstring_free(reply);
+ rspamd_http_message_set_body_from_fstring_steal(msg, compressed_reply);
+ rspamd_http_message_add_header(msg, COMPRESSION_HEADER, "zstd");
+
+ if (task->cfg->libs_ctx->out_dict &&
+ task->cfg->libs_ctx->out_dict->id != 0) {
+ gchar dict_str[32];
+
+ rspamd_snprintf(dict_str, sizeof(dict_str), "%ud",
+ task->cfg->libs_ctx->out_dict->id);
+ rspamd_http_message_add_header(msg, "Dictionary", dict_str);
+ }
+ }
+ else {
+ rspamd_http_message_set_body_from_fstring_steal(msg, reply);
+ }
+
+end:
+ if (!(task->flags & RSPAMD_TASK_FLAG_NO_STAT)) {
+ /* Update stat for default metric */
+
+ msg_debug_protocol("skip stats update due to no_stat flag");
+ metric_res = task->result;
+
+ if (metric_res != NULL) {
+
+ action = rspamd_check_action_metric(task, NULL, NULL);
+ /* TODO: handle custom actions in stats */
+ if (action->action_type == METRIC_ACTION_SOFT_REJECT &&
+ (task->flags & RSPAMD_TASK_FLAG_GREYLISTED)) {
+ /* Set stat action to greylist to display greylisted messages */
+#ifndef HAVE_ATOMIC_BUILTINS
+ task->worker->srv->stat->actions_stat[METRIC_ACTION_GREYLIST]++;
+#else
+ __atomic_add_fetch(&task->worker->srv->stat->actions_stat[METRIC_ACTION_GREYLIST],
+ 1, __ATOMIC_RELEASE);
+#endif
+ }
+ else if (action->action_type < METRIC_ACTION_MAX) {
+#ifndef HAVE_ATOMIC_BUILTINS
+ task->worker->srv->stat->actions_stat[action->action_type]++;
+#else
+ __atomic_add_fetch(&task->worker->srv->stat->actions_stat[action->action_type],
+ 1, __ATOMIC_RELEASE);
+#endif
+ }
+ }
+
+ /* Increase counters */
+#ifndef HAVE_ATOMIC_BUILTINS
+ task->worker->srv->stat->messages_scanned++;
+#else
+ __atomic_add_fetch(&task->worker->srv->stat->messages_scanned,
+ 1, __ATOMIC_RELEASE);
+#endif
+
+ /* Set average processing time */
+ guint32 slot;
+ float processing_time = task->time_real_finish - task->task_timestamp;
+
+#ifndef HAVE_ATOMIC_BUILTINS
+ slot = task->worker->srv->stat->avg_time.cur_slot++;
+#else
+ slot = __atomic_fetch_add(&task->worker->srv->stat->avg_time.cur_slot,
+ 1, __ATOMIC_RELEASE);
+#endif
+ slot = slot % MAX_AVG_TIME_SLOTS;
+ /* TODO: this should be atomic but it is not supported in C */
+ task->worker->srv->stat->avg_time.avg_time[slot] = processing_time;
+ }
+}
+
+void rspamd_protocol_write_log_pipe(struct rspamd_task *task)
+{
+ struct rspamd_worker_log_pipe *lp;
+ struct rspamd_protocol_log_message_sum *ls;
+ lua_State *L = task->cfg->lua_state;
+ struct rspamd_scan_result *mres;
+ struct rspamd_symbol_result *sym;
+ gint id, i;
+ guint32 n = 0, nextra = 0;
+ gsize sz;
+ GArray *extra;
+ struct rspamd_protocol_log_symbol_result er;
+ struct rspamd_task **ptask;
+
+ /* Get extra results from lua plugins */
+ extra = g_array_new(FALSE, FALSE, sizeof(er));
+
+ lua_getglobal(L, "rspamd_plugins");
+ if (lua_istable(L, -1)) {
+ lua_pushnil(L);
+
+ while (lua_next(L, -2)) {
+ if (lua_istable(L, -1)) {
+ lua_pushvalue(L, -2);
+ /* stack:
+ * -1: copy of key
+ * -2: value (module table)
+ * -3: key (module name)
+ * -4: global
+ */
+ lua_pushstring(L, "log_callback");
+ lua_gettable(L, -3);
+ /* stack:
+ * -1: func
+ * -2: copy of key
+ * -3: value (module table)
+ * -3: key (module name)
+ * -4: global
+ */
+ if (lua_isfunction(L, -1)) {
+ ptask = lua_newuserdata(L, sizeof(*ptask));
+ *ptask = task;
+ rspamd_lua_setclass(L, "rspamd{task}", -1);
+ /* stack:
+ * -1: task
+ * -2: func
+ * -3: key copy
+ * -4: value (module table)
+ * -5: key (module name)
+ * -6: global
+ */
+ msg_debug_protocol("calling for %s", lua_tostring(L, -3));
+ if (lua_pcall(L, 1, 1, 0) != 0) {
+ msg_info_protocol("call to log callback %s failed: %s",
+ lua_tostring(L, -2), lua_tostring(L, -1));
+ lua_pop(L, 1);
+ /* stack:
+ * -1: key copy
+ * -2: value
+ * -3: key
+ */
+ }
+ else {
+ /* stack:
+ * -1: result
+ * -2: key copy
+ * -3: value
+ * -4: key
+ */
+ if (lua_istable(L, -1)) {
+ /* Another iteration */
+ lua_pushnil(L);
+
+ while (lua_next(L, -2)) {
+ /* stack:
+ * -1: value
+ * -2: key
+ * -3: result table (pcall)
+ * -4: key copy (parent)
+ * -5: value (parent)
+ * -6: key (parent)
+ */
+ if (lua_istable(L, -1)) {
+ er.id = 0;
+ er.score = 0.0;
+
+ lua_rawgeti(L, -1, 1);
+ if (lua_isnumber(L, -1)) {
+ er.id = lua_tonumber(L, -1);
+ }
+ lua_rawgeti(L, -2, 2);
+ if (lua_isnumber(L, -1)) {
+ er.score = lua_tonumber(L, -1);
+ }
+ /* stack:
+ * -1: value[2]
+ * -2: value[1]
+ * -3: values
+ * -4: key
+ * -5: result table (pcall)
+ * -6: key copy (parent)
+ * -7: value (parent)
+ * -8: key (parent)
+ */
+ lua_pop(L, 2); /* Values */
+ g_array_append_val(extra, er);
+ }
+
+ lua_pop(L, 1); /* Value for lua_next */
+ }
+
+ lua_pop(L, 1); /* Table result of pcall */
+ }
+ else {
+ msg_info_protocol("call to log callback %s returned "
+ "wrong type: %s",
+ lua_tostring(L, -2),
+ lua_typename(L, lua_type(L, -1)));
+ lua_pop(L, 1); /* Returned error */
+ }
+ }
+ }
+ else {
+ lua_pop(L, 1);
+ /* stack:
+ * -1: key copy
+ * -2: value
+ * -3: key
+ */
+ }
+ }
+
+ lua_pop(L, 2); /* Top table + key copy */
+ }
+
+ lua_pop(L, 1); /* rspamd_plugins global */
+ }
+ else {
+ lua_pop(L, 1);
+ }
+
+ nextra = extra->len;
+
+ LL_FOREACH(task->cfg->log_pipes, lp)
+ {
+ if (lp->fd != -1) {
+ switch (lp->type) {
+ case RSPAMD_LOG_PIPE_SYMBOLS:
+ mres = task->result;
+
+ if (mres) {
+ n = kh_size(mres->symbols);
+ sz = sizeof(*ls) +
+ sizeof(struct rspamd_protocol_log_symbol_result) *
+ (n + nextra);
+ ls = g_malloc0(sz);
+
+ /* Handle settings id */
+
+ if (task->settings_elt) {
+ ls->settings_id = task->settings_elt->id;
+ }
+ else {
+ ls->settings_id = 0;
+ }
+
+ ls->score = mres->score;
+ ls->required_score = rspamd_task_get_required_score(task,
+ mres);
+ ls->nresults = n;
+ ls->nextra = nextra;
+
+ i = 0;
+
+ kh_foreach_value(mres->symbols, sym, {
+ id = rspamd_symcache_find_symbol(task->cfg->cache,
+ sym->name);
+
+ if (id >= 0) {
+ ls->results[i].id = id;
+ ls->results[i].score = sym->score;
+ }
+ else {
+ ls->results[i].id = -1;
+ ls->results[i].score = 0.0;
+ }
+
+ i++;
+ });
+
+ memcpy(&ls->results[n], extra->data, nextra * sizeof(er));
+ }
+ else {
+ sz = sizeof(*ls);
+ ls = g_malloc0(sz);
+ ls->nresults = 0;
+ }
+
+ /* We don't really care about return value here */
+ if (write(lp->fd, ls, sz) == -1) {
+ msg_info_protocol("cannot write to log pipe: %s",
+ strerror(errno));
+ }
+
+ g_free(ls);
+ break;
+ default:
+ msg_err_protocol("unknown log format %d", lp->type);
+ break;
+ }
+ }
+ }
+
+ g_array_free(extra, TRUE);
+}
+
+void rspamd_protocol_write_reply(struct rspamd_task *task, ev_tstamp timeout)
+{
+ struct rspamd_http_message *msg;
+ const gchar *ctype = "application/json";
+ rspamd_fstring_t *reply;
+
+ msg = rspamd_http_new_message(HTTP_RESPONSE);
+
+ if (rspamd_http_connection_is_encrypted(task->http_conn)) {
+ msg_info_protocol("<%s> writing encrypted reply",
+ MESSAGE_FIELD_CHECK(task, message_id));
+ }
+
+ /* Compatibility */
+ if (task->cmd == CMD_CHECK_RSPAMC) {
+ msg->method = HTTP_SYMBOLS;
+ }
+ else if (task->cmd == CMD_CHECK_SPAMC) {
+ msg->method = HTTP_SYMBOLS;
+ msg->flags |= RSPAMD_HTTP_FLAG_SPAMC;
+ }
+
+ if (task->err != NULL) {
+ msg_debug_protocol("writing error reply to client");
+ ucl_object_t *top = NULL;
+
+ top = ucl_object_typed_new(UCL_OBJECT);
+ msg->code = 500 + task->err->code % 100;
+ msg->status = rspamd_fstring_new_init(task->err->message,
+ strlen(task->err->message));
+ ucl_object_insert_key(top, ucl_object_fromstring(task->err->message),
+ "error", 0, false);
+ ucl_object_insert_key(top,
+ ucl_object_fromstring(g_quark_to_string(task->err->domain)),
+ "error_domain", 0, false);
+ reply = rspamd_fstring_sized_new(256);
+ rspamd_ucl_emit_fstring(top, UCL_EMIT_JSON_COMPACT, &reply);
+ ucl_object_unref(top);
+
+ /* We also need to validate utf8 */
+ if (rspamd_fast_utf8_validate(reply->str, reply->len) != 0) {
+ gsize valid_len;
+ gchar *validated;
+
+ /* We copy reply several times here but it should be a rare case */
+ validated = rspamd_str_make_utf_valid(reply->str, reply->len,
+ &valid_len, task->task_pool);
+ rspamd_http_message_set_body(msg, validated, valid_len);
+ rspamd_fstring_free(reply);
+ }
+ else {
+ rspamd_http_message_set_body_from_fstring_steal(msg, reply);
+ }
+ }
+ else {
+ msg->status = rspamd_fstring_new_init("OK", 2);
+
+ switch (task->cmd) {
+ case CMD_CHECK:
+ case CMD_CHECK_RSPAMC:
+ case CMD_CHECK_SPAMC:
+ case CMD_SKIP:
+ case CMD_CHECK_V2:
+ rspamd_protocol_http_reply(msg, task, NULL);
+ rspamd_protocol_write_log_pipe(task);
+ break;
+ case CMD_PING:
+ msg_debug_protocol("writing pong to client");
+ rspamd_http_message_set_body(msg, "pong" CRLF, 6);
+ ctype = "text/plain";
+ break;
+ default:
+ msg_err_protocol("BROKEN");
+ break;
+ }
+ }
+
+ ev_now_update(task->event_loop);
+ msg->date = ev_time();
+
+ rspamd_http_connection_reset(task->http_conn);
+ rspamd_http_connection_write_message(task->http_conn, msg, NULL,
+ ctype, task, timeout);
+
+ task->processed_stages |= RSPAMD_TASK_STAGE_REPLIED;
+}
diff --git a/src/libserver/protocol.h b/src/libserver/protocol.h
new file mode 100644
index 0000000..0e3c187
--- /dev/null
+++ b/src/libserver/protocol.h
@@ -0,0 +1,130 @@
+/**
+ * @file protocol.h
+ * Rspamd protocol definition
+ */
+
+#ifndef RSPAMD_PROTOCOL_H
+#define RSPAMD_PROTOCOL_H
+
+#include "config.h"
+#include "scan_result.h"
+#include "libserver/http/http_connection.h"
+#include "task.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define RSPAMD_BASE_ERROR 500
+#define RSPAMD_FILTER_ERROR RSPAMD_BASE_ERROR + 1
+#define RSPAMD_NETWORK_ERROR RSPAMD_BASE_ERROR + 2
+#define RSPAMD_PROTOCOL_ERROR RSPAMD_BASE_ERROR + 3
+#define RSPAMD_LENGTH_ERROR RSPAMD_BASE_ERROR + 4
+#define RSPAMD_STATFILE_ERROR RSPAMD_BASE_ERROR + 5
+
+struct rspamd_protocol_log_symbol_result {
+ guint32 id;
+ float score;
+};
+struct rspamd_protocol_log_message_sum {
+ guint32 nresults;
+ guint32 nextra;
+ guint32 settings_id;
+ gdouble score;
+ gdouble required_score;
+ struct rspamd_protocol_log_symbol_result results[];
+};
+
+struct rspamd_metric;
+
+/**
+ * Process headers into HTTP message and set appropriate task fields
+ * @param task
+ * @param msg
+ * @return
+ */
+gboolean rspamd_protocol_handle_headers(struct rspamd_task *task,
+ struct rspamd_http_message *msg);
+
+/**
+ * Process control chunk and update task structure accordingly
+ * @param task
+ * @param control
+ * @return
+ */
+gboolean rspamd_protocol_handle_control(struct rspamd_task *task,
+ const ucl_object_t *control);
+
+/**
+ * Process HTTP request to the task structure
+ * @param task
+ * @param msg
+ * @return
+ */
+gboolean rspamd_protocol_handle_request(struct rspamd_task *task,
+ struct rspamd_http_message *msg);
+
+/**
+ * Write task results to http message
+ * @param msg
+ * @param task
+ */
+void rspamd_protocol_http_reply(struct rspamd_http_message *msg,
+ struct rspamd_task *task, ucl_object_t **pobj);
+
+/**
+ * Write data to log pipes
+ * @param task
+ */
+void rspamd_protocol_write_log_pipe(struct rspamd_task *task);
+
+enum rspamd_protocol_flags {
+ RSPAMD_PROTOCOL_BASIC = 1 << 0,
+ RSPAMD_PROTOCOL_METRICS = 1 << 1,
+ RSPAMD_PROTOCOL_MESSAGES = 1 << 2,
+ RSPAMD_PROTOCOL_RMILTER = 1 << 3,
+ RSPAMD_PROTOCOL_DKIM = 1 << 4,
+ RSPAMD_PROTOCOL_URLS = 1 << 5,
+ RSPAMD_PROTOCOL_EXTRA = 1 << 6,
+};
+
+#define RSPAMD_PROTOCOL_DEFAULT (RSPAMD_PROTOCOL_BASIC | \
+ RSPAMD_PROTOCOL_METRICS | \
+ RSPAMD_PROTOCOL_MESSAGES | \
+ RSPAMD_PROTOCOL_RMILTER | \
+ RSPAMD_PROTOCOL_DKIM | \
+ RSPAMD_PROTOCOL_EXTRA)
+
+/**
+ * Write reply to ucl object filling log buffer
+ * @param task
+ * @param logbuf
+ * @return
+ */
+ucl_object_t *rspamd_protocol_write_ucl(struct rspamd_task *task,
+ enum rspamd_protocol_flags flags);
+
+/**
+ * Write reply for specified task command
+ * @param task task object
+ * @return 0 if we wrote reply and -1 if there was some error
+ */
+void rspamd_protocol_write_reply(struct rspamd_task *task, ev_tstamp timeout);
+
+/**
+ * Convert rspamd output to legacy protocol reply
+ * @param task
+ * @param top
+ * @param out
+ */
+void rspamd_ucl_torspamc_output(const ucl_object_t *top,
+ rspamd_fstring_t **out);
+
+void rspamd_ucl_tospamc_output(const ucl_object_t *top,
+ rspamd_fstring_t **out);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/protocol_internal.h b/src/libserver/protocol_internal.h
new file mode 100644
index 0000000..c604e96
--- /dev/null
+++ b/src/libserver/protocol_internal.h
@@ -0,0 +1,99 @@
+/*-
+ * Copyright 2017 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_PROTOCOL_INTERNAL_H
+#define RSPAMD_PROTOCOL_INTERNAL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Just check if the passed message is spam or not and reply as
+ * described below
+ */
+#define MSG_CMD_CHECK "check"
+
+/*
+ * Modern check version
+ */
+#define MSG_CMD_CHECK_V2 "checkv2"
+#define MSG_CMD_SCAN "scan"
+
+/*
+ * Check if message is spam or not, and return score plus list
+ * of symbols hit
+ */
+#define MSG_CMD_SYMBOLS "symbols"
+/*
+ * Check if message is spam or not, and return score plus report
+ */
+#define MSG_CMD_REPORT "report"
+/*
+ * Check if message is spam or not, and return score plus report
+ * if the message is spam
+ */
+#define MSG_CMD_REPORT_IFSPAM "report_ifspam"
+/*
+ * Ignore this message -- client opened connection then changed
+ */
+#define MSG_CMD_SKIP "skip"
+/*
+ * Return a confirmation that spamd is alive
+ */
+#define MSG_CMD_PING "ping"
+/*
+ * Process this message as described above and return modified message
+ */
+#define MSG_CMD_PROCESS "process"
+/*
+ * Headers
+ */
+#define HELO_HEADER "Helo"
+#define FROM_HEADER "From"
+#define IP_ADDR_HEADER "IP"
+#define RCPT_HEADER "Rcpt"
+#define SUBJECT_HEADER "Subject"
+#define SETTINGS_ID_HEADER "Settings-ID"
+#define SETTINGS_HEADER "Settings"
+#define QUEUE_ID_HEADER "Queue-ID"
+#define USER_HEADER "User"
+#define URLS_HEADER "URL-Format"
+#define PASS_HEADER "Pass"
+#define HOSTNAME_HEADER "Hostname"
+#define DELIVER_TO_HEADER "Deliver-To"
+#define NO_LOG_HEADER "Log"
+#define MLEN_HEADER "Message-Length"
+#define USER_AGENT_HEADER "User-Agent"
+#define MTA_TAG_HEADER "MTA-Tag"
+#define PROFILE_HEADER "Profile"
+#define TLS_CIPHER_HEADER "TLS-Cipher"
+#define TLS_VERSION_HEADER "TLS-Version"
+#define MTA_NAME_HEADER "MTA-Name"
+#define MILTER_HEADER "Milter"
+#define FILENAME_HEADER "Filename"
+#define FLAGS_HEADER "Flags"
+#define CERT_ISSUER_HEADER "TLS-Cert-Issuer"
+#define MAILER_HEADER "Mailer"
+#define RAW_DATA_HEADER "Raw"
+#define COMPRESSION_HEADER "Compression"
+#define MESSAGE_OFFSET_HEADER "Message-Offset"
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif//RSPAMD_PROTOCOL_INTERNAL_H
diff --git a/src/libserver/re_cache.c b/src/libserver/re_cache.c
new file mode 100644
index 0000000..d51dba6
--- /dev/null
+++ b/src/libserver/re_cache.c
@@ -0,0 +1,2712 @@
+/*
+ * Copyright 2024 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 "libmime/message.h"
+#include "re_cache.h"
+#include "cryptobox.h"
+#include "ref.h"
+#include "libserver/url.h"
+#include "libserver/task.h"
+#include "libserver/cfg_file.h"
+#include "libutil/util.h"
+#include "libutil/regexp.h"
+#include "lua/lua_common.h"
+#include "libstat/stat_api.h"
+#include "contrib/uthash/utlist.h"
+
+#include "khash.h"
+
+#ifdef WITH_HYPERSCAN
+#include "hs.h"
+#include "hyperscan_tools.h"
+#endif
+
+#include "unix-std.h"
+#include <signal.h>
+#include <stdalign.h>
+#include <math.h>
+#include "contrib/libev/ev.h"
+
+#ifndef WITH_PCRE2
+#include <pcre.h>
+#else
+#include <pcre2.h>
+#endif
+
+#include "contrib/fastutf8/fastutf8.h"
+
+#ifdef HAVE_SYS_WAIT_H
+#include <sys/wait.h>
+#endif
+
+#define msg_err_re_cache(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "re_cache", cache->hash, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_re_cache(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ "re_cache", cache->hash, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_re_cache(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ "re_cache", cache->hash, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+#define msg_debug_re_task(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_re_cache_log_id, "re_cache", task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_re_cache(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_re_cache_log_id, "re_cache", cache->hash, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+INIT_LOG_MODULE(re_cache)
+
+#ifdef WITH_HYPERSCAN
+#define RSPAMD_HS_MAGIC_LEN (sizeof(rspamd_hs_magic))
+static const guchar rspamd_hs_magic[] = {'r', 's', 'h', 's', 'r', 'e', '1', '1'},
+ rspamd_hs_magic_vector[] = {'r', 's', 'h', 's', 'r', 'v', '1', '1'};
+#endif
+
+
+struct rspamd_re_class {
+ guint64 id;
+ enum rspamd_re_type type;
+ gboolean has_utf8; /* if there are any utf8 regexps */
+ gpointer type_data;
+ gsize type_len;
+ GHashTable *re;
+ rspamd_cryptobox_hash_state_t *st;
+
+ gchar hash[rspamd_cryptobox_HASHBYTES + 1];
+
+#ifdef WITH_HYPERSCAN
+ rspamd_hyperscan_t *hs_db;
+ hs_scratch_t *hs_scratch;
+ gint *hs_ids;
+ guint nhs;
+#endif
+};
+
+enum rspamd_re_cache_elt_match_type {
+ RSPAMD_RE_CACHE_PCRE = 0,
+ RSPAMD_RE_CACHE_HYPERSCAN,
+ RSPAMD_RE_CACHE_HYPERSCAN_PRE
+};
+
+struct rspamd_re_cache_elt {
+ rspamd_regexp_t *re;
+ gint lua_cbref;
+ enum rspamd_re_cache_elt_match_type match_type;
+};
+
+KHASH_INIT(lua_selectors_hash, gchar *, int, 1, kh_str_hash_func, kh_str_hash_equal);
+
+struct rspamd_re_cache {
+ GHashTable *re_classes;
+
+ GPtrArray *re;
+ khash_t(lua_selectors_hash) * selectors;
+ ref_entry_t ref;
+ guint nre;
+ guint max_re_data;
+ gchar hash[rspamd_cryptobox_HASHBYTES + 1];
+ lua_State *L;
+#ifdef WITH_HYPERSCAN
+ enum rspamd_hyperscan_status hyperscan_loaded;
+ gboolean disable_hyperscan;
+ hs_platform_info_t plt;
+#endif
+};
+
+struct rspamd_re_selector_result {
+ guchar **scvec;
+ guint *lenvec;
+ guint cnt;
+};
+
+KHASH_INIT(selectors_results_hash, int, struct rspamd_re_selector_result, 1,
+ kh_int_hash_func, kh_int_hash_equal);
+
+struct rspamd_re_runtime {
+ guchar *checked;
+ guchar *results;
+ khash_t(selectors_results_hash) * sel_cache;
+ struct rspamd_re_cache *cache;
+ struct rspamd_re_cache_stat stat;
+ gboolean has_hs;
+};
+
+static GQuark
+rspamd_re_cache_quark(void)
+{
+ return g_quark_from_static_string("re_cache");
+}
+
+static guint64
+rspamd_re_cache_class_id(enum rspamd_re_type type,
+ gconstpointer type_data,
+ gsize datalen)
+{
+ rspamd_cryptobox_fast_hash_state_t st;
+
+ rspamd_cryptobox_fast_hash_init(&st, 0xdeadbabe);
+ rspamd_cryptobox_fast_hash_update(&st, &type, sizeof(type));
+
+ if (datalen > 0) {
+ rspamd_cryptobox_fast_hash_update(&st, type_data, datalen);
+ }
+
+ return rspamd_cryptobox_fast_hash_final(&st);
+}
+
+static void
+rspamd_re_cache_destroy(struct rspamd_re_cache *cache)
+{
+ GHashTableIter it;
+ gpointer k, v;
+ struct rspamd_re_class *re_class;
+ gchar *skey;
+ gint sref;
+
+ g_assert(cache != NULL);
+ g_hash_table_iter_init(&it, cache->re_classes);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ re_class = v;
+ g_hash_table_iter_steal(&it);
+ g_hash_table_unref(re_class->re);
+
+ if (re_class->type_data) {
+ g_free(re_class->type_data);
+ }
+
+#ifdef WITH_HYPERSCAN
+ if (re_class->hs_db) {
+ rspamd_hyperscan_free(re_class->hs_db, false);
+ }
+ if (re_class->hs_scratch) {
+ hs_free_scratch(re_class->hs_scratch);
+ }
+ if (re_class->hs_ids) {
+ g_free(re_class->hs_ids);
+ }
+#endif
+ g_free(re_class);
+ }
+
+ if (cache->L) {
+ kh_foreach(cache->selectors, skey, sref, {
+ luaL_unref(cache->L, LUA_REGISTRYINDEX, sref);
+ g_free(skey);
+ });
+
+ struct rspamd_re_cache_elt *elt;
+ guint i;
+
+ PTR_ARRAY_FOREACH(cache->re, i, elt)
+ {
+ if (elt->lua_cbref != -1) {
+ luaL_unref(cache->L, LUA_REGISTRYINDEX, elt->lua_cbref);
+ }
+ }
+ }
+
+ kh_destroy(lua_selectors_hash, cache->selectors);
+
+ g_hash_table_unref(cache->re_classes);
+ g_ptr_array_free(cache->re, TRUE);
+ g_free(cache);
+}
+
+static void
+rspamd_re_cache_elt_dtor(gpointer e)
+{
+ struct rspamd_re_cache_elt *elt = e;
+
+ rspamd_regexp_unref(elt->re);
+ g_free(elt);
+}
+
+struct rspamd_re_cache *
+rspamd_re_cache_new(void)
+{
+ struct rspamd_re_cache *cache;
+
+ cache = g_malloc0(sizeof(*cache));
+ cache->re_classes = g_hash_table_new(g_int64_hash, g_int64_equal);
+ cache->nre = 0;
+ cache->re = g_ptr_array_new_full(256, rspamd_re_cache_elt_dtor);
+ cache->selectors = kh_init(lua_selectors_hash);
+#ifdef WITH_HYPERSCAN
+ cache->hyperscan_loaded = RSPAMD_HYPERSCAN_UNKNOWN;
+#endif
+ REF_INIT_RETAIN(cache, rspamd_re_cache_destroy);
+
+ return cache;
+}
+
+enum rspamd_hyperscan_status
+rspamd_re_cache_is_hs_loaded(struct rspamd_re_cache *cache)
+{
+ g_assert(cache != NULL);
+
+#ifdef WITH_HYPERSCAN
+ return cache->hyperscan_loaded;
+#else
+ return RSPAMD_HYPERSCAN_UNSUPPORTED;
+#endif
+}
+
+rspamd_regexp_t *
+rspamd_re_cache_add(struct rspamd_re_cache *cache,
+ rspamd_regexp_t *re,
+ enum rspamd_re_type type,
+ gconstpointer type_data, gsize datalen,
+ gint lua_cbref)
+{
+ guint64 class_id;
+ struct rspamd_re_class *re_class;
+ rspamd_regexp_t *nre;
+ struct rspamd_re_cache_elt *elt;
+
+ g_assert(cache != NULL);
+ g_assert(re != NULL);
+
+ class_id = rspamd_re_cache_class_id(type, type_data, datalen);
+ re_class = g_hash_table_lookup(cache->re_classes, &class_id);
+
+ if (re_class == NULL) {
+ re_class = g_malloc0(sizeof(*re_class));
+ re_class->id = class_id;
+ re_class->type_len = datalen;
+ re_class->type = type;
+ re_class->re = g_hash_table_new_full(rspamd_regexp_hash,
+ rspamd_regexp_equal, NULL, (GDestroyNotify) rspamd_regexp_unref);
+
+ if (datalen > 0) {
+ re_class->type_data = g_malloc0(datalen);
+ memcpy(re_class->type_data, type_data, datalen);
+ }
+
+ g_hash_table_insert(cache->re_classes, &re_class->id, re_class);
+ }
+
+ if ((nre = g_hash_table_lookup(re_class->re, rspamd_regexp_get_id(re))) == NULL) {
+ /*
+ * We set re id based on the global position in the cache
+ */
+ elt = g_malloc0(sizeof(*elt));
+ /* One ref for re_class */
+ nre = rspamd_regexp_ref(re);
+ rspamd_regexp_set_cache_id(re, cache->nre++);
+ /* One ref for cache */
+ elt->re = rspamd_regexp_ref(re);
+ g_ptr_array_add(cache->re, elt);
+ rspamd_regexp_set_class(re, re_class);
+ elt->lua_cbref = lua_cbref;
+
+ g_hash_table_insert(re_class->re, rspamd_regexp_get_id(nre), nre);
+ }
+
+ if (rspamd_regexp_get_flags(re) & RSPAMD_REGEXP_FLAG_UTF) {
+ re_class->has_utf8 = TRUE;
+ }
+
+ return nre;
+}
+
+void rspamd_re_cache_replace(struct rspamd_re_cache *cache,
+ rspamd_regexp_t *what,
+ rspamd_regexp_t *with)
+{
+ guint64 re_id;
+ struct rspamd_re_class *re_class;
+ rspamd_regexp_t *src;
+ struct rspamd_re_cache_elt *elt;
+
+ g_assert(cache != NULL);
+ g_assert(what != NULL);
+ g_assert(with != NULL);
+
+ re_class = rspamd_regexp_get_class(what);
+
+ if (re_class != NULL) {
+ re_id = rspamd_regexp_get_cache_id(what);
+
+ g_assert(re_id != RSPAMD_INVALID_ID);
+ src = g_hash_table_lookup(re_class->re, rspamd_regexp_get_id(what));
+ elt = g_ptr_array_index(cache->re, re_id);
+ g_assert(elt != NULL);
+ g_assert(src != NULL);
+
+ rspamd_regexp_set_cache_id(what, RSPAMD_INVALID_ID);
+ rspamd_regexp_set_class(what, NULL);
+ rspamd_regexp_set_cache_id(with, re_id);
+ rspamd_regexp_set_class(with, re_class);
+ /*
+ * On calling of this function, we actually unref old re (what)
+ */
+ g_hash_table_insert(re_class->re,
+ rspamd_regexp_get_id(what),
+ rspamd_regexp_ref(with));
+
+ rspamd_regexp_unref(elt->re);
+ elt->re = rspamd_regexp_ref(with);
+ /* XXX: do not touch match type here */
+ }
+}
+
+static gint
+rspamd_re_cache_sort_func(gconstpointer a, gconstpointer b)
+{
+ struct rspamd_re_cache_elt *const *re1 = a, *const *re2 = b;
+
+ return rspamd_regexp_cmp(rspamd_regexp_get_id((*re1)->re),
+ rspamd_regexp_get_id((*re2)->re));
+}
+
+void rspamd_re_cache_init(struct rspamd_re_cache *cache, struct rspamd_config *cfg)
+{
+ guint i, fl;
+ GHashTableIter it;
+ gpointer k, v;
+ struct rspamd_re_class *re_class;
+ rspamd_cryptobox_hash_state_t st_global;
+ rspamd_regexp_t *re;
+ struct rspamd_re_cache_elt *elt;
+ guchar hash_out[rspamd_cryptobox_HASHBYTES];
+
+ g_assert(cache != NULL);
+
+ rspamd_cryptobox_hash_init(&st_global, NULL, 0);
+ /* Resort all regexps */
+ g_ptr_array_sort(cache->re, rspamd_re_cache_sort_func);
+
+ for (i = 0; i < cache->re->len; i++) {
+ elt = g_ptr_array_index(cache->re, i);
+ re = elt->re;
+ re_class = rspamd_regexp_get_class(re);
+ g_assert(re_class != NULL);
+ rspamd_regexp_set_cache_id(re, i);
+
+ if (re_class->st == NULL) {
+ (void) !posix_memalign((void **) &re_class->st, RSPAMD_ALIGNOF(rspamd_cryptobox_hash_state_t),
+ sizeof(*re_class->st));
+ g_assert(re_class->st != NULL);
+ rspamd_cryptobox_hash_init(re_class->st, NULL, 0);
+ }
+
+ /* Update hashes */
+ /* Id of re class */
+ rspamd_cryptobox_hash_update(re_class->st, (gpointer) &re_class->id,
+ sizeof(re_class->id));
+ rspamd_cryptobox_hash_update(&st_global, (gpointer) &re_class->id,
+ sizeof(re_class->id));
+ /* Id of re expression */
+ rspamd_cryptobox_hash_update(re_class->st, rspamd_regexp_get_id(re),
+ rspamd_cryptobox_HASHBYTES);
+ rspamd_cryptobox_hash_update(&st_global, rspamd_regexp_get_id(re),
+ rspamd_cryptobox_HASHBYTES);
+ /* PCRE flags */
+ fl = rspamd_regexp_get_pcre_flags(re);
+ rspamd_cryptobox_hash_update(re_class->st, (const guchar *) &fl,
+ sizeof(fl));
+ rspamd_cryptobox_hash_update(&st_global, (const guchar *) &fl,
+ sizeof(fl));
+ /* Rspamd flags */
+ fl = rspamd_regexp_get_flags(re);
+ rspamd_cryptobox_hash_update(re_class->st, (const guchar *) &fl,
+ sizeof(fl));
+ rspamd_cryptobox_hash_update(&st_global, (const guchar *) &fl,
+ sizeof(fl));
+ /* Limit of hits */
+ fl = rspamd_regexp_get_maxhits(re);
+ rspamd_cryptobox_hash_update(re_class->st, (const guchar *) &fl,
+ sizeof(fl));
+ rspamd_cryptobox_hash_update(&st_global, (const guchar *) &fl,
+ sizeof(fl));
+ /* Numeric order */
+ rspamd_cryptobox_hash_update(re_class->st, (const guchar *) &i,
+ sizeof(i));
+ rspamd_cryptobox_hash_update(&st_global, (const guchar *) &i,
+ sizeof(i));
+ }
+
+ rspamd_cryptobox_hash_final(&st_global, hash_out);
+ rspamd_snprintf(cache->hash, sizeof(cache->hash), "%*xs",
+ (gint) rspamd_cryptobox_HASHBYTES, hash_out);
+
+ /* Now finalize all classes */
+ g_hash_table_iter_init(&it, cache->re_classes);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ re_class = v;
+
+ if (re_class->st) {
+ /*
+ * We finally update all classes with the number of expressions
+ * in the cache to ensure that if even a single re has been changed
+ * we won't be broken due to id mismatch
+ */
+ rspamd_cryptobox_hash_update(re_class->st,
+ (gpointer) &cache->re->len,
+ sizeof(cache->re->len));
+ rspamd_cryptobox_hash_final(re_class->st, hash_out);
+ rspamd_snprintf(re_class->hash, sizeof(re_class->hash), "%*xs",
+ (gint) rspamd_cryptobox_HASHBYTES, hash_out);
+ free(re_class->st); /* Due to posix_memalign */
+ re_class->st = NULL;
+ }
+ }
+
+ cache->L = cfg->lua_state;
+
+#ifdef WITH_HYPERSCAN
+ const gchar *platform = "generic";
+ rspamd_fstring_t *features = rspamd_fstring_new();
+
+ cache->disable_hyperscan = cfg->disable_hyperscan;
+
+ g_assert(hs_populate_platform(&cache->plt) == HS_SUCCESS);
+
+ /* Now decode what we do have */
+ switch (cache->plt.tune) {
+ case HS_TUNE_FAMILY_HSW:
+ platform = "haswell";
+ break;
+ case HS_TUNE_FAMILY_SNB:
+ platform = "sandy";
+ break;
+ case HS_TUNE_FAMILY_BDW:
+ platform = "broadwell";
+ break;
+ case HS_TUNE_FAMILY_IVB:
+ platform = "ivy";
+ break;
+ default:
+ break;
+ }
+
+ if (cache->plt.cpu_features & HS_CPU_FEATURES_AVX2) {
+ features = rspamd_fstring_append(features, "AVX2", 4);
+ }
+
+ hs_set_allocator(g_malloc, g_free);
+
+ msg_info_re_cache("loaded hyperscan engine with cpu tune '%s' and features '%V'",
+ platform, features);
+
+ rspamd_fstring_free(features);
+#endif
+}
+
+struct rspamd_re_runtime *
+rspamd_re_cache_runtime_new(struct rspamd_re_cache *cache)
+{
+ struct rspamd_re_runtime *rt;
+ g_assert(cache != NULL);
+
+ rt = g_malloc0(sizeof(*rt) + NBYTES(cache->nre) + cache->nre);
+ rt->cache = cache;
+ REF_RETAIN(cache);
+ rt->checked = ((guchar *) rt) + sizeof(*rt);
+ rt->results = rt->checked + NBYTES(cache->nre);
+ rt->stat.regexp_total = cache->nre;
+#ifdef WITH_HYPERSCAN
+ rt->has_hs = cache->hyperscan_loaded;
+#endif
+
+ return rt;
+}
+
+const struct rspamd_re_cache_stat *
+rspamd_re_cache_get_stat(struct rspamd_re_runtime *rt)
+{
+ g_assert(rt != NULL);
+
+ return &rt->stat;
+}
+
+static gboolean
+rspamd_re_cache_check_lua_condition(struct rspamd_task *task,
+ rspamd_regexp_t *re,
+ const guchar *in, gsize len,
+ goffset start, goffset end,
+ gint lua_cbref)
+{
+ lua_State *L = (lua_State *) task->cfg->lua_state;
+ GError *err = NULL;
+ struct rspamd_lua_text __attribute__((unused)) * t;
+ gint text_pos;
+
+ if (G_LIKELY(lua_cbref == -1)) {
+ return TRUE;
+ }
+
+ t = lua_new_text(L, in, len, FALSE);
+ text_pos = lua_gettop(L);
+
+ if (!rspamd_lua_universal_pcall(L, lua_cbref,
+ G_STRLOC, 1, "utii", &err,
+ "rspamd{task}", task,
+ text_pos, start, end)) {
+ msg_warn_task("cannot call for re_cache_check_lua_condition for re %s: %e",
+ rspamd_regexp_get_pattern(re), err);
+ g_error_free(err);
+ lua_settop(L, text_pos - 1);
+
+ return TRUE;
+ }
+
+ gboolean res = lua_toboolean(L, -1);
+
+ lua_settop(L, text_pos - 1);
+
+ return res;
+}
+
+static guint
+rspamd_re_cache_process_pcre(struct rspamd_re_runtime *rt,
+ rspamd_regexp_t *re, struct rspamd_task *task,
+ const guchar *in, gsize len,
+ gboolean is_raw,
+ gint lua_cbref)
+{
+ guint r = 0;
+ const gchar *start = NULL, *end = NULL;
+ guint max_hits = rspamd_regexp_get_maxhits(re);
+ guint64 id = rspamd_regexp_get_cache_id(re);
+ gdouble t1 = NAN, t2, pr;
+ const gdouble slow_time = 1e8;
+
+ if (in == NULL) {
+ return rt->results[id];
+ }
+
+ if (len == 0) {
+ return rt->results[id];
+ }
+
+ if (rt->cache->max_re_data > 0 && len > rt->cache->max_re_data) {
+ len = rt->cache->max_re_data;
+ }
+
+ r = rt->results[id];
+
+ if (max_hits == 0 || r < max_hits) {
+ pr = rspamd_random_double_fast();
+
+ if (pr > 0.9) {
+ t1 = rspamd_get_ticks(TRUE);
+ }
+
+ while (rspamd_regexp_search(re,
+ in,
+ len,
+ &start,
+ &end,
+ is_raw,
+ NULL)) {
+ if (rspamd_re_cache_check_lua_condition(task, re, in, len,
+ start - (const gchar *) in, end - (const gchar *) in, lua_cbref)) {
+ r++;
+ msg_debug_re_task("found regexp /%s/, total hits: %d",
+ rspamd_regexp_get_pattern(re), r);
+ }
+
+ if (max_hits > 0 && r >= max_hits) {
+ break;
+ }
+ }
+
+ rt->results[id] += r;
+ rt->stat.regexp_checked++;
+ rt->stat.bytes_scanned_pcre += len;
+ rt->stat.bytes_scanned += len;
+
+ if (r > 0) {
+ rt->stat.regexp_matched += r;
+ }
+
+ if (!isnan(t1)) {
+ t2 = rspamd_get_ticks(TRUE);
+
+ if (t2 - t1 > slow_time) {
+ rspamd_symcache_enable_profile(task);
+ msg_info_task("regexp '%16s' took %.0f ticks to execute",
+ rspamd_regexp_get_pattern(re), t2 - t1);
+ }
+ }
+ }
+
+ return r;
+}
+
+#ifdef WITH_HYPERSCAN
+struct rspamd_re_hyperscan_cbdata {
+ struct rspamd_re_runtime *rt;
+ const guchar **ins;
+ const guint *lens;
+ guint count;
+ rspamd_regexp_t *re;
+ struct rspamd_task *task;
+};
+
+static gint
+rspamd_re_cache_hyperscan_cb(unsigned int id,
+ unsigned long long from,
+ unsigned long long to,
+ unsigned int flags,
+ void *ud)
+{
+ struct rspamd_re_hyperscan_cbdata *cbdata = ud;
+ struct rspamd_re_runtime *rt;
+ struct rspamd_re_cache_elt *cache_elt;
+ guint ret, maxhits, i, processed;
+ struct rspamd_task *task;
+
+ rt = cbdata->rt;
+ task = cbdata->task;
+ cache_elt = g_ptr_array_index(rt->cache->re, id);
+ maxhits = rspamd_regexp_get_maxhits(cache_elt->re);
+
+ if (cache_elt->match_type == RSPAMD_RE_CACHE_HYPERSCAN) {
+ if (rspamd_re_cache_check_lua_condition(task, cache_elt->re,
+ cbdata->ins[0], cbdata->lens[0], from, to, cache_elt->lua_cbref)) {
+ ret = 1;
+ setbit(rt->checked, id);
+
+ if (maxhits == 0 || rt->results[id] < maxhits) {
+ rt->results[id] += ret;
+ rt->stat.regexp_matched++;
+ }
+ msg_debug_re_task("found regexp /%s/ using hyperscan only, total hits: %d",
+ rspamd_regexp_get_pattern(cache_elt->re), rt->results[id]);
+ }
+ }
+ else {
+ if (!isset(rt->checked, id)) {
+
+ processed = 0;
+
+ for (i = 0; i < cbdata->count; i++) {
+ rspamd_re_cache_process_pcre(rt,
+ cache_elt->re,
+ cbdata->task,
+ cbdata->ins[i],
+ cbdata->lens[i],
+ FALSE,
+ cache_elt->lua_cbref);
+ setbit(rt->checked, id);
+
+ processed += cbdata->lens[i];
+
+ if (processed >= to) {
+ break;
+ }
+ }
+ }
+ }
+
+ return 0;
+}
+#endif
+
+static guint
+rspamd_re_cache_process_regexp_data(struct rspamd_re_runtime *rt,
+ rspamd_regexp_t *re, struct rspamd_task *task,
+ const guchar **in, guint *lens,
+ guint count,
+ gboolean is_raw,
+ gboolean *processed_hyperscan)
+{
+
+ guint64 re_id;
+ guint ret = 0;
+ guint i;
+ struct rspamd_re_cache_elt *cache_elt;
+
+ re_id = rspamd_regexp_get_cache_id(re);
+
+ if (count == 0 || in == NULL) {
+ /* We assume this as absence of the specified data */
+ setbit(rt->checked, re_id);
+ rt->results[re_id] = ret;
+ return ret;
+ }
+
+ cache_elt = (struct rspamd_re_cache_elt *) g_ptr_array_index(rt->cache->re, re_id);
+
+#ifndef WITH_HYPERSCAN
+ for (i = 0; i < count; i++) {
+ ret = rspamd_re_cache_process_pcre(rt,
+ re,
+ task,
+ in[i],
+ lens[i],
+ is_raw,
+ cache_elt->lua_cbref);
+ rt->results[re_id] = ret;
+ }
+
+ setbit(rt->checked, re_id);
+#else
+ struct rspamd_re_class *re_class;
+ struct rspamd_re_hyperscan_cbdata cbdata;
+
+ cache_elt = g_ptr_array_index(rt->cache->re, re_id);
+ re_class = rspamd_regexp_get_class(re);
+
+ if (rt->cache->disable_hyperscan || cache_elt->match_type == RSPAMD_RE_CACHE_PCRE ||
+ !rt->has_hs || (is_raw && re_class->has_utf8)) {
+ for (i = 0; i < count; i++) {
+ ret = rspamd_re_cache_process_pcre(rt,
+ re,
+ task,
+ in[i],
+ lens[i],
+ is_raw,
+ cache_elt->lua_cbref);
+ }
+
+ setbit(rt->checked, re_id);
+ }
+ else {
+ for (i = 0; i < count; i++) {
+ /* For Hyperscan we can probably safely disable all those limits */
+#if 0
+ if (rt->cache->max_re_data > 0 && lens[i] > rt->cache->max_re_data) {
+ lens[i] = rt->cache->max_re_data;
+ }
+#endif
+ rt->stat.bytes_scanned += lens[i];
+ }
+
+ g_assert(re_class->hs_scratch != NULL);
+ g_assert(re_class->hs_db != NULL);
+
+ /* Go through hyperscan API */
+ for (i = 0; i < count; i++) {
+ cbdata.ins = &in[i];
+ cbdata.re = re;
+ cbdata.rt = rt;
+ cbdata.lens = &lens[i];
+ cbdata.count = 1;
+ cbdata.task = task;
+
+ if ((hs_scan(rspamd_hyperscan_get_database(re_class->hs_db),
+ in[i], lens[i], 0,
+ re_class->hs_scratch,
+ rspamd_re_cache_hyperscan_cb, &cbdata)) != HS_SUCCESS) {
+ ret = 0;
+ }
+ else {
+ ret = rt->results[re_id];
+ *processed_hyperscan = TRUE;
+ }
+ }
+ }
+#endif
+
+ return ret;
+}
+
+static void
+rspamd_re_cache_finish_class(struct rspamd_task *task,
+ struct rspamd_re_runtime *rt,
+ struct rspamd_re_class *re_class,
+ const gchar *class_name)
+{
+#ifdef WITH_HYPERSCAN
+ guint i;
+ guint64 re_id;
+ guint found = 0;
+
+ /* Set all bits that are not checked and included in hyperscan to 1 */
+ for (i = 0; i < re_class->nhs; i++) {
+ re_id = re_class->hs_ids[i];
+
+ if (!isset(rt->checked, re_id)) {
+ g_assert(rt->results[re_id] == 0);
+ rt->results[re_id] = 0;
+ setbit(rt->checked, re_id);
+ }
+ else {
+ found++;
+ }
+ }
+
+ msg_debug_re_task("finished hyperscan for class %s; %d "
+ "matches found; %d hyperscan supported regexps; %d total regexps",
+ class_name, found, re_class->nhs, (gint) g_hash_table_size(re_class->re));
+#endif
+}
+
+static gboolean
+rspamd_re_cache_process_selector(struct rspamd_task *task,
+ struct rspamd_re_runtime *rt,
+ const gchar *name,
+ guchar ***svec,
+ guint **lenvec,
+ guint *n)
+{
+ gint ref;
+ khiter_t k;
+ lua_State *L;
+ gint err_idx, ret;
+ struct rspamd_task **ptask;
+ gboolean result = FALSE;
+ struct rspamd_re_cache *cache = rt->cache;
+ struct rspamd_re_selector_result *sr;
+
+ L = cache->L;
+ k = kh_get(lua_selectors_hash, cache->selectors, (gchar *) name);
+
+ if (k == kh_end(cache->selectors)) {
+ msg_err_task("cannot find selector %s, not registered", name);
+
+ return FALSE;
+ }
+
+ ref = kh_value(cache->selectors, k);
+
+ /* First, search for the cached result */
+ if (rt->sel_cache) {
+ k = kh_get(selectors_results_hash, rt->sel_cache, ref);
+
+ if (k != kh_end(rt->sel_cache)) {
+ sr = &kh_value(rt->sel_cache, k);
+
+ *svec = sr->scvec;
+ *lenvec = sr->lenvec;
+ *n = sr->cnt;
+
+ return TRUE;
+ }
+ }
+ else {
+ rt->sel_cache = kh_init(selectors_results_hash);
+ }
+
+ lua_pushcfunction(L, &rspamd_lua_traceback);
+ err_idx = lua_gettop(L);
+
+ lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
+ ptask = lua_newuserdata(L, sizeof(*ptask));
+ *ptask = task;
+ rspamd_lua_setclass(L, "rspamd{task}", -1);
+
+ if ((ret = lua_pcall(L, 1, 1, err_idx)) != 0) {
+ msg_err_task("call to selector %s "
+ "failed (%d): %s",
+ name, ret,
+ lua_tostring(L, -1));
+ }
+ else {
+ struct rspamd_lua_text *txt;
+ gsize slen;
+ const gchar *sel_data;
+
+ if (lua_type(L, -1) != LUA_TTABLE) {
+ txt = lua_check_text_or_string(L, -1);
+
+
+ if (txt) {
+ msg_debug_re_cache("re selector %s returned 1 element", name);
+ sel_data = txt->start;
+ slen = txt->len;
+ *n = 1;
+ *svec = g_malloc(sizeof(guchar *));
+ *lenvec = g_malloc(sizeof(guint));
+ (*svec)[0] = g_malloc(slen);
+ memcpy((*svec)[0], sel_data, slen);
+ (*lenvec)[0] = slen;
+ result = TRUE;
+ }
+ else {
+ msg_debug_re_cache("re selector %s returned NULL", name);
+ }
+ }
+ else {
+ *n = rspamd_lua_table_size(L, -1);
+
+ msg_debug_re_cache("re selector %s returned %d elements", name, *n);
+
+ if (*n > 0) {
+ *svec = g_malloc(sizeof(guchar *) * (*n));
+ *lenvec = g_malloc(sizeof(guint) * (*n));
+
+ for (int i = 0; i < *n; i++) {
+ lua_rawgeti(L, -1, i + 1);
+
+ txt = lua_check_text_or_string(L, -1);
+ if (txt && txt->len > 0) {
+ sel_data = txt->start;
+ slen = txt->len;
+ (*svec)[i] = g_malloc(slen);
+ memcpy((*svec)[i], sel_data, slen);
+ }
+ else {
+ /* A hack to avoid malloc(0) */
+ sel_data = "";
+ slen = 0;
+ (*svec)[i] = g_malloc(1);
+ memcpy((*svec)[i], sel_data, 1);
+ }
+
+ (*lenvec)[i] = slen;
+ lua_pop(L, 1);
+ }
+ }
+
+ /* Empty table is also a valid result */
+ result = TRUE;
+ }
+ }
+
+ lua_settop(L, err_idx - 1);
+
+ if (result) {
+ k = kh_put(selectors_results_hash, rt->sel_cache, ref, &ret);
+ sr = &kh_value(rt->sel_cache, k);
+
+ sr->cnt = *n;
+ sr->scvec = *svec;
+ sr->lenvec = *lenvec;
+ }
+
+ return result;
+}
+
+static inline guint
+rspamd_process_words_vector(GArray *words,
+ const guchar **scvec,
+ guint *lenvec,
+ struct rspamd_re_class *re_class,
+ guint cnt,
+ gboolean *raw)
+{
+ guint j;
+ rspamd_stat_token_t *tok;
+
+ if (words) {
+ for (j = 0; j < words->len; j++) {
+ tok = &g_array_index(words, rspamd_stat_token_t, j);
+
+ if (tok->flags & RSPAMD_STAT_TOKEN_FLAG_TEXT) {
+ if (!(tok->flags & RSPAMD_STAT_TOKEN_FLAG_UTF)) {
+ if (!re_class->has_utf8) {
+ *raw = TRUE;
+ }
+ else {
+ continue; /* Skip */
+ }
+ }
+ }
+ else {
+ continue; /* Skip non text */
+ }
+
+ if (re_class->type == RSPAMD_RE_RAWWORDS) {
+ if (tok->original.len > 0) {
+ scvec[cnt] = tok->original.begin;
+ lenvec[cnt++] = tok->original.len;
+ }
+ }
+ else if (re_class->type == RSPAMD_RE_WORDS) {
+ if (tok->normalized.len > 0) {
+ scvec[cnt] = tok->normalized.begin;
+ lenvec[cnt++] = tok->normalized.len;
+ }
+ }
+ else {
+ /* Stemmed words */
+ if (tok->stemmed.len > 0) {
+ scvec[cnt] = tok->stemmed.begin;
+ lenvec[cnt++] = tok->stemmed.len;
+ }
+ }
+ }
+ }
+
+ return cnt;
+}
+
+static guint
+rspamd_re_cache_process_headers_list(struct rspamd_task *task,
+ struct rspamd_re_runtime *rt,
+ rspamd_regexp_t *re,
+ struct rspamd_re_class *re_class,
+ struct rspamd_mime_header *rh,
+ gboolean is_strong,
+ gboolean *processed_hyperscan)
+{
+ const guchar **scvec, *in;
+ gboolean raw = FALSE;
+ guint *lenvec;
+ struct rspamd_mime_header *cur;
+ guint cnt = 0, i = 0, ret = 0;
+
+ DL_COUNT(rh, cur, cnt);
+
+ scvec = g_malloc(sizeof(*scvec) * cnt);
+ lenvec = g_malloc(sizeof(*lenvec) * cnt);
+
+ DL_FOREACH(rh, cur)
+ {
+
+ if (is_strong && strcmp(cur->name, re_class->type_data) != 0) {
+ /* Skip a different case */
+ continue;
+ }
+
+ if (re_class->type == RSPAMD_RE_RAWHEADER) {
+ in = (const guchar *) cur->value;
+ lenvec[i] = strlen(cur->value);
+
+ if (rspamd_fast_utf8_validate(in, lenvec[i]) != 0) {
+ raw = TRUE;
+ }
+ }
+ else {
+ in = (const guchar *) cur->decoded;
+ /* Validate input^W^WNo need to validate as it is already valid */
+ if (!in) {
+ lenvec[i] = 0;
+ scvec[i] = (guchar *) "";
+ continue;
+ }
+
+ lenvec[i] = strlen(in);
+ }
+
+ scvec[i] = in;
+
+ i++;
+ }
+
+ if (i > 0) {
+ ret = rspamd_re_cache_process_regexp_data(rt, re,
+ task, scvec, lenvec, i, raw, processed_hyperscan);
+ msg_debug_re_task("checking header %s regexp: %s=%*s -> %d",
+ re_class->type_data,
+ rspamd_regexp_get_pattern(re),
+ (int) lenvec[0], scvec[0], ret);
+ }
+
+ g_free(scvec);
+ g_free(lenvec);
+
+ return ret;
+}
+
+/*
+ * Calculates the specified regexp for the specified class if it's not calculated
+ */
+static guint
+rspamd_re_cache_exec_re(struct rspamd_task *task,
+ struct rspamd_re_runtime *rt,
+ rspamd_regexp_t *re,
+ struct rspamd_re_class *re_class,
+ gboolean is_strong)
+{
+ guint ret = 0, i, re_id;
+ struct rspamd_mime_header *rh;
+ const gchar *in;
+ const guchar **scvec = NULL;
+ guint *lenvec = NULL;
+ gboolean raw = FALSE, processed_hyperscan = FALSE;
+ struct rspamd_mime_text_part *text_part;
+ struct rspamd_mime_part *mime_part;
+ struct rspamd_url *url;
+ guint len = 0, cnt = 0;
+ const gchar *class_name;
+
+ class_name = rspamd_re_cache_type_to_string(re_class->type);
+ msg_debug_re_task("start check re type: %s: /%s/",
+ class_name,
+ rspamd_regexp_get_pattern(re));
+ re_id = rspamd_regexp_get_cache_id(re);
+
+ switch (re_class->type) {
+ case RSPAMD_RE_HEADER:
+ case RSPAMD_RE_RAWHEADER:
+ /* Get list of specified headers */
+ rh = rspamd_message_get_header_array(task,
+ re_class->type_data, FALSE);
+
+ if (rh) {
+ ret = rspamd_re_cache_process_headers_list(task, rt, re,
+ re_class, rh, is_strong, &processed_hyperscan);
+ msg_debug_re_task("checked header(%s) regexp: %s -> %d",
+ (const char *) re_class->type_data,
+ rspamd_regexp_get_pattern(re),
+ ret);
+ }
+ break;
+ case RSPAMD_RE_ALLHEADER:
+ raw = TRUE;
+ in = MESSAGE_FIELD(task, raw_headers_content).begin;
+ len = MESSAGE_FIELD(task, raw_headers_content).len;
+ ret = rspamd_re_cache_process_regexp_data(rt, re,
+ task, (const guchar **) &in, &len, 1, raw, &processed_hyperscan);
+ msg_debug_re_task("checked allheader regexp: %s -> %d",
+ rspamd_regexp_get_pattern(re), ret);
+ break;
+ case RSPAMD_RE_MIMEHEADER:
+ PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, parts), i, mime_part)
+ {
+ if (mime_part->parent_part == NULL ||
+ !IS_PART_MULTIPART(mime_part->parent_part) ||
+ IS_PART_MESSAGE(mime_part)) {
+ /* We filter parts that have no multipart parent or are a messages here */
+ continue;
+ }
+ rh = rspamd_message_get_header_from_hash(mime_part->raw_headers,
+ re_class->type_data, FALSE);
+
+ if (rh) {
+ ret += rspamd_re_cache_process_headers_list(task, rt, re,
+ re_class, rh, is_strong, &processed_hyperscan);
+ }
+ msg_debug_re_task("checked mime header(%s) regexp: %s -> %d",
+ (const char *) re_class->type_data,
+ rspamd_regexp_get_pattern(re),
+ ret);
+ }
+ break;
+ case RSPAMD_RE_MIME:
+ case RSPAMD_RE_RAWMIME:
+ /* Iterate through text parts */
+ if (MESSAGE_FIELD(task, text_parts)->len > 0) {
+ cnt = MESSAGE_FIELD(task, text_parts)->len;
+ scvec = g_malloc(sizeof(*scvec) * cnt);
+ lenvec = g_malloc(sizeof(*lenvec) * cnt);
+
+ PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, text_part)
+ {
+ /* Select data for regexp */
+ if (re_class->type == RSPAMD_RE_RAWMIME) {
+ if (text_part->raw.len == 0) {
+ len = 0;
+ in = "";
+ }
+ else {
+ in = text_part->raw.begin;
+ len = text_part->raw.len;
+ }
+
+ raw = TRUE;
+ }
+ else {
+ /* Skip empty parts */
+ if (IS_TEXT_PART_EMPTY(text_part)) {
+ len = 0;
+ in = "";
+ }
+ else {
+ /* Check raw flags */
+ if (!IS_TEXT_PART_UTF(text_part)) {
+ raw = TRUE;
+ }
+
+ in = text_part->utf_content.begin;
+ len = text_part->utf_content.len;
+ }
+ }
+
+ scvec[i] = (guchar *) in;
+ lenvec[i] = len;
+ }
+
+ ret = rspamd_re_cache_process_regexp_data(rt, re,
+ task, scvec, lenvec, cnt, raw, &processed_hyperscan);
+ msg_debug_re_task("checked mime regexp: %s -> %d",
+ rspamd_regexp_get_pattern(re), ret);
+ g_free(scvec);
+ g_free(lenvec);
+ }
+ break;
+ case RSPAMD_RE_URL:
+ cnt = kh_size(MESSAGE_FIELD(task, urls));
+
+ if (cnt > 0) {
+ scvec = g_malloc(sizeof(*scvec) * cnt);
+ lenvec = g_malloc(sizeof(*lenvec) * cnt);
+ i = 0;
+ raw = FALSE;
+
+ kh_foreach_key(MESSAGE_FIELD(task, urls), url, {
+ if ((url->protocol & PROTOCOL_MAILTO)) {
+ continue;
+ }
+ in = url->string;
+ len = url->urllen;
+
+ if (len > 0 && !(url->flags & RSPAMD_URL_FLAG_IMAGE)) {
+ scvec[i] = (guchar *) in;
+ lenvec[i++] = len;
+ }
+ });
+
+ /* URL regexps do not include emails, that's why the code below is commented */
+#if 0
+ g_hash_table_iter_init (&it, MESSAGE_FIELD (task, emails));
+
+ while (g_hash_table_iter_next (&it, &k, &v)) {
+ url = v;
+ in = url->string;
+ len = url->urllen;
+
+ if (len > 0 && !(url->flags & RSPAMD_URL_FLAG_IMAGE)) {
+ scvec[i] = (guchar *) in;
+ lenvec[i++] = len;
+ }
+ }
+#endif
+ ret = rspamd_re_cache_process_regexp_data(rt, re,
+ task, scvec, lenvec, i, raw, &processed_hyperscan);
+ msg_debug_re_task("checked url regexp: %s -> %d",
+ rspamd_regexp_get_pattern(re), ret);
+ g_free(scvec);
+ g_free(lenvec);
+ }
+ break;
+ case RSPAMD_RE_EMAIL:
+ cnt = kh_size(MESSAGE_FIELD(task, urls));
+
+ if (cnt > 0) {
+ scvec = g_malloc(sizeof(*scvec) * cnt);
+ lenvec = g_malloc(sizeof(*lenvec) * cnt);
+ i = 0;
+ raw = FALSE;
+
+ kh_foreach_key(MESSAGE_FIELD(task, urls), url, {
+ if (!(url->protocol & PROTOCOL_MAILTO)) {
+ continue;
+ }
+ if (url->userlen == 0 || url->hostlen == 0) {
+ continue;
+ }
+
+ in = rspamd_url_user_unsafe(url);
+ len = url->userlen + 1 + url->hostlen;
+ scvec[i] = (guchar *) in;
+ lenvec[i++] = len;
+ });
+
+ ret = rspamd_re_cache_process_regexp_data(rt, re,
+ task, scvec, lenvec, i, raw, &processed_hyperscan);
+ msg_debug_re_task("checked email regexp: %s -> %d",
+ rspamd_regexp_get_pattern(re), ret);
+ g_free(scvec);
+ g_free(lenvec);
+ }
+ break;
+ case RSPAMD_RE_BODY:
+ raw = TRUE;
+ in = task->msg.begin;
+ len = task->msg.len;
+
+ ret = rspamd_re_cache_process_regexp_data(rt, re, task,
+ (const guchar **) &in, &len, 1, raw, &processed_hyperscan);
+ msg_debug_re_task("checked rawbody regexp: %s -> %d",
+ rspamd_regexp_get_pattern(re), ret);
+ break;
+ case RSPAMD_RE_SABODY:
+ /* According to SA docs:
+ * The 'body' in this case is the textual parts of the message body;
+ * any non-text MIME parts are stripped, and the message decoded from
+ * Quoted-Printable or Base-64-encoded format if necessary. The message
+ * Subject header is considered part of the body and becomes the first
+ * paragraph when running the rules. All HTML tags and line breaks will
+ * be removed before matching.
+ */
+ cnt = MESSAGE_FIELD(task, text_parts)->len + 1;
+ scvec = g_malloc(sizeof(*scvec) * cnt);
+ lenvec = g_malloc(sizeof(*lenvec) * cnt);
+
+ /*
+ * Body rules also include the Subject as the first line
+ * of the body content.
+ */
+
+ rh = rspamd_message_get_header_array(task, "Subject", FALSE);
+
+ if (rh) {
+ scvec[0] = (guchar *) rh->decoded;
+ lenvec[0] = strlen(rh->decoded);
+ }
+ else {
+ scvec[0] = (guchar *) "";
+ lenvec[0] = 0;
+ }
+
+ PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, text_part)
+ {
+ if (text_part->utf_stripped_content) {
+ scvec[i + 1] = (guchar *) text_part->utf_stripped_content->data;
+ lenvec[i + 1] = text_part->utf_stripped_content->len;
+
+ if (!IS_TEXT_PART_UTF(text_part)) {
+ raw = TRUE;
+ }
+ }
+ else {
+ scvec[i + 1] = (guchar *) "";
+ lenvec[i + 1] = 0;
+ }
+ }
+
+ ret = rspamd_re_cache_process_regexp_data(rt, re,
+ task, scvec, lenvec, cnt, raw, &processed_hyperscan);
+ msg_debug_re_task("checked sa body regexp: %s -> %d",
+ rspamd_regexp_get_pattern(re), ret);
+ g_free(scvec);
+ g_free(lenvec);
+ break;
+ case RSPAMD_RE_SARAWBODY:
+ /* According to SA docs:
+ * The 'raw body' of a message is the raw data inside all textual
+ * parts. The text will be decoded from base64 or quoted-printable
+ * encoding, but HTML tags and line breaks will still be present.
+ * Multiline expressions will need to be used to match strings that are
+ * broken by line breaks.
+ */
+ if (MESSAGE_FIELD(task, text_parts)->len > 0) {
+ cnt = MESSAGE_FIELD(task, text_parts)->len;
+ scvec = g_malloc(sizeof(*scvec) * cnt);
+ lenvec = g_malloc(sizeof(*lenvec) * cnt);
+
+ for (i = 0; i < cnt; i++) {
+ text_part = g_ptr_array_index(MESSAGE_FIELD(task, text_parts), i);
+
+ if (text_part->parsed.len > 0) {
+ scvec[i] = (guchar *) text_part->parsed.begin;
+ lenvec[i] = text_part->parsed.len;
+
+ if (!IS_TEXT_PART_UTF(text_part)) {
+ raw = TRUE;
+ }
+ }
+ else {
+ scvec[i] = (guchar *) "";
+ lenvec[i] = 0;
+ }
+ }
+
+ ret = rspamd_re_cache_process_regexp_data(rt, re,
+ task, scvec, lenvec, cnt, raw, &processed_hyperscan);
+ msg_debug_re_task("checked sa rawbody regexp: %s -> %d",
+ rspamd_regexp_get_pattern(re), ret);
+ g_free(scvec);
+ g_free(lenvec);
+ }
+ break;
+ case RSPAMD_RE_WORDS:
+ case RSPAMD_RE_STEMWORDS:
+ case RSPAMD_RE_RAWWORDS:
+ if (MESSAGE_FIELD(task, text_parts)->len > 0) {
+ cnt = 0;
+ raw = FALSE;
+
+ PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, text_part)
+ {
+ if (text_part->utf_words) {
+ cnt += text_part->utf_words->len;
+ }
+ }
+
+ if (task->meta_words && task->meta_words->len > 0) {
+ cnt += task->meta_words->len;
+ }
+
+ if (cnt > 0) {
+ scvec = g_malloc(sizeof(*scvec) * cnt);
+ lenvec = g_malloc(sizeof(*lenvec) * cnt);
+
+ cnt = 0;
+
+ PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, text_part)
+ {
+ if (text_part->utf_words) {
+ cnt = rspamd_process_words_vector(text_part->utf_words,
+ scvec, lenvec, re_class, cnt, &raw);
+ }
+ }
+
+ if (task->meta_words) {
+ cnt = rspamd_process_words_vector(task->meta_words,
+ scvec, lenvec, re_class, cnt, &raw);
+ }
+
+ ret = rspamd_re_cache_process_regexp_data(rt, re,
+ task, scvec, lenvec, cnt, raw, &processed_hyperscan);
+
+ msg_debug_re_task("checked sa words regexp: %s -> %d",
+ rspamd_regexp_get_pattern(re), ret);
+ g_free(scvec);
+ g_free(lenvec);
+ }
+ }
+ break;
+ case RSPAMD_RE_SELECTOR:
+ if (rspamd_re_cache_process_selector(task, rt,
+ re_class->type_data,
+ (guchar ***) &scvec,
+ &lenvec, &cnt)) {
+
+ ret = rspamd_re_cache_process_regexp_data(rt, re,
+ task, scvec, lenvec, cnt, raw, &processed_hyperscan);
+ msg_debug_re_task("checked selector(%s) regexp: %s -> %d",
+ re_class->type_data,
+ rspamd_regexp_get_pattern(re), ret);
+
+ /* Do not free vectors as they are managed by rt->sel_cache */
+ }
+ break;
+ case RSPAMD_RE_MAX:
+ msg_err_task("regexp of class invalid has been called: %s",
+ rspamd_regexp_get_pattern(re));
+ break;
+ }
+
+#if WITH_HYPERSCAN
+ if (processed_hyperscan) {
+ rspamd_re_cache_finish_class(task, rt, re_class, class_name);
+ }
+#endif
+
+ setbit(rt->checked, re_id);
+
+ return rt->results[re_id];
+}
+
+gint rspamd_re_cache_process(struct rspamd_task *task,
+ rspamd_regexp_t *re,
+ enum rspamd_re_type type,
+ gconstpointer type_data,
+ gsize datalen,
+ gboolean is_strong)
+{
+ guint64 re_id;
+ struct rspamd_re_class *re_class;
+ struct rspamd_re_cache *cache;
+ struct rspamd_re_runtime *rt;
+
+ g_assert(task != NULL);
+ rt = task->re_rt;
+ g_assert(rt != NULL);
+ g_assert(re != NULL);
+
+ cache = rt->cache;
+ re_id = rspamd_regexp_get_cache_id(re);
+
+ if (re_id == RSPAMD_INVALID_ID || re_id > cache->nre) {
+ msg_err_task("re '%s' has no valid id for the cache",
+ rspamd_regexp_get_pattern(re));
+ return 0;
+ }
+
+ if (isset(rt->checked, re_id)) {
+ /* Fast path */
+ rt->stat.regexp_fast_cached++;
+ return rt->results[re_id];
+ }
+ else {
+ /* Slow path */
+ re_class = rspamd_regexp_get_class(re);
+
+ if (re_class == NULL) {
+ msg_err_task("cannot find re class for regexp '%s'",
+ rspamd_regexp_get_pattern(re));
+ return 0;
+ }
+
+ return rspamd_re_cache_exec_re(task, rt, re, re_class,
+ is_strong);
+ }
+
+ return 0;
+}
+
+int rspamd_re_cache_process_ffi(void *ptask,
+ void *pre,
+ int type,
+ void *type_data,
+ int is_strong)
+{
+ struct rspamd_lua_regexp **lua_re = pre;
+ struct rspamd_task **real_task = ptask;
+ gsize typelen = 0;
+
+ if (type_data) {
+ typelen = strlen(type_data);
+ }
+
+ return rspamd_re_cache_process(*real_task, (*lua_re)->re,
+ type, type_data, typelen, is_strong);
+}
+
+void rspamd_re_cache_runtime_destroy(struct rspamd_re_runtime *rt)
+{
+ g_assert(rt != NULL);
+
+ if (rt->sel_cache) {
+ struct rspamd_re_selector_result sr;
+
+ kh_foreach_value(rt->sel_cache, sr, {
+ for (guint i = 0; i < sr.cnt; i++) {
+ g_free((gpointer) sr.scvec[i]);
+ }
+
+ g_free(sr.scvec);
+ g_free(sr.lenvec);
+ });
+ kh_destroy(selectors_results_hash, rt->sel_cache);
+ }
+
+ REF_RELEASE(rt->cache);
+ g_free(rt);
+}
+
+void rspamd_re_cache_unref(struct rspamd_re_cache *cache)
+{
+ if (cache) {
+ REF_RELEASE(cache);
+ }
+}
+
+struct rspamd_re_cache *
+rspamd_re_cache_ref(struct rspamd_re_cache *cache)
+{
+ if (cache) {
+ REF_RETAIN(cache);
+ }
+
+ return cache;
+}
+
+guint rspamd_re_cache_set_limit(struct rspamd_re_cache *cache, guint limit)
+{
+ guint old;
+
+ g_assert(cache != NULL);
+
+ old = cache->max_re_data;
+ cache->max_re_data = limit;
+
+ return old;
+}
+
+const gchar *
+rspamd_re_cache_type_to_string(enum rspamd_re_type type)
+{
+ const gchar *ret = "unknown";
+
+ switch (type) {
+ case RSPAMD_RE_HEADER:
+ ret = "header";
+ break;
+ case RSPAMD_RE_RAWHEADER:
+ ret = "raw header";
+ break;
+ case RSPAMD_RE_MIMEHEADER:
+ ret = "mime header";
+ break;
+ case RSPAMD_RE_ALLHEADER:
+ ret = "all headers";
+ break;
+ case RSPAMD_RE_MIME:
+ ret = "part";
+ break;
+ case RSPAMD_RE_RAWMIME:
+ ret = "raw part";
+ break;
+ case RSPAMD_RE_BODY:
+ ret = "rawbody";
+ break;
+ case RSPAMD_RE_URL:
+ ret = "url";
+ break;
+ case RSPAMD_RE_EMAIL:
+ ret = "email";
+ break;
+ case RSPAMD_RE_SABODY:
+ ret = "sa body";
+ break;
+ case RSPAMD_RE_SARAWBODY:
+ ret = "sa raw body";
+ break;
+ case RSPAMD_RE_SELECTOR:
+ ret = "selector";
+ break;
+ case RSPAMD_RE_WORDS:
+ ret = "words";
+ break;
+ case RSPAMD_RE_RAWWORDS:
+ ret = "raw_words";
+ break;
+ case RSPAMD_RE_STEMWORDS:
+ ret = "stem_words";
+ break;
+ case RSPAMD_RE_MAX:
+ default:
+ ret = "invalid class";
+ break;
+ }
+
+ return ret;
+}
+
+enum rspamd_re_type
+rspamd_re_cache_type_from_string(const char *str)
+{
+ enum rspamd_re_type ret;
+ guint64 h;
+
+ /*
+ * To optimize this function, we apply hash to input string and
+ * pre-select it from the values
+ */
+
+ if (str != NULL) {
+ h = rspamd_cryptobox_fast_hash_specific(RSPAMD_CRYPTOBOX_XXHASH64,
+ str, strlen(str), 0xdeadbabe);
+
+ switch (h) {
+ case G_GUINT64_CONSTANT(0x298b9c8a58887d44): /* header */
+ ret = RSPAMD_RE_HEADER;
+ break;
+ case G_GUINT64_CONSTANT(0x467bfb5cd7ddf890): /* rawheader */
+ ret = RSPAMD_RE_RAWHEADER;
+ break;
+ case G_GUINT64_CONSTANT(0xda081341fb600389): /* mime */
+ ret = RSPAMD_RE_MIME;
+ break;
+ case G_GUINT64_CONSTANT(0xc35831e067a8221d): /* rawmime */
+ ret = RSPAMD_RE_RAWMIME;
+ break;
+ case G_GUINT64_CONSTANT(0xc625e13dbe636de2): /* body */
+ case G_GUINT64_CONSTANT(0xCCDEBA43518F721C): /* message */
+ ret = RSPAMD_RE_BODY;
+ break;
+ case G_GUINT64_CONSTANT(0x286edbe164c791d2): /* url */
+ case G_GUINT64_CONSTANT(0x7D9ACDF6685661A1): /* uri */
+ ret = RSPAMD_RE_URL;
+ break;
+ case G_GUINT64_CONSTANT(0x7e232b0f60b571be): /* email */
+ ret = RSPAMD_RE_EMAIL;
+ break;
+ case G_GUINT64_CONSTANT(0x796d62205a8778c7): /* allheader */
+ ret = RSPAMD_RE_ALLHEADER;
+ break;
+ case G_GUINT64_CONSTANT(0xa3c6c153b3b00a5e): /* mimeheader */
+ ret = RSPAMD_RE_MIMEHEADER;
+ break;
+ case G_GUINT64_CONSTANT(0x7794501506e604e9): /* sabody */
+ ret = RSPAMD_RE_SABODY;
+ break;
+ case G_GUINT64_CONSTANT(0x28828962E7D2A05F): /* sarawbody */
+ ret = RSPAMD_RE_SARAWBODY;
+ break;
+ default:
+ ret = RSPAMD_RE_MAX;
+ break;
+ }
+ }
+ else {
+ ret = RSPAMD_RE_MAX;
+ }
+
+ return ret;
+}
+
+#ifdef WITH_HYPERSCAN
+static gchar *
+rspamd_re_cache_hs_pattern_from_pcre(rspamd_regexp_t *re)
+{
+ /*
+ * Workaround for bug in ragel 7.0.0.11
+ * https://github.com/intel/hyperscan/issues/133
+ */
+ const gchar *pat = rspamd_regexp_get_pattern(re);
+ guint flags = rspamd_regexp_get_flags(re), esc_flags = RSPAMD_REGEXP_ESCAPE_RE;
+ gchar *escaped;
+ gsize esc_len;
+
+ if (flags & RSPAMD_REGEXP_FLAG_UTF) {
+ esc_flags |= RSPAMD_REGEXP_ESCAPE_UTF;
+ }
+
+ escaped = rspamd_str_regexp_escape(pat, strlen(pat), &esc_len, esc_flags);
+
+ return escaped;
+}
+
+static gboolean
+rspamd_re_cache_is_finite(struct rspamd_re_cache *cache,
+ rspamd_regexp_t *re, gint flags, gdouble max_time)
+{
+ pid_t cld;
+ gint status;
+ struct timespec ts;
+ hs_compile_error_t *hs_errors;
+ hs_database_t *test_db;
+ gdouble wait_time;
+ const gint max_tries = 10;
+ gint tries = 0, rc;
+ void (*old_hdl)(int);
+
+ wait_time = max_time / max_tries;
+ /* We need to restore SIGCHLD processing */
+ old_hdl = signal(SIGCHLD, SIG_DFL);
+ cld = fork();
+
+ if (cld == 0) {
+ /* Try to compile pattern */
+
+ gchar *pat = rspamd_re_cache_hs_pattern_from_pcre(re);
+
+ if (hs_compile(pat,
+ flags | HS_FLAG_PREFILTER,
+ HS_MODE_BLOCK,
+ &cache->plt,
+ &test_db,
+ &hs_errors) != HS_SUCCESS) {
+
+ msg_info_re_cache("cannot compile (prefilter mode) '%s' to hyperscan: '%s'",
+ pat,
+ hs_errors != NULL ? hs_errors->message : "unknown error");
+
+ hs_free_compile_error(hs_errors);
+ g_free(pat);
+
+ exit(EXIT_FAILURE);
+ }
+
+ g_free(pat);
+ exit(EXIT_SUCCESS);
+ }
+ else if (cld > 0) {
+ double_to_ts(wait_time, &ts);
+
+ while ((rc = waitpid(cld, &status, WNOHANG)) == 0 && tries++ < max_tries) {
+ (void) nanosleep(&ts, NULL);
+ }
+
+ /* Child has been terminated */
+ if (rc > 0) {
+ /* Forget about SIGCHLD after this point */
+ signal(SIGCHLD, old_hdl);
+
+ if (WIFEXITED(status) && WEXITSTATUS(status) == EXIT_SUCCESS) {
+ return TRUE;
+ }
+ else {
+ msg_err_re_cache(
+ "cannot approximate %s to hyperscan",
+ rspamd_regexp_get_pattern(re));
+
+ return FALSE;
+ }
+ }
+ else {
+ /* We consider that as timeout */
+ kill(cld, SIGKILL);
+ g_assert(waitpid(cld, &status, 0) != -1);
+ msg_err_re_cache(
+ "cannot approximate %s to hyperscan: timeout waiting",
+ rspamd_regexp_get_pattern(re));
+ signal(SIGCHLD, old_hdl);
+ }
+ }
+ else {
+ msg_err_re_cache(
+ "cannot approximate %s to hyperscan: fork failed: %s",
+ rspamd_regexp_get_pattern(re), strerror(errno));
+ signal(SIGCHLD, old_hdl);
+ }
+
+ return FALSE;
+}
+#endif
+
+#ifdef WITH_HYPERSCAN
+struct rspamd_re_cache_hs_compile_cbdata {
+ GHashTableIter it;
+ struct rspamd_re_cache *cache;
+ const char *cache_dir;
+ gdouble max_time;
+ gboolean silent;
+ guint total;
+ void (*cb)(guint ncompiled, GError *err, void *cbd);
+ void *cbd;
+};
+
+static void
+rspamd_re_cache_compile_err(EV_P_ ev_timer *w, GError *err,
+ struct rspamd_re_cache_hs_compile_cbdata *cbdata, bool is_fatal)
+{
+ cbdata->cb(cbdata->total, err, cbdata->cbd);
+
+ if (is_fatal) {
+ ev_timer_stop(EV_A_ w);
+ g_free(w);
+ g_free(cbdata);
+ }
+ else {
+ /* Continue compilation */
+ ev_timer_again(EV_A_ w);
+ }
+ g_error_free(err);
+}
+
+static void
+rspamd_re_cache_compile_timer_cb(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_re_cache_hs_compile_cbdata *cbdata =
+ (struct rspamd_re_cache_hs_compile_cbdata *) w->data;
+ GHashTableIter cit;
+ gpointer k, v;
+ struct rspamd_re_class *re_class;
+ gchar path[PATH_MAX], npath[PATH_MAX];
+ hs_database_t *test_db;
+ gint fd, i, n, *hs_ids = NULL, pcre_flags, re_flags;
+ rspamd_cryptobox_fast_hash_state_t crc_st;
+ guint64 crc;
+ rspamd_regexp_t *re;
+ hs_compile_error_t *hs_errors = NULL;
+ guint *hs_flags = NULL;
+ const hs_expr_ext_t **hs_exts = NULL;
+ gchar **hs_pats = NULL;
+ gchar *hs_serialized = NULL;
+ gsize serialized_len;
+ struct iovec iov[7];
+ struct rspamd_re_cache *cache;
+ GError *err;
+ pid_t our_pid = getpid();
+
+ cache = cbdata->cache;
+
+ if (!g_hash_table_iter_next(&cbdata->it, &k, &v)) {
+ /* All done */
+ ev_timer_stop(EV_A_ w);
+ cbdata->cb(cbdata->total, NULL, cbdata->cbd);
+ g_free(w);
+ g_free(cbdata);
+
+ return;
+ }
+
+ re_class = v;
+ rspamd_snprintf(path, sizeof(path), "%s%c%s.hs", cbdata->cache_dir,
+ G_DIR_SEPARATOR, re_class->hash);
+
+ if (rspamd_re_cache_is_valid_hyperscan_file(cache, path, TRUE, TRUE, NULL)) {
+
+ fd = open(path, O_RDONLY, 00600);
+
+ /* Read number of regexps */
+ g_assert(fd != -1);
+ g_assert(lseek(fd, RSPAMD_HS_MAGIC_LEN + sizeof(cache->plt), SEEK_SET) != -1);
+ g_assert(read(fd, &n, sizeof(n)) == sizeof(n));
+ close(fd);
+
+ if (re_class->type_len > 0) {
+ if (!cbdata->silent) {
+ msg_info_re_cache(
+ "skip already valid class %s(%*s) to cache %6s, %d regexps",
+ rspamd_re_cache_type_to_string(re_class->type),
+ (gint) re_class->type_len - 1,
+ re_class->type_data,
+ re_class->hash,
+ n);
+ }
+ }
+ else {
+ if (!cbdata->silent) {
+ msg_info_re_cache(
+ "skip already valid class %s to cache %6s, %d regexps",
+ rspamd_re_cache_type_to_string(re_class->type),
+ re_class->hash,
+ n);
+ }
+ }
+
+ ev_timer_again(EV_A_ w);
+ return;
+ }
+
+ rspamd_snprintf(path, sizeof(path), "%s%c%s%P-XXXXXXXXXX", cbdata->cache_dir,
+ G_DIR_SEPARATOR, re_class->hash, our_pid);
+ fd = g_mkstemp_full(path, O_CREAT | O_TRUNC | O_EXCL | O_WRONLY, 00600);
+
+ if (fd == -1) {
+ err = g_error_new(rspamd_re_cache_quark(), errno,
+ "cannot open file %s: %s", path, strerror(errno));
+ rspamd_re_cache_compile_err(EV_A_ w, err, cbdata, false);
+ return;
+ }
+
+ g_hash_table_iter_init(&cit, re_class->re);
+ n = g_hash_table_size(re_class->re);
+ hs_flags = g_new0(guint, n);
+ hs_ids = g_new0(guint, n);
+ hs_pats = g_new0(char *, n);
+ hs_exts = g_new0(const hs_expr_ext_t *, n);
+ i = 0;
+
+ while (g_hash_table_iter_next(&cit, &k, &v)) {
+ re = v;
+
+ pcre_flags = rspamd_regexp_get_pcre_flags(re);
+ re_flags = rspamd_regexp_get_flags(re);
+
+ if (re_flags & RSPAMD_REGEXP_FLAG_PCRE_ONLY) {
+ /* Do not try to compile bad regexp */
+ msg_info_re_cache(
+ "do not try compile %s to hyperscan as it is PCRE only",
+ rspamd_regexp_get_pattern(re));
+ continue;
+ }
+
+ hs_flags[i] = 0;
+ hs_exts[i] = NULL;
+#ifndef WITH_PCRE2
+ if (pcre_flags & PCRE_FLAG(UTF8)) {
+ hs_flags[i] |= HS_FLAG_UTF8;
+ }
+#else
+ if (pcre_flags & PCRE_FLAG(UTF)) {
+ hs_flags[i] |= HS_FLAG_UTF8;
+ }
+#endif
+ if (pcre_flags & PCRE_FLAG(CASELESS)) {
+ hs_flags[i] |= HS_FLAG_CASELESS;
+ }
+ if (pcre_flags & PCRE_FLAG(MULTILINE)) {
+ hs_flags[i] |= HS_FLAG_MULTILINE;
+ }
+ if (pcre_flags & PCRE_FLAG(DOTALL)) {
+ hs_flags[i] |= HS_FLAG_DOTALL;
+ }
+
+
+ if (re_flags & RSPAMD_REGEXP_FLAG_LEFTMOST) {
+ hs_flags[i] |= HS_FLAG_SOM_LEFTMOST;
+ }
+ else if (rspamd_regexp_get_maxhits(re) == 1) {
+ hs_flags[i] |= HS_FLAG_SINGLEMATCH;
+ }
+
+ gchar *pat = rspamd_re_cache_hs_pattern_from_pcre(re);
+
+ if (hs_compile(pat,
+ hs_flags[i],
+ HS_MODE_BLOCK,
+ &cache->plt,
+ &test_db,
+ &hs_errors) != HS_SUCCESS) {
+ msg_info_re_cache("cannot compile '%s' to hyperscan: '%s', try prefilter match",
+ pat,
+ hs_errors != NULL ? hs_errors->message : "unknown error");
+ hs_free_compile_error(hs_errors);
+
+ /* The approximation operation might take a significant
+ * amount of time, so we need to check if it's finite
+ */
+ if (rspamd_re_cache_is_finite(cache, re, hs_flags[i], cbdata->max_time)) {
+ hs_flags[i] |= HS_FLAG_PREFILTER;
+ hs_ids[i] = rspamd_regexp_get_cache_id(re);
+ hs_pats[i] = pat;
+ i++;
+ }
+ else {
+ g_free(pat); /* Avoid leak */
+ }
+ }
+ else {
+ hs_ids[i] = rspamd_regexp_get_cache_id(re);
+ hs_pats[i] = pat;
+ i++;
+ hs_free_database(test_db);
+ }
+ }
+ /* Adjust real re number */
+ n = i;
+
+#define CLEANUP_ALLOCATED(is_err) \
+ do { \
+ g_free(hs_flags); \
+ g_free(hs_ids); \
+ for (guint j = 0; j < i; j++) { \
+ g_free(hs_pats[j]); \
+ } \
+ g_free(hs_pats); \
+ g_free(hs_exts); \
+ if (is_err) { \
+ close(fd); \
+ unlink(path); \
+ if (hs_errors) hs_free_compile_error(hs_errors); \
+ } \
+ } while (0)
+
+ if (n > 0) {
+ /* Create the hs tree */
+ hs_errors = NULL;
+ if (hs_compile_ext_multi((const char **) hs_pats,
+ hs_flags,
+ hs_ids,
+ hs_exts,
+ n,
+ HS_MODE_BLOCK,
+ &cache->plt,
+ &test_db,
+ &hs_errors) != HS_SUCCESS) {
+
+ err = g_error_new(rspamd_re_cache_quark(), EINVAL,
+ "cannot create tree of regexp when processing '%s': %s",
+ hs_pats[hs_errors->expression], hs_errors->message);
+ CLEANUP_ALLOCATED(true);
+ rspamd_re_cache_compile_err(EV_A_ w, err, cbdata, false);
+
+ return;
+ }
+
+ if (hs_serialize_database(test_db, &hs_serialized,
+ &serialized_len) != HS_SUCCESS) {
+ err = g_error_new(rspamd_re_cache_quark(),
+ errno,
+ "cannot serialize tree of regexp for %s",
+ re_class->hash);
+
+ CLEANUP_ALLOCATED(true);
+ hs_free_database(test_db);
+ rspamd_re_cache_compile_err(EV_A_ w, err, cbdata, false);
+ return;
+ }
+
+ hs_free_database(test_db);
+
+ /*
+ * Magic - 8 bytes
+ * Platform - sizeof (platform)
+ * n - number of regexps
+ * n * <regexp ids>
+ * n * <regexp flags>
+ * crc - 8 bytes checksum
+ * <hyperscan blob>
+ */
+ rspamd_cryptobox_fast_hash_init(&crc_st, 0xdeadbabe);
+ /* IDs -> Flags -> Hs blob */
+ rspamd_cryptobox_fast_hash_update(&crc_st,
+ hs_ids, sizeof(*hs_ids) * n);
+ rspamd_cryptobox_fast_hash_update(&crc_st,
+ hs_flags, sizeof(*hs_flags) * n);
+ rspamd_cryptobox_fast_hash_update(&crc_st,
+ hs_serialized, serialized_len);
+ crc = rspamd_cryptobox_fast_hash_final(&crc_st);
+
+
+ iov[0].iov_base = (void *) rspamd_hs_magic;
+ iov[0].iov_len = RSPAMD_HS_MAGIC_LEN;
+ iov[1].iov_base = &cache->plt;
+ iov[1].iov_len = sizeof(cache->plt);
+ iov[2].iov_base = &n;
+ iov[2].iov_len = sizeof(n);
+ iov[3].iov_base = hs_ids;
+ iov[3].iov_len = sizeof(*hs_ids) * n;
+ iov[4].iov_base = hs_flags;
+ iov[4].iov_len = sizeof(*hs_flags) * n;
+ iov[5].iov_base = &crc;
+ iov[5].iov_len = sizeof(crc);
+ iov[6].iov_base = hs_serialized;
+ iov[6].iov_len = serialized_len;
+
+ if (writev(fd, iov, G_N_ELEMENTS(iov)) == -1) {
+ err = g_error_new(rspamd_re_cache_quark(),
+ errno,
+ "cannot serialize tree of regexp to %s: %s",
+ path, strerror(errno));
+
+ CLEANUP_ALLOCATED(true);
+ g_free(hs_serialized);
+
+ rspamd_re_cache_compile_err(EV_A_ w, err, cbdata, false);
+ return;
+ }
+
+ if (re_class->type_len > 0) {
+ msg_info_re_cache(
+ "compiled class %s(%*s) to cache %6s, %d/%d regexps",
+ rspamd_re_cache_type_to_string(re_class->type),
+ (gint) re_class->type_len - 1,
+ re_class->type_data,
+ re_class->hash,
+ n,
+ (gint) g_hash_table_size(re_class->re));
+ }
+ else {
+ msg_info_re_cache(
+ "compiled class %s to cache %6s, %d/%d regexps",
+ rspamd_re_cache_type_to_string(re_class->type),
+ re_class->hash,
+ n,
+ (gint) g_hash_table_size(re_class->re));
+ }
+
+ cbdata->total += n;
+ CLEANUP_ALLOCATED(false);
+
+ /* Now rename temporary file to the new .hs file */
+ rspamd_snprintf(npath, sizeof(npath), "%s%c%s.hs", cbdata->cache_dir,
+ G_DIR_SEPARATOR, re_class->hash);
+
+ if (rename(path, npath) == -1) {
+ err = g_error_new(rspamd_re_cache_quark(),
+ errno,
+ "cannot rename %s to %s: %s",
+ path, npath, strerror(errno));
+ unlink(path);
+ close(fd);
+
+ rspamd_re_cache_compile_err(EV_A_ w, err, cbdata, false);
+ return;
+ }
+
+ close(fd);
+ }
+ else {
+ err = g_error_new(rspamd_re_cache_quark(),
+ errno,
+ "no suitable regular expressions %s (%d original): "
+ "remove temporary file %s",
+ rspamd_re_cache_type_to_string(re_class->type),
+ (gint) g_hash_table_size(re_class->re),
+ path);
+
+ CLEANUP_ALLOCATED(true);
+ rspamd_re_cache_compile_err(EV_A_ w, err, cbdata, false);
+
+ return;
+ }
+
+ /* Continue process */
+ ev_timer_again(EV_A_ w);
+}
+
+#endif
+
+gint rspamd_re_cache_compile_hyperscan(struct rspamd_re_cache *cache,
+ const char *cache_dir,
+ gdouble max_time,
+ gboolean silent,
+ struct ev_loop *event_loop,
+ void (*cb)(guint ncompiled, GError *err, void *cbd),
+ void *cbd)
+{
+ g_assert(cache != NULL);
+ g_assert(cache_dir != NULL);
+
+#ifndef WITH_HYPERSCAN
+ return -1;
+#else
+ static ev_timer *timer;
+ static const ev_tstamp timer_interval = 0.1;
+ struct rspamd_re_cache_hs_compile_cbdata *cbdata;
+
+ cbdata = g_malloc0(sizeof(*cbdata));
+ g_hash_table_iter_init(&cbdata->it, cache->re_classes);
+ cbdata->cache = cache;
+ cbdata->cache_dir = cache_dir;
+ cbdata->cb = cb;
+ cbdata->cbd = cbd;
+ cbdata->max_time = max_time;
+ cbdata->silent = silent;
+ cbdata->total = 0;
+ timer = g_malloc0(sizeof(*timer));
+ timer->data = (void *) cbdata; /* static */
+
+ ev_timer_init(timer, rspamd_re_cache_compile_timer_cb,
+ timer_interval, timer_interval);
+ ev_timer_start(event_loop, timer);
+
+ return 0;
+#endif
+}
+
+gboolean
+rspamd_re_cache_is_valid_hyperscan_file(struct rspamd_re_cache *cache,
+ const char *path, gboolean silent, gboolean try_load, GError **err)
+{
+ g_assert(cache != NULL);
+ g_assert(path != NULL);
+
+#ifndef WITH_HYPERSCAN
+ return FALSE;
+#else
+ gint fd, n, ret;
+ guchar magicbuf[RSPAMD_HS_MAGIC_LEN];
+ const guchar *mb;
+ GHashTableIter it;
+ gpointer k, v;
+ struct rspamd_re_class *re_class;
+ gsize len;
+ const gchar *hash_pos;
+ hs_platform_info_t test_plt;
+ hs_database_t *test_db = NULL;
+ guchar *map, *p, *end;
+ rspamd_cryptobox_fast_hash_state_t crc_st;
+ guint64 crc, valid_crc;
+
+ len = strlen(path);
+
+ if (len < sizeof(rspamd_cryptobox_HASHBYTES + 3)) {
+ if (!silent) {
+ msg_err_re_cache("cannot open hyperscan cache file %s: too short filename",
+ path);
+ }
+ g_set_error(err, rspamd_re_cache_quark(), 0,
+ "too short filename");
+
+ return FALSE;
+ }
+
+ if (memcmp(path + len - 3, ".hs", 3) != 0) {
+ if (!silent) {
+ msg_err_re_cache("cannot open hyperscan cache file %s: not ending with .hs",
+ path);
+ }
+ g_set_error(err, rspamd_re_cache_quark(), 0,
+ "not ending with .hs");
+ return FALSE;
+ }
+
+ hash_pos = path + len - 3 - (sizeof(re_class->hash) - 1);
+ g_hash_table_iter_init(&it, cache->re_classes);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ re_class = v;
+
+ if (memcmp(hash_pos, re_class->hash, sizeof(re_class->hash) - 1) == 0) {
+ /* Open file and check magic */
+ gssize r;
+
+ fd = open(path, O_RDONLY);
+
+ if (fd == -1) {
+ if (errno != ENOENT || !silent) {
+ msg_err_re_cache("cannot open hyperscan cache file %s: %s",
+ path, strerror(errno));
+ }
+ g_set_error(err, rspamd_re_cache_quark(), 0,
+ "%s",
+ strerror(errno));
+ return FALSE;
+ }
+
+ if ((r = read(fd, magicbuf, sizeof(magicbuf))) != sizeof(magicbuf)) {
+ if (r == -1) {
+ msg_err_re_cache("cannot read magic from hyperscan "
+ "cache file %s: %s",
+ path, strerror(errno));
+ g_set_error(err, rspamd_re_cache_quark(), 0,
+ "cannot read magic: %s",
+ strerror(errno));
+ }
+ else {
+ msg_err_re_cache("truncated read magic from hyperscan "
+ "cache file %s: %z, %z wanted",
+ path, r, (gsize) sizeof(magicbuf));
+ g_set_error(err, rspamd_re_cache_quark(), 0,
+ "truncated read magic %zd, %zd wanted",
+ r, (gsize) sizeof(magicbuf));
+ }
+
+ close(fd);
+ return FALSE;
+ }
+
+ mb = rspamd_hs_magic;
+
+ if (memcmp(magicbuf, mb, sizeof(magicbuf)) != 0) {
+ msg_err_re_cache("cannot open hyperscan cache file %s: "
+ "bad magic ('%*xs', '%*xs' expected)",
+ path, (int) RSPAMD_HS_MAGIC_LEN, magicbuf,
+ (int) RSPAMD_HS_MAGIC_LEN, mb);
+
+ close(fd);
+ g_set_error(err, rspamd_re_cache_quark(), 0, "invalid magic");
+ return FALSE;
+ }
+
+ if ((r = read(fd, &test_plt, sizeof(test_plt))) != sizeof(test_plt)) {
+ if (r == -1) {
+ msg_err_re_cache("cannot read platform data from hyperscan "
+ "cache file %s: %s",
+ path, strerror(errno));
+ }
+ else {
+ msg_err_re_cache("truncated read platform data from hyperscan "
+ "cache file %s: %z, %z wanted",
+ path, r, (gsize) sizeof(magicbuf));
+ }
+
+ g_set_error(err, rspamd_re_cache_quark(), 0,
+ "cannot read platform data: %s", strerror(errno));
+
+ close(fd);
+ return FALSE;
+ }
+
+ if (test_plt.cpu_features != cache->plt.cpu_features) {
+ msg_err_re_cache("cannot open hyperscan cache file %s: "
+ "compiled for a different platform",
+ path);
+ g_set_error(err, rspamd_re_cache_quark(), 0,
+ "compiled for a different platform");
+
+ close(fd);
+ return FALSE;
+ }
+
+ close(fd);
+
+ if (try_load) {
+ map = rspamd_file_xmap(path, PROT_READ, &len, TRUE);
+
+ if (map == NULL) {
+ msg_err_re_cache("cannot mmap hyperscan cache file %s: "
+ "%s",
+ path, strerror(errno));
+ g_set_error(err, rspamd_re_cache_quark(), 0,
+ "mmap error: %s", strerror(errno));
+ return FALSE;
+ }
+
+ p = map + RSPAMD_HS_MAGIC_LEN + sizeof(test_plt);
+ end = map + len;
+ memcpy(&n, p, sizeof(n));
+ p += sizeof(gint);
+
+ if (n <= 0 || 2 * n * sizeof(gint) + /* IDs + flags */
+ sizeof(guint64) + /* crc */
+ RSPAMD_HS_MAGIC_LEN + /* header */
+ sizeof(cache->plt) >
+ len) {
+ /* Some wrong amount of regexps */
+ msg_err_re_cache("bad number of expressions in %s: %d",
+ path, n);
+ g_set_error(err, rspamd_re_cache_quark(), 0,
+ "bad number of expressions: %d", n);
+ munmap(map, len);
+ return FALSE;
+ }
+
+ /*
+ * Magic - 8 bytes
+ * Platform - sizeof (platform)
+ * n - number of regexps
+ * n * <regexp ids>
+ * n * <regexp flags>
+ * crc - 8 bytes checksum
+ * <hyperscan blob>
+ */
+
+ memcpy(&crc, p + n * 2 * sizeof(gint), sizeof(crc));
+ rspamd_cryptobox_fast_hash_init(&crc_st, 0xdeadbabe);
+ /* IDs */
+ rspamd_cryptobox_fast_hash_update(&crc_st, p, n * sizeof(gint));
+ /* Flags */
+ rspamd_cryptobox_fast_hash_update(&crc_st, p + n * sizeof(gint),
+ n * sizeof(gint));
+ /* HS database */
+ p += n * sizeof(gint) * 2 + sizeof(guint64);
+ rspamd_cryptobox_fast_hash_update(&crc_st, p, end - p);
+ valid_crc = rspamd_cryptobox_fast_hash_final(&crc_st);
+
+ if (crc != valid_crc) {
+ msg_warn_re_cache("outdated or invalid hs database in %s: "
+ "crc read %xL, crc expected %xL",
+ path, crc, valid_crc);
+ g_set_error(err, rspamd_re_cache_quark(), 0,
+ "outdated or invalid hs database, crc check failure");
+ munmap(map, len);
+
+ return FALSE;
+ }
+
+ if ((ret = hs_deserialize_database(p, end - p, &test_db)) != HS_SUCCESS) {
+ msg_err_re_cache("bad hs database in %s: %d", path, ret);
+ g_set_error(err, rspamd_re_cache_quark(), 0,
+ "deserialize error: %d", ret);
+ munmap(map, len);
+
+ return FALSE;
+ }
+
+ hs_free_database(test_db);
+ munmap(map, len);
+ }
+ /* XXX: add crc check */
+
+ return TRUE;
+ }
+ }
+
+ if (!silent) {
+ msg_warn_re_cache("unknown hyperscan cache file %s", path);
+ }
+
+ g_set_error(err, rspamd_re_cache_quark(), 0,
+ "unknown hyperscan file");
+
+ return FALSE;
+#endif
+}
+
+
+enum rspamd_hyperscan_status
+rspamd_re_cache_load_hyperscan(struct rspamd_re_cache *cache,
+ const char *cache_dir, bool try_load)
+{
+ g_assert(cache != NULL);
+ g_assert(cache_dir != NULL);
+
+#ifndef WITH_HYPERSCAN
+ return RSPAMD_HYPERSCAN_UNSUPPORTED;
+#else
+ gchar path[PATH_MAX];
+ gint fd, i, n, *hs_ids = NULL, *hs_flags = NULL, total = 0, ret;
+ GHashTableIter it;
+ gpointer k, v;
+ guint8 *map, *p;
+ struct rspamd_re_class *re_class;
+ struct rspamd_re_cache_elt *elt;
+ struct stat st;
+ gboolean has_valid = FALSE, all_valid = FALSE;
+
+ g_hash_table_iter_init(&it, cache->re_classes);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ re_class = v;
+ rspamd_snprintf(path, sizeof(path), "%s%c%s.hs", cache_dir,
+ G_DIR_SEPARATOR, re_class->hash);
+
+ if (rspamd_re_cache_is_valid_hyperscan_file(cache, path, try_load, FALSE, NULL)) {
+ msg_debug_re_cache("load hyperscan database from '%s'",
+ re_class->hash);
+
+ fd = open(path, O_RDONLY);
+
+ /* Read number of regexps */
+ g_assert(fd != -1);
+ fstat(fd, &st);
+
+ map = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
+
+ if (map == MAP_FAILED) {
+ if (!try_load) {
+ msg_err_re_cache("cannot mmap %s: %s", path, strerror(errno));
+ }
+ else {
+ msg_debug_re_cache("cannot mmap %s: %s", path, strerror(errno));
+ }
+
+ close(fd);
+ all_valid = FALSE;
+ continue;
+ }
+
+ close(fd);
+ p = map + RSPAMD_HS_MAGIC_LEN + sizeof(cache->plt);
+ n = *(gint *) p;
+
+ if (n <= 0 || 2 * n * sizeof(gint) + /* IDs + flags */
+ sizeof(guint64) + /* crc */
+ RSPAMD_HS_MAGIC_LEN + /* header */
+ sizeof(cache->plt) >
+ (gsize) st.st_size) {
+ /* Some wrong amount of regexps */
+ if (!try_load) {
+ msg_err_re_cache("bad number of expressions in %s: %d",
+ path, n);
+ }
+ else {
+ msg_debug_re_cache("bad number of expressions in %s: %d",
+ path, n);
+ }
+
+ munmap(map, st.st_size);
+ all_valid = FALSE;
+ continue;
+ }
+
+ total += n;
+ p += sizeof(n);
+ hs_ids = g_malloc(n * sizeof(*hs_ids));
+ memcpy(hs_ids, p, n * sizeof(*hs_ids));
+ p += n * sizeof(*hs_ids);
+ hs_flags = g_malloc(n * sizeof(*hs_flags));
+ memcpy(hs_flags, p, n * sizeof(*hs_flags));
+
+ /* Skip crc */
+ p += n * sizeof(*hs_ids) + sizeof(guint64);
+
+ /* Cleanup */
+ if (re_class->hs_scratch != NULL) {
+ hs_free_scratch(re_class->hs_scratch);
+ }
+
+ if (re_class->hs_db != NULL) {
+ rspamd_hyperscan_free(re_class->hs_db, false);
+ }
+
+ if (re_class->hs_ids) {
+ g_free(re_class->hs_ids);
+ }
+
+ re_class->hs_ids = NULL;
+ re_class->hs_scratch = NULL;
+ re_class->hs_db = NULL;
+ munmap(map, st.st_size);
+
+ re_class->hs_db = rspamd_hyperscan_maybe_load(path, p - map);
+ if (re_class->hs_db == NULL) {
+ if (!try_load) {
+ msg_err_re_cache("bad hs database in %s", path);
+ }
+ else {
+ msg_debug_re_cache("bad hs database in %s", path);
+ }
+ g_free(hs_ids);
+ g_free(hs_flags);
+
+ re_class->hs_ids = NULL;
+ re_class->hs_scratch = NULL;
+ re_class->hs_db = NULL;
+ all_valid = FALSE;
+
+ continue;
+ }
+
+ if ((ret = hs_alloc_scratch(rspamd_hyperscan_get_database(re_class->hs_db),
+ &re_class->hs_scratch)) != HS_SUCCESS) {
+ if (!try_load) {
+ msg_err_re_cache("bad hs database in %s; error code: %d", path, ret);
+ }
+ else {
+ msg_debug_re_cache("bad hs database in %s; error code: %d", path, ret);
+ }
+ g_free(hs_ids);
+ g_free(hs_flags);
+
+ rspamd_hyperscan_free(re_class->hs_db, true);
+ re_class->hs_ids = NULL;
+ re_class->hs_scratch = NULL;
+ re_class->hs_db = NULL;
+ all_valid = FALSE;
+
+ continue;
+ }
+
+ /*
+ * Now find hyperscan elts that are successfully compiled and
+ * specify that they should be matched using hyperscan
+ */
+ for (i = 0; i < n; i++) {
+ g_assert((gint) cache->re->len > hs_ids[i] && hs_ids[i] >= 0);
+ elt = g_ptr_array_index(cache->re, hs_ids[i]);
+
+ if (hs_flags[i] & HS_FLAG_PREFILTER) {
+ elt->match_type = RSPAMD_RE_CACHE_HYPERSCAN_PRE;
+ }
+ else {
+ elt->match_type = RSPAMD_RE_CACHE_HYPERSCAN;
+ }
+ }
+
+ re_class->hs_ids = hs_ids;
+ g_free(hs_flags);
+ re_class->nhs = n;
+
+ if (!has_valid) {
+ has_valid = TRUE;
+ all_valid = TRUE;
+ }
+ }
+ else {
+ if (!try_load) {
+ msg_err_re_cache("invalid hyperscan hash file '%s'",
+ path);
+ }
+ else {
+ msg_debug_re_cache("invalid hyperscan hash file '%s'",
+ path);
+ }
+ all_valid = FALSE;
+ continue;
+ }
+ }
+
+ if (has_valid) {
+ if (all_valid) {
+ msg_info_re_cache("full hyperscan database of %d regexps has been loaded", total);
+ cache->hyperscan_loaded = RSPAMD_HYPERSCAN_LOADED_FULL;
+ }
+ else {
+ msg_info_re_cache("partial hyperscan database of %d regexps has been loaded", total);
+ cache->hyperscan_loaded = RSPAMD_HYPERSCAN_LOADED_PARTIAL;
+ }
+ }
+ else {
+ msg_info_re_cache("hyperscan database has NOT been loaded; no valid expressions");
+ cache->hyperscan_loaded = RSPAMD_HYPERSCAN_LOAD_ERROR;
+ }
+
+
+ return cache->hyperscan_loaded;
+#endif
+}
+
+void rspamd_re_cache_add_selector(struct rspamd_re_cache *cache,
+ const gchar *sname,
+ gint ref)
+{
+ khiter_t k;
+
+ k = kh_get(lua_selectors_hash, cache->selectors, (gchar *) sname);
+
+ if (k == kh_end(cache->selectors)) {
+ gchar *cpy = g_strdup(sname);
+ gint res;
+
+ k = kh_put(lua_selectors_hash, cache->selectors, cpy, &res);
+
+ kh_value(cache->selectors, k) = ref;
+ }
+ else {
+ msg_warn_re_cache("replacing selector with name %s", sname);
+
+ if (cache->L) {
+ luaL_unref(cache->L, LUA_REGISTRYINDEX, kh_value(cache->selectors, k));
+ }
+
+ kh_value(cache->selectors, k) = ref;
+ }
+}
diff --git a/src/libserver/re_cache.h b/src/libserver/re_cache.h
new file mode 100644
index 0000000..d6449a9
--- /dev/null
+++ b/src/libserver/re_cache.h
@@ -0,0 +1,212 @@
+/*-
+ * 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_RE_CACHE_H
+#define RSPAMD_RE_CACHE_H
+
+#include "config.h"
+#include "libutil/regexp.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_re_cache;
+struct rspamd_re_runtime;
+struct rspamd_task;
+struct rspamd_config;
+
+enum rspamd_re_type {
+ RSPAMD_RE_HEADER,
+ RSPAMD_RE_RAWHEADER,
+ RSPAMD_RE_ALLHEADER,
+ RSPAMD_RE_MIMEHEADER,
+ RSPAMD_RE_MIME,
+ RSPAMD_RE_RAWMIME,
+ RSPAMD_RE_URL,
+ RSPAMD_RE_EMAIL,
+ RSPAMD_RE_BODY, /* full in SA */
+ RSPAMD_RE_SABODY, /* body in SA */
+ RSPAMD_RE_SARAWBODY, /* rawbody in SA */
+ RSPAMD_RE_WORDS, /* normalized words */
+ RSPAMD_RE_RAWWORDS, /* raw words */
+ RSPAMD_RE_STEMWORDS, /* stemmed words */
+ RSPAMD_RE_SELECTOR, /* use lua selector to process regexp */
+ RSPAMD_RE_MAX
+};
+
+struct rspamd_re_cache_stat {
+ guint64 bytes_scanned;
+ guint64 bytes_scanned_pcre;
+ guint regexp_checked;
+ guint regexp_matched;
+ guint regexp_total;
+ guint regexp_fast_cached;
+};
+
+/**
+ * Initialize re_cache persistent structure
+ */
+struct rspamd_re_cache *rspamd_re_cache_new(void);
+
+/**
+ * Add the existing regexp to the cache
+ * @param cache cache object
+ * @param re regexp object
+ * @param type type of object
+ * @param type_data associated data with the type (e.g. header name)
+ * @param datalen associated data length
+ * @param lua_cbref optional lua callback reference for matching purposes
+ */
+rspamd_regexp_t *
+rspamd_re_cache_add(struct rspamd_re_cache *cache, rspamd_regexp_t *re,
+ enum rspamd_re_type type,
+ gconstpointer type_data, gsize datalen,
+ gint lua_cbref);
+
+/**
+ * Replace regexp in the cache with another regexp
+ * @param cache cache object
+ * @param what re to replace
+ * @param with regexp object to replace the origin
+ */
+void rspamd_re_cache_replace(struct rspamd_re_cache *cache,
+ rspamd_regexp_t *what,
+ rspamd_regexp_t *with);
+
+/**
+ * Initialize and optimize re cache structure
+ */
+void rspamd_re_cache_init(struct rspamd_re_cache *cache,
+ struct rspamd_config *cfg);
+
+enum rspamd_hyperscan_status {
+ RSPAMD_HYPERSCAN_UNKNOWN = 0,
+ RSPAMD_HYPERSCAN_UNSUPPORTED,
+ RSPAMD_HYPERSCAN_LOADED_PARTIAL,
+ RSPAMD_HYPERSCAN_LOADED_FULL,
+ RSPAMD_HYPERSCAN_LOAD_ERROR,
+};
+
+/**
+ * Returns true when hyperscan is loaded
+ * @param cache
+ * @return
+ */
+enum rspamd_hyperscan_status rspamd_re_cache_is_hs_loaded(struct rspamd_re_cache *cache);
+
+/**
+ * Get runtime data for a cache
+ */
+struct rspamd_re_runtime *rspamd_re_cache_runtime_new(struct rspamd_re_cache *cache);
+
+/**
+ * Get runtime statistics
+ */
+const struct rspamd_re_cache_stat *
+rspamd_re_cache_get_stat(struct rspamd_re_runtime *rt);
+
+/**
+ * Process regexp runtime and return the result for a specific regexp
+ * @param task task object
+ * @param rt cache runtime object
+ * @param re regexp object
+ * @param type type of object
+ * @param type_data associated data with the type (e.g. header name)
+ * @param datalen associated data length
+ * @param is_strong use case sensitive match when looking for headers
+ */
+gint rspamd_re_cache_process(struct rspamd_task *task,
+ rspamd_regexp_t *re,
+ enum rspamd_re_type type,
+ gconstpointer type_data,
+ gsize datalen,
+ gboolean is_strong);
+
+int rspamd_re_cache_process_ffi(void *ptask,
+ void *pre,
+ int type,
+ void *type_data,
+ int is_strong);
+
+/**
+ * Destroy runtime data
+ */
+void rspamd_re_cache_runtime_destroy(struct rspamd_re_runtime *rt);
+
+/**
+ * Unref re cache
+ */
+void rspamd_re_cache_unref(struct rspamd_re_cache *cache);
+
+/**
+ * Retain reference to re cache
+ */
+struct rspamd_re_cache *rspamd_re_cache_ref(struct rspamd_re_cache *cache);
+
+/**
+ * Set limit for all regular expressions in the cache, returns previous limit
+ */
+guint rspamd_re_cache_set_limit(struct rspamd_re_cache *cache, guint limit);
+
+/**
+ * Convert re type to a human readable string (constant one)
+ */
+const gchar *rspamd_re_cache_type_to_string(enum rspamd_re_type type);
+
+/**
+ * Convert re type string to the type enum
+ */
+enum rspamd_re_type rspamd_re_cache_type_from_string(const char *str);
+
+struct ev_loop;
+/**
+ * Compile expressions to the hyperscan tree and store in the `cache_dir`
+ */
+gint rspamd_re_cache_compile_hyperscan(struct rspamd_re_cache *cache,
+ const char *cache_dir,
+ gdouble max_time,
+ gboolean silent,
+ struct ev_loop *event_loop,
+ void (*cb)(guint ncompiled, GError *err, void *cbd),
+ void *cbd);
+
+/**
+ * Returns TRUE if the specified file is valid hyperscan cache
+ */
+gboolean rspamd_re_cache_is_valid_hyperscan_file(struct rspamd_re_cache *cache,
+ const char *path,
+ gboolean silent,
+ gboolean try_load,
+ GError **err);
+
+/**
+ * Loads all hyperscan regexps precompiled
+ */
+enum rspamd_hyperscan_status rspamd_re_cache_load_hyperscan(
+ struct rspamd_re_cache *cache,
+ const char *cache_dir, bool try_load);
+
+/**
+ * Registers lua selector in the cache
+ */
+void rspamd_re_cache_add_selector(struct rspamd_re_cache *cache,
+ const gchar *sname, gint ref);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/redis_pool.cxx b/src/libserver/redis_pool.cxx
new file mode 100644
index 0000000..9c2d6cf
--- /dev/null
+++ b/src/libserver/redis_pool.cxx
@@ -0,0 +1,663 @@
+/*
+ * 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 "contrib/libev/ev.h"
+#include "redis_pool.h"
+#include "cfg_file.h"
+#include "contrib/hiredis/hiredis.h"
+#include "contrib/hiredis/async.h"
+#include "contrib/hiredis/adapters/libev.h"
+#include "cryptobox.h"
+#include "logger.h"
+#include "contrib/ankerl/unordered_dense.h"
+
+#include <list>
+#include <unordered_map>
+
+namespace rspamd {
+class redis_pool_elt;
+class redis_pool;
+
+#define msg_debug_rpool(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_redis_pool_log_id, "redis_pool", conn->tag, \
+ __FUNCTION__, \
+ __VA_ARGS__)
+
+INIT_LOG_MODULE(redis_pool)
+
+enum class rspamd_redis_pool_connection_state : std::uint8_t {
+ RSPAMD_REDIS_POOL_CONN_INACTIVE = 0,
+ RSPAMD_REDIS_POOL_CONN_ACTIVE,
+ RSPAMD_REDIS_POOL_CONN_FINALISING
+};
+
+struct redis_pool_connection {
+ using redis_pool_connection_ptr = std::unique_ptr<redis_pool_connection>;
+ using conn_iter_t = std::list<redis_pool_connection_ptr>::iterator;
+ struct redisAsyncContext *ctx;
+ redis_pool_elt *elt;
+ redis_pool *pool;
+ conn_iter_t elt_pos;
+ ev_timer timeout;
+ gchar tag[MEMPOOL_UID_LEN];
+ rspamd_redis_pool_connection_state state;
+
+ auto schedule_timeout() -> void;
+ ~redis_pool_connection();
+
+ explicit redis_pool_connection(redis_pool *_pool,
+ redis_pool_elt *_elt,
+ const std::string &db,
+ const std::string &username,
+ const std::string &password,
+ struct redisAsyncContext *_ctx);
+
+private:
+ static auto redis_conn_timeout_cb(EV_P_ ev_timer *w, int revents) -> void;
+ static auto redis_quit_cb(redisAsyncContext *c, void *r, void *priv) -> void;
+ static auto redis_on_disconnect(const struct redisAsyncContext *ac, int status) -> auto;
+};
+
+
+using redis_pool_key_t = std::uint64_t;
+class redis_pool;
+
+class redis_pool_elt {
+ using redis_pool_connection_ptr = std::unique_ptr<redis_pool_connection>;
+ redis_pool *pool;
+ /*
+ * These lists owns connections, so if an element is removed from both
+ * lists, it is destructed
+ */
+ std::list<redis_pool_connection_ptr> active;
+ std::list<redis_pool_connection_ptr> inactive;
+ std::list<redis_pool_connection_ptr> terminating;
+ std::string ip;
+ std::string db;
+ std::string username;
+ std::string password;
+ int port;
+ redis_pool_key_t key;
+ bool is_unix;
+
+public:
+ /* Disable copy */
+ redis_pool_elt() = delete;
+ redis_pool_elt(const redis_pool_elt &) = delete;
+ /* Enable move */
+ redis_pool_elt(redis_pool_elt &&other) = default;
+
+ explicit redis_pool_elt(redis_pool *_pool,
+ const gchar *_db, const gchar *_username,
+ const gchar *_password,
+ const char *_ip, int _port)
+ : pool(_pool), ip(_ip), port(_port),
+ key(redis_pool_elt::make_key(_db, _username, _password, _ip, _port))
+ {
+ is_unix = ip[0] == '.' || ip[0] == '/';
+
+ if (_db) {
+ db = _db;
+ }
+ if (_username) {
+ username = _username;
+ }
+ if (_password) {
+ password = _password;
+ }
+ }
+
+ auto new_connection() -> redisAsyncContext *;
+
+ auto release_connection(const redis_pool_connection *conn) -> void
+ {
+ switch (conn->state) {
+ case rspamd_redis_pool_connection_state::RSPAMD_REDIS_POOL_CONN_ACTIVE:
+ active.erase(conn->elt_pos);
+ break;
+ case rspamd_redis_pool_connection_state::RSPAMD_REDIS_POOL_CONN_INACTIVE:
+ inactive.erase(conn->elt_pos);
+ break;
+ case rspamd_redis_pool_connection_state::RSPAMD_REDIS_POOL_CONN_FINALISING:
+ terminating.erase(conn->elt_pos);
+ break;
+ }
+ }
+
+ auto move_to_inactive(redis_pool_connection *conn) -> void
+ {
+ inactive.splice(std::end(inactive), active, conn->elt_pos);
+ conn->elt_pos = std::prev(std::end(inactive));
+ }
+
+ auto move_to_terminating(redis_pool_connection *conn) -> void
+ {
+ terminating.splice(std::end(terminating), inactive, conn->elt_pos);
+ conn->elt_pos = std::prev(std::end(terminating));
+ }
+
+ inline static auto make_key(const gchar *db, const gchar *username,
+ const gchar *password, const char *ip, int port) -> redis_pool_key_t
+ {
+ rspamd_cryptobox_fast_hash_state_t st;
+
+ rspamd_cryptobox_fast_hash_init(&st, rspamd_hash_seed());
+
+ if (db) {
+ rspamd_cryptobox_fast_hash_update(&st, db, strlen(db));
+ }
+ if (username) {
+ rspamd_cryptobox_fast_hash_update(&st, username, strlen(username));
+ }
+ if (password) {
+ rspamd_cryptobox_fast_hash_update(&st, password, strlen(password));
+ }
+
+ rspamd_cryptobox_fast_hash_update(&st, ip, strlen(ip));
+ rspamd_cryptobox_fast_hash_update(&st, &port, sizeof(port));
+
+ return rspamd_cryptobox_fast_hash_final(&st);
+ }
+
+ auto num_active() const -> auto
+ {
+ return active.size();
+ }
+
+ ~redis_pool_elt()
+ {
+ rspamd_explicit_memzero(password.data(), password.size());
+ }
+
+private:
+ auto redis_async_new() -> redisAsyncContext *
+ {
+ struct redisAsyncContext *ctx;
+
+ if (is_unix) {
+ ctx = redisAsyncConnectUnix(ip.c_str());
+ }
+ else {
+ ctx = redisAsyncConnect(ip.c_str(), port);
+ }
+
+ if (ctx && ctx->err != REDIS_OK) {
+ msg_err("cannot connect to redis %s (port %d): %s", ip.c_str(), port,
+ ctx->errstr);
+ redisAsyncFree(ctx);
+
+ return nullptr;
+ }
+
+ return ctx;
+ }
+};
+
+class redis_pool final {
+ static constexpr const double default_timeout = 10.0;
+ static constexpr const unsigned default_max_conns = 100;
+
+ /* We want to have references integrity */
+ ankerl::unordered_dense::map<redisAsyncContext *,
+ redis_pool_connection *>
+ conns_by_ctx;
+ /*
+ * We store a pointer to the element in each connection, so this has to be
+ * a buckets map with pointers/references stability guarantees.
+ */
+ std::unordered_map<redis_pool_key_t, redis_pool_elt> elts_by_key;
+ bool wanna_die = false; /* Hiredis is 'clever' so we can call ourselves from destructor */
+public:
+ double timeout = default_timeout;
+ unsigned max_conns = default_max_conns;
+ struct ev_loop *event_loop;
+ struct rspamd_config *cfg;
+
+public:
+ explicit redis_pool()
+ : event_loop(nullptr), cfg(nullptr)
+ {
+ conns_by_ctx.reserve(max_conns);
+ }
+
+ /* Legacy stuff */
+ auto do_config(struct ev_loop *_loop, struct rspamd_config *_cfg) -> void
+ {
+ event_loop = _loop;
+ cfg = _cfg;
+ }
+
+ auto new_connection(const gchar *db, const gchar *username,
+ const gchar *password, const char *ip, int port) -> redisAsyncContext *;
+
+ auto release_connection(redisAsyncContext *ctx,
+ enum rspamd_redis_pool_release_type how) -> void;
+
+ auto unregister_context(redisAsyncContext *ctx) -> void
+ {
+ conns_by_ctx.erase(ctx);
+ }
+
+ auto register_context(redisAsyncContext *ctx, redis_pool_connection *conn)
+ {
+ conns_by_ctx.emplace(ctx, conn);
+ }
+
+ /* Hack to prevent Redis callbacks to be executed */
+ auto prepare_to_die() -> void
+ {
+ wanna_die = true;
+ }
+
+ ~redis_pool()
+ {
+ }
+};
+
+
+redis_pool_connection::~redis_pool_connection()
+{
+ const auto *conn = this; /* For debug */
+
+ if (state == rspamd_redis_pool_connection_state::RSPAMD_REDIS_POOL_CONN_ACTIVE) {
+ msg_debug_rpool("active connection destructed: %p", ctx);
+
+ if (ctx) {
+ pool->unregister_context(ctx);
+
+ if (!(ctx->c.flags & REDIS_FREEING)) {
+ auto *ac = ctx;
+ ctx = nullptr;
+ ac->onDisconnect = nullptr;
+ redisAsyncFree(ac);
+ }
+ }
+ }
+ else {
+ msg_debug_rpool("inactive connection destructed: %p", ctx);
+
+ ev_timer_stop(pool->event_loop, &timeout);
+ if (ctx) {
+ pool->unregister_context(ctx);
+
+ if (!(ctx->c.flags & REDIS_FREEING)) {
+ auto *ac = ctx;
+ /* To prevent on_disconnect here */
+ ctx = nullptr;
+ ac->onDisconnect = nullptr;
+ redisAsyncFree(ac);
+ }
+ }
+ }
+}
+
+auto redis_pool_connection::redis_quit_cb(redisAsyncContext *c, void *r, void *priv) -> void
+{
+ struct redis_pool_connection *conn =
+ (struct redis_pool_connection *) priv;
+
+ msg_debug_rpool("quit command reply for the connection %p",
+ conn->ctx);
+ /*
+ * The connection will be freed by hiredis itself as we are here merely after
+ * quit command has succeeded and we have timer being set already.
+ * The problem is that when this callback is called, our connection is likely
+ * dead, so probably even on_disconnect callback has been already called...
+ *
+ * Hence, the connection might already be freed, so even (conn) pointer may be
+ * inaccessible.
+ *
+ * TODO: Use refcounts to prevent this stuff to happen, the problem is how
+ * to handle Redis timeout on `quit` command in fact... The good thing is that
+ * it will not likely happen.
+ */
+}
+
+/*
+ * Called for inactive connections that due to be removed
+ */
+auto redis_pool_connection::redis_conn_timeout_cb(EV_P_ ev_timer *w, int revents) -> void
+{
+ auto *conn = (struct redis_pool_connection *) w->data;
+
+ g_assert(conn->state != rspamd_redis_pool_connection_state::RSPAMD_REDIS_POOL_CONN_ACTIVE);
+
+ if (conn->state == rspamd_redis_pool_connection_state::RSPAMD_REDIS_POOL_CONN_INACTIVE) {
+ msg_debug_rpool("scheduled soft removal of connection %p",
+ conn->ctx);
+ conn->state = rspamd_redis_pool_connection_state::RSPAMD_REDIS_POOL_CONN_FINALISING;
+ ev_timer_again(EV_A_ w);
+ redisAsyncCommand(conn->ctx, redis_pool_connection::redis_quit_cb, conn, "QUIT");
+ conn->elt->move_to_terminating(conn);
+ }
+ else {
+ /* Finalising by timeout */
+ ev_timer_stop(EV_A_ w);
+ msg_debug_rpool("final removal of connection %p, refcount: %d",
+ conn->ctx);
+
+ /* Erasure of shared pointer will cause it to be removed */
+ conn->elt->release_connection(conn);
+ }
+}
+
+auto redis_pool_connection::redis_on_disconnect(const struct redisAsyncContext *ac, int status) -> auto
+{
+ auto *conn = (struct redis_pool_connection *) ac->data;
+
+ /*
+ * Here, we know that redis itself will free this connection
+ * so, we need to do something very clever about it
+ */
+ if (conn->state != rspamd_redis_pool_connection_state::RSPAMD_REDIS_POOL_CONN_ACTIVE) {
+ /* Do nothing for active connections as it is already handled somewhere */
+ if (conn->ctx) {
+ msg_debug_rpool("inactive connection terminated: %s",
+ conn->ctx->errstr);
+ }
+
+ /* Erasure of shared pointer will cause it to be removed */
+ conn->elt->release_connection(conn);
+ }
+}
+
+auto redis_pool_connection::schedule_timeout() -> void
+{
+ const auto *conn = this; /* For debug */
+ double real_timeout;
+ auto active_elts = elt->num_active();
+
+ if (active_elts > pool->max_conns) {
+ real_timeout = pool->timeout / 2.0;
+ real_timeout = rspamd_time_jitter(real_timeout, real_timeout / 4.0);
+ }
+ else {
+ real_timeout = pool->timeout;
+ real_timeout = rspamd_time_jitter(real_timeout, real_timeout / 2.0);
+ }
+
+ msg_debug_rpool("scheduled connection %p cleanup in %.1f seconds",
+ ctx, real_timeout);
+
+ timeout.data = this;
+ /* Restore in case if these fields have been modified externally */
+ ctx->data = this;
+ redisAsyncSetDisconnectCallback(ctx, redis_pool_connection::redis_on_disconnect);
+ ev_timer_init(&timeout,
+ redis_pool_connection::redis_conn_timeout_cb,
+ real_timeout, real_timeout / 2.0);
+ ev_timer_start(pool->event_loop, &timeout);
+}
+
+
+redis_pool_connection::redis_pool_connection(redis_pool *_pool,
+ redis_pool_elt *_elt,
+ const std::string &db,
+ const std::string &username,
+ const std::string &password,
+ struct redisAsyncContext *_ctx)
+ : ctx(_ctx), elt(_elt), pool(_pool)
+{
+
+ state = rspamd_redis_pool_connection_state::RSPAMD_REDIS_POOL_CONN_ACTIVE;
+
+ pool->register_context(ctx, this);
+ ctx->data = this;
+ memset(tag, 0, sizeof(tag));
+ rspamd_random_hex(tag, sizeof(tag) - 1);
+
+ redisLibevAttach(pool->event_loop, ctx);
+ redisAsyncSetDisconnectCallback(ctx, redis_pool_connection::redis_on_disconnect);
+
+ if (!username.empty()) {
+ if (!password.empty()) {
+ redisAsyncCommand(ctx, nullptr, nullptr,
+ "AUTH %s %s", username.c_str(), password.c_str());
+ }
+ else {
+ msg_warn("Redis requires a password when username is supplied");
+ }
+ }
+ else if (!password.empty()) {
+ redisAsyncCommand(ctx, nullptr, nullptr,
+ "AUTH %s", password.c_str());
+ }
+ if (!db.empty()) {
+ redisAsyncCommand(ctx, nullptr, nullptr,
+ "SELECT %s", db.c_str());
+ }
+}
+
+auto redis_pool_elt::new_connection() -> redisAsyncContext *
+{
+ if (!inactive.empty()) {
+ decltype(inactive)::value_type conn;
+ conn.swap(inactive.back());
+ inactive.pop_back();
+
+ g_assert(conn->state != rspamd_redis_pool_connection_state::RSPAMD_REDIS_POOL_CONN_ACTIVE);
+ if (conn->ctx->err == REDIS_OK) {
+ /* Also check SO_ERROR */
+ gint err;
+ socklen_t len = sizeof(gint);
+
+ if (getsockopt(conn->ctx->c.fd, SOL_SOCKET, SO_ERROR,
+ (void *) &err, &len) == -1) {
+ err = errno;
+ }
+
+ if (err != 0) {
+ /*
+ * We cannot reuse connection, so we just recursively call
+ * this function one more time
+ */
+ return new_connection();
+ }
+ else {
+ /* Reuse connection */
+ ev_timer_stop(pool->event_loop, &conn->timeout);
+ conn->state = rspamd_redis_pool_connection_state::RSPAMD_REDIS_POOL_CONN_ACTIVE;
+ msg_debug_rpool("reused existing connection to %s:%d: %p",
+ ip.c_str(), port, conn->ctx);
+ active.emplace_front(std::move(conn));
+ active.front()->elt_pos = active.begin();
+
+ return active.front()->ctx;
+ }
+ }
+ else {
+ auto *nctx = redis_async_new();
+ if (nctx) {
+ active.emplace_front(std::make_unique<redis_pool_connection>(pool, this,
+ db.c_str(), username.c_str(), password.c_str(), nctx));
+ active.front()->elt_pos = active.begin();
+ }
+
+ return nctx;
+ }
+ }
+ else {
+ auto *nctx = redis_async_new();
+ if (nctx) {
+ active.emplace_front(std::make_unique<redis_pool_connection>(pool, this,
+ db.c_str(), username.c_str(), password.c_str(), nctx));
+ active.front()->elt_pos = active.begin();
+ }
+
+ return nctx;
+ }
+
+ RSPAMD_UNREACHABLE;
+}
+
+auto redis_pool::new_connection(const gchar *db, const gchar *username,
+ const gchar *password, const char *ip, int port) -> redisAsyncContext *
+{
+
+ if (!wanna_die) {
+ auto key = redis_pool_elt::make_key(db, username, password, ip, port);
+ auto found_elt = elts_by_key.find(key);
+
+ if (found_elt != elts_by_key.end()) {
+ auto &elt = found_elt->second;
+
+ return elt.new_connection();
+ }
+ else {
+ /* Need to create a pool */
+ auto nelt = elts_by_key.try_emplace(key,
+ this, db, username, password, ip, port);
+
+ return nelt.first->second.new_connection();
+ }
+ }
+
+ return nullptr;
+}
+
+auto redis_pool::release_connection(redisAsyncContext *ctx,
+ enum rspamd_redis_pool_release_type how) -> void
+{
+ if (!wanna_die) {
+ auto conn_it = conns_by_ctx.find(ctx);
+ if (conn_it != conns_by_ctx.end()) {
+ auto *conn = conn_it->second;
+ g_assert(conn->state == rspamd_redis_pool_connection_state::RSPAMD_REDIS_POOL_CONN_ACTIVE);
+
+ if (ctx->err != REDIS_OK) {
+ /* We need to terminate connection forcefully */
+ msg_debug_rpool("closed connection %p due to an error", conn->ctx);
+ }
+ else {
+ if (how == RSPAMD_REDIS_RELEASE_DEFAULT) {
+ /* Ensure that there are no callbacks attached to this conn */
+ if (ctx->replies.head == nullptr && (ctx->c.flags & REDIS_CONNECTED)) {
+ /* Just move it to the inactive queue */
+ conn->state = rspamd_redis_pool_connection_state::RSPAMD_REDIS_POOL_CONN_INACTIVE;
+ conn->elt->move_to_inactive(conn);
+ conn->schedule_timeout();
+ msg_debug_rpool("mark connection %p inactive", conn->ctx);
+
+ return;
+ }
+ else {
+ msg_debug_rpool("closed connection %p due to callbacks left",
+ conn->ctx);
+ }
+ }
+ else {
+ if (how == RSPAMD_REDIS_RELEASE_FATAL) {
+ msg_debug_rpool("closed connection %p due to an fatal termination",
+ conn->ctx);
+ }
+ else {
+ msg_debug_rpool("closed connection %p due to explicit termination",
+ conn->ctx);
+ }
+ }
+ }
+
+ conn->elt->release_connection(conn);
+ }
+ else {
+ msg_err("fatal internal error, connection with ctx %p is not found in the Redis pool",
+ ctx);
+ RSPAMD_UNREACHABLE;
+ }
+ }
+}
+
+}// namespace rspamd
+
+void *
+rspamd_redis_pool_init(void)
+{
+ return new rspamd::redis_pool{};
+}
+
+void rspamd_redis_pool_config(void *p,
+ struct rspamd_config *cfg,
+ struct ev_loop *ev_base)
+{
+ g_assert(p != NULL);
+ auto *pool = reinterpret_cast<class rspamd::redis_pool *>(p);
+
+ pool->do_config(ev_base, cfg);
+}
+
+
+struct redisAsyncContext *
+rspamd_redis_pool_connect(void *p,
+ const gchar *db, const gchar *username,
+ const gchar *password, const char *ip, int port)
+{
+ g_assert(p != NULL);
+ auto *pool = reinterpret_cast<class rspamd::redis_pool *>(p);
+
+ return pool->new_connection(db, username, password, ip, port);
+}
+
+
+void rspamd_redis_pool_release_connection(void *p,
+ struct redisAsyncContext *ctx, enum rspamd_redis_pool_release_type how)
+{
+ g_assert(p != NULL);
+ g_assert(ctx != NULL);
+ auto *pool = reinterpret_cast<class rspamd::redis_pool *>(p);
+
+ pool->release_connection(ctx, how);
+}
+
+
+void rspamd_redis_pool_destroy(void *p)
+{
+ auto *pool = reinterpret_cast<class rspamd::redis_pool *>(p);
+
+ pool->prepare_to_die();
+ delete pool;
+}
+
+const gchar *
+rspamd_redis_type_to_string(int type)
+{
+ const gchar *ret = "unknown";
+
+ switch (type) {
+ case REDIS_REPLY_STRING:
+ ret = "string";
+ break;
+ case REDIS_REPLY_ARRAY:
+ ret = "array";
+ break;
+ case REDIS_REPLY_INTEGER:
+ ret = "int";
+ break;
+ case REDIS_REPLY_STATUS:
+ ret = "status";
+ break;
+ case REDIS_REPLY_NIL:
+ ret = "nil";
+ break;
+ case REDIS_REPLY_ERROR:
+ ret = "error";
+ break;
+ default:
+ break;
+ }
+
+ return ret;
+}
diff --git a/src/libserver/redis_pool.h b/src/libserver/redis_pool.h
new file mode 100644
index 0000000..ecdaa0f
--- /dev/null
+++ b/src/libserver/redis_pool.h
@@ -0,0 +1,91 @@
+/*-
+ * 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 SRC_LIBSERVER_REDIS_POOL_H_
+#define SRC_LIBSERVER_REDIS_POOL_H_
+
+#include "config.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+struct rspamd_config;
+struct redisAsyncContext;
+struct ev_loop;
+
+/**
+ * Creates new redis pool
+ * @return
+ */
+void *rspamd_redis_pool_init(void);
+
+/**
+ * Configure redis pool and binds it to a specific event base
+ * @param cfg
+ * @param ev_base
+ */
+void rspamd_redis_pool_config(void *pool,
+ struct rspamd_config *cfg,
+ struct ev_loop *ev_base);
+
+
+/**
+ * Create or reuse the specific redis connection
+ * @param pool
+ * @param db
+ * @param username
+ * @param password
+ * @param ip
+ * @param port
+ * @return
+ */
+struct redisAsyncContext *rspamd_redis_pool_connect(
+ void *pool,
+ const gchar *db, const gchar *username, const gchar *password,
+ const char *ip, int port);
+
+enum rspamd_redis_pool_release_type {
+ RSPAMD_REDIS_RELEASE_DEFAULT = 0,
+ RSPAMD_REDIS_RELEASE_FATAL = 1,
+ RSPAMD_REDIS_RELEASE_ENFORCE
+};
+
+/**
+ * Release a connection to the pool
+ * @param pool
+ * @param ctx
+ */
+void rspamd_redis_pool_release_connection(void *pool,
+ struct redisAsyncContext *ctx,
+ enum rspamd_redis_pool_release_type how);
+
+/**
+ * Stops redis pool and destroys it
+ * @param pool
+ */
+void rspamd_redis_pool_destroy(void *pool);
+
+/**
+ * Missing in hiredis
+ * @param type
+ * @return
+ */
+const gchar *rspamd_redis_type_to_string(int type);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SRC_LIBSERVER_REDIS_POOL_H_ */
diff --git a/src/libserver/roll_history.c b/src/libserver/roll_history.c
new file mode 100644
index 0000000..f567b0b
--- /dev/null
+++ b/src/libserver/roll_history.c
@@ -0,0 +1,432 @@
+/*-
+ * 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 "rspamd.h"
+#include "libmime/message.h"
+#include "lua/lua_common.h"
+#include "unix-std.h"
+#include "cfg_file_private.h"
+
+static const gchar rspamd_history_magic_old[] = {'r', 's', 'h', '1'};
+
+/**
+ * Returns new roll history
+ * @param pool pool for shared memory
+ * @return new structure
+ */
+struct roll_history *
+rspamd_roll_history_new(rspamd_mempool_t *pool, guint max_rows,
+ struct rspamd_config *cfg)
+{
+ struct roll_history *history;
+ lua_State *L = cfg->lua_state;
+
+ if (pool == NULL || max_rows == 0) {
+ return NULL;
+ }
+
+ history = rspamd_mempool_alloc0_shared(pool, sizeof(struct roll_history));
+
+ /*
+ * Here, we check if there is any plugin that handles history,
+ * in this case, we disable this code completely
+ */
+ lua_getglobal(L, "rspamd_plugins");
+ if (lua_istable(L, -1)) {
+ lua_pushstring(L, "history");
+ lua_gettable(L, -2);
+
+ if (lua_istable(L, -1)) {
+ history->disabled = TRUE;
+ }
+
+ lua_pop(L, 1);
+ }
+
+ lua_pop(L, 1);
+
+ if (!history->disabled) {
+ history->rows = rspamd_mempool_alloc0_shared(pool,
+ sizeof(struct roll_history_row) * max_rows);
+ history->nrows = max_rows;
+ }
+
+ return history;
+}
+
+struct history_metric_callback_data {
+ gchar *pos;
+ gint remain;
+};
+
+static void
+roll_history_symbols_callback(gpointer key, gpointer value, void *user_data)
+{
+ struct history_metric_callback_data *cb = user_data;
+ struct rspamd_symbol_result *s = value;
+ guint wr;
+
+ if (s->flags & RSPAMD_SYMBOL_RESULT_IGNORED) {
+ return;
+ }
+
+ if (cb->remain > 0) {
+ wr = rspamd_snprintf(cb->pos, cb->remain, "%s, ", s->name);
+ cb->pos += wr;
+ cb->remain -= wr;
+ }
+}
+
+/**
+ * Update roll history with data from task
+ * @param history roll history object
+ * @param task task object
+ */
+void rspamd_roll_history_update(struct roll_history *history,
+ struct rspamd_task *task)
+{
+ guint row_num;
+ struct roll_history_row *row;
+ struct rspamd_scan_result *metric_res;
+ struct history_metric_callback_data cbdata;
+ struct rspamd_action *action;
+
+ if (history->disabled) {
+ return;
+ }
+
+ /* First of all obtain check and obtain row number */
+ g_atomic_int_compare_and_exchange(&history->cur_row, history->nrows, 0);
+#if ((GLIB_MAJOR_VERSION == 2) && (GLIB_MINOR_VERSION > 30))
+ row_num = g_atomic_int_add(&history->cur_row, 1);
+#else
+ row_num = g_atomic_int_exchange_and_add(&history->cur_row, 1);
+#endif
+
+ if (row_num < history->nrows) {
+ row = &history->rows[row_num];
+ g_atomic_int_set(&row->completed, FALSE);
+ }
+ else {
+ /* Race condition */
+ history->cur_row = 0;
+ return;
+ }
+
+ /* Add information from task to roll history */
+ if (task->from_addr) {
+ rspamd_strlcpy(row->from_addr,
+ rspamd_inet_address_to_string(task->from_addr),
+ sizeof(row->from_addr));
+ }
+ else {
+ rspamd_strlcpy(row->from_addr, "unknown", sizeof(row->from_addr));
+ }
+
+ row->timestamp = task->task_timestamp;
+
+ /* Strings */
+ if (task->message) {
+ rspamd_strlcpy(row->message_id, MESSAGE_FIELD(task, message_id),
+ sizeof(row->message_id));
+ }
+ if (task->auth_user) {
+ rspamd_strlcpy(row->user, task->auth_user, sizeof(row->user));
+ }
+ else {
+ row->user[0] = '\0';
+ }
+
+ /* Get default metric */
+ metric_res = task->result;
+
+ if (metric_res == NULL) {
+ row->symbols[0] = '\0';
+ row->action = METRIC_ACTION_NOACTION;
+ }
+ else {
+ row->score = metric_res->score;
+ action = rspamd_check_action_metric(task, NULL, NULL);
+ row->action = action->action_type;
+ row->required_score = rspamd_task_get_required_score(task, metric_res);
+ cbdata.pos = row->symbols;
+ cbdata.remain = sizeof(row->symbols);
+ rspamd_task_symbol_result_foreach(task, NULL,
+ roll_history_symbols_callback,
+ &cbdata);
+ if (cbdata.remain > 0) {
+ /* Remove last whitespace and comma */
+ *cbdata.pos-- = '\0';
+ *cbdata.pos-- = '\0';
+ *cbdata.pos = '\0';
+ }
+ }
+
+ row->scan_time = task->time_real_finish - task->task_timestamp;
+ row->len = task->msg.len;
+ g_atomic_int_set(&row->completed, TRUE);
+}
+
+/**
+ * Load previously saved history from file
+ * @param history roll history object
+ * @param filename filename to load from
+ * @return TRUE if history has been loaded
+ */
+gboolean
+rspamd_roll_history_load(struct roll_history *history, const gchar *filename)
+{
+ gint fd;
+ struct stat st;
+ gchar magic[sizeof(rspamd_history_magic_old)];
+ ucl_object_t *top;
+ const ucl_object_t *cur, *elt;
+ struct ucl_parser *parser;
+ struct roll_history_row *row;
+ guint n, i;
+
+ g_assert(history != NULL);
+ if (history->disabled) {
+ return TRUE;
+ }
+
+ if (stat(filename, &st) == -1) {
+ msg_info("cannot load history from %s: %s", filename,
+ strerror(errno));
+ return FALSE;
+ }
+
+ if ((fd = open(filename, O_RDONLY)) == -1) {
+ msg_info("cannot load history from %s: %s", filename,
+ strerror(errno));
+ return FALSE;
+ }
+
+ /* Check for old format */
+ if (read(fd, magic, sizeof(magic)) == -1) {
+ close(fd);
+ msg_info("cannot read history from %s: %s", filename,
+ strerror(errno));
+ return FALSE;
+ }
+
+ if (memcmp(magic, rspamd_history_magic_old, sizeof(magic)) == 0) {
+ close(fd);
+ msg_warn("cannot read history from old format %s, "
+ "it will be replaced after restart",
+ filename);
+ return FALSE;
+ }
+
+ parser = ucl_parser_new(0);
+
+ if (!ucl_parser_add_fd(parser, fd)) {
+ msg_warn("cannot parse history file %s: %s", filename,
+ ucl_parser_get_error(parser));
+ ucl_parser_free(parser);
+ close(fd);
+
+ return FALSE;
+ }
+
+ top = ucl_parser_get_object(parser);
+ ucl_parser_free(parser);
+ close(fd);
+
+ if (top == NULL) {
+ msg_warn("cannot parse history file %s: no object", filename);
+
+ return FALSE;
+ }
+
+ if (ucl_object_type(top) != UCL_ARRAY) {
+ msg_warn("invalid object type read from: %s", filename);
+ ucl_object_unref(top);
+
+ return FALSE;
+ }
+
+ if (top->len > history->nrows) {
+ msg_warn("stored history is larger than the current one: %ud (file) vs "
+ "%ud (history)",
+ top->len, history->nrows);
+ n = history->nrows;
+ }
+ else if (top->len < history->nrows) {
+ msg_warn(
+ "stored history is smaller than the current one: %ud (file) vs "
+ "%ud (history)",
+ top->len, history->nrows);
+ n = top->len;
+ }
+ else {
+ n = top->len;
+ }
+
+ for (i = 0; i < n; i++) {
+ cur = ucl_array_find_index(top, i);
+
+ if (cur != NULL && ucl_object_type(cur) == UCL_OBJECT) {
+ row = &history->rows[i];
+ memset(row, 0, sizeof(*row));
+
+ elt = ucl_object_lookup(cur, "time");
+
+ if (elt && ucl_object_type(elt) == UCL_FLOAT) {
+ row->timestamp = ucl_object_todouble(elt);
+ }
+
+ elt = ucl_object_lookup(cur, "id");
+
+ if (elt && ucl_object_type(elt) == UCL_STRING) {
+ rspamd_strlcpy(row->message_id, ucl_object_tostring(elt),
+ sizeof(row->message_id));
+ }
+
+ elt = ucl_object_lookup(cur, "symbols");
+
+ if (elt && ucl_object_type(elt) == UCL_STRING) {
+ rspamd_strlcpy(row->symbols, ucl_object_tostring(elt),
+ sizeof(row->symbols));
+ }
+
+ elt = ucl_object_lookup(cur, "user");
+
+ if (elt && ucl_object_type(elt) == UCL_STRING) {
+ rspamd_strlcpy(row->user, ucl_object_tostring(elt),
+ sizeof(row->user));
+ }
+
+ elt = ucl_object_lookup(cur, "from");
+
+ if (elt && ucl_object_type(elt) == UCL_STRING) {
+ rspamd_strlcpy(row->from_addr, ucl_object_tostring(elt),
+ sizeof(row->from_addr));
+ }
+
+ elt = ucl_object_lookup(cur, "len");
+
+ if (elt && ucl_object_type(elt) == UCL_INT) {
+ row->len = ucl_object_toint(elt);
+ }
+
+ elt = ucl_object_lookup(cur, "scan_time");
+
+ if (elt && ucl_object_type(elt) == UCL_FLOAT) {
+ row->scan_time = ucl_object_todouble(elt);
+ }
+
+ elt = ucl_object_lookup(cur, "score");
+
+ if (elt && ucl_object_type(elt) == UCL_FLOAT) {
+ row->score = ucl_object_todouble(elt);
+ }
+
+ elt = ucl_object_lookup(cur, "required_score");
+
+ if (elt && ucl_object_type(elt) == UCL_FLOAT) {
+ row->required_score = ucl_object_todouble(elt);
+ }
+
+ elt = ucl_object_lookup(cur, "action");
+
+ if (elt && ucl_object_type(elt) == UCL_INT) {
+ row->action = ucl_object_toint(elt);
+ }
+
+ row->completed = TRUE;
+ }
+ }
+
+ ucl_object_unref(top);
+
+ history->cur_row = n;
+
+ return TRUE;
+}
+
+/**
+ * Save history to file
+ * @param history roll history object
+ * @param filename filename to load from
+ * @return TRUE if history has been saved
+ */
+gboolean
+rspamd_roll_history_save(struct roll_history *history, const gchar *filename)
+{
+ gint fd;
+ FILE *fp;
+ ucl_object_t *obj, *elt;
+ guint i;
+ struct roll_history_row *row;
+ struct ucl_emitter_functions *emitter_func;
+
+ g_assert(history != NULL);
+
+ if (history->disabled) {
+ return TRUE;
+ }
+
+ if ((fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 00600)) == -1) {
+ msg_info("cannot save history to %s: %s", filename, strerror(errno));
+ return FALSE;
+ }
+
+ fp = fdopen(fd, "w");
+ obj = ucl_object_typed_new(UCL_ARRAY);
+
+ for (i = 0; i < history->nrows; i++) {
+ row = &history->rows[i];
+
+ if (!row->completed) {
+ continue;
+ }
+
+ elt = ucl_object_typed_new(UCL_OBJECT);
+
+ ucl_object_insert_key(elt, ucl_object_fromdouble(row->timestamp),
+ "time", 0, false);
+ ucl_object_insert_key(elt, ucl_object_fromstring(row->message_id),
+ "id", 0, false);
+ ucl_object_insert_key(elt, ucl_object_fromstring(row->symbols),
+ "symbols", 0, false);
+ ucl_object_insert_key(elt, ucl_object_fromstring(row->user),
+ "user", 0, false);
+ ucl_object_insert_key(elt, ucl_object_fromstring(row->from_addr),
+ "from", 0, false);
+ ucl_object_insert_key(elt, ucl_object_fromint(row->len),
+ "len", 0, false);
+ ucl_object_insert_key(elt, ucl_object_fromdouble(row->scan_time),
+ "scan_time", 0, false);
+ ucl_object_insert_key(elt, ucl_object_fromdouble(row->score),
+ "score", 0, false);
+ ucl_object_insert_key(elt, ucl_object_fromdouble(row->required_score),
+ "required_score", 0, false);
+ ucl_object_insert_key(elt, ucl_object_fromint(row->action),
+ "action", 0, false);
+
+ ucl_array_append(obj, elt);
+ }
+
+ emitter_func = ucl_object_emit_file_funcs(fp);
+ ucl_object_emit_full(obj, UCL_EMIT_JSON_COMPACT, emitter_func, NULL);
+ ucl_object_emit_funcs_free(emitter_func);
+ ucl_object_unref(obj);
+
+ fclose(fp);
+
+ return TRUE;
+}
diff --git a/src/libserver/roll_history.h b/src/libserver/roll_history.h
new file mode 100644
index 0000000..62bce7f
--- /dev/null
+++ b/src/libserver/roll_history.h
@@ -0,0 +1,98 @@
+/*-
+ * 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 ROLL_HISTORY_H_
+#define ROLL_HISTORY_H_
+
+#include "config.h"
+#include "mem_pool.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Roll history is a special cycled buffer for checked messages, it is designed for writing history messages
+ * and displaying them in webui
+ */
+
+#define HISTORY_MAX_ID 256
+#define HISTORY_MAX_SYMBOLS 256
+#define HISTORY_MAX_USER 32
+#define HISTORY_MAX_ADDR 32
+
+struct rspamd_task;
+struct rspamd_config;
+
+struct roll_history_row {
+ ev_tstamp timestamp;
+ gchar message_id[HISTORY_MAX_ID];
+ gchar symbols[HISTORY_MAX_SYMBOLS];
+ gchar user[HISTORY_MAX_USER];
+ gchar from_addr[HISTORY_MAX_ADDR];
+ gsize len;
+ gdouble scan_time;
+ gdouble score;
+ gdouble required_score;
+ gint action;
+ guint completed;
+};
+
+struct roll_history {
+ struct roll_history_row *rows;
+ gboolean disabled;
+ guint nrows;
+ guint cur_row;
+};
+
+/**
+ * Returns new roll history
+ * @param pool pool for shared memory
+ * @return new structure
+ */
+struct roll_history *rspamd_roll_history_new(rspamd_mempool_t *pool,
+ guint max_rows, struct rspamd_config *cfg);
+
+/**
+ * Update roll history with data from task
+ * @param history roll history object
+ * @param task task object
+ */
+void rspamd_roll_history_update(struct roll_history *history,
+ struct rspamd_task *task);
+
+/**
+ * Load previously saved history from file
+ * @param history roll history object
+ * @param filename filename to load from
+ * @return TRUE if history has been loaded
+ */
+gboolean rspamd_roll_history_load(struct roll_history *history,
+ const gchar *filename);
+
+/**
+ * Save history to file
+ * @param history roll history object
+ * @param filename filename to load from
+ * @return TRUE if history has been saved
+ */
+gboolean rspamd_roll_history_save(struct roll_history *history,
+ const gchar *filename);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ROLL_HISTORY_H_ */
diff --git a/src/libserver/rspamd_control.c b/src/libserver/rspamd_control.c
new file mode 100644
index 0000000..69af059
--- /dev/null
+++ b/src/libserver/rspamd_control.c
@@ -0,0 +1,1334 @@
+/*
+ * 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 "rspamd.h"
+#include "rspamd_control.h"
+#include "worker_util.h"
+#include "libserver/http/http_connection.h"
+#include "libserver/http/http_private.h"
+#include "libutil/libev_helper.h"
+#include "unix-std.h"
+#include "utlist.h"
+
+#ifdef HAVE_SYS_RESOURCE_H
+#include <sys/resource.h>
+#endif
+
+#ifdef WITH_HYPERSCAN
+#include "hyperscan_tools.h"
+#endif
+
+static ev_tstamp io_timeout = 30.0;
+static ev_tstamp worker_io_timeout = 0.5;
+
+struct rspamd_control_session;
+
+struct rspamd_control_reply_elt {
+ struct rspamd_control_reply reply;
+ struct rspamd_io_ev ev;
+ struct ev_loop *event_loop;
+ GQuark wrk_type;
+ pid_t wrk_pid;
+ gpointer ud;
+ gint attached_fd;
+ GHashTable *pending_elts;
+ struct rspamd_control_reply_elt *prev, *next;
+};
+
+struct rspamd_control_session {
+ gint fd;
+ struct ev_loop *event_loop;
+ struct rspamd_main *rspamd_main;
+ struct rspamd_http_connection *conn;
+ struct rspamd_control_command cmd;
+ struct rspamd_control_reply_elt *replies;
+ rspamd_inet_addr_t *addr;
+ guint replies_remain;
+ gboolean is_reply;
+};
+
+static const struct rspamd_control_cmd_match {
+ rspamd_ftok_t name;
+ enum rspamd_control_type type;
+} cmd_matches[] = {
+ {.name = {
+ .begin = "/stat",
+ .len = sizeof("/stat") - 1},
+ .type = RSPAMD_CONTROL_STAT},
+ {.name = {.begin = "/reload", .len = sizeof("/reload") - 1}, .type = RSPAMD_CONTROL_RELOAD},
+ {.name = {.begin = "/reresolve", .len = sizeof("/reresolve") - 1}, .type = RSPAMD_CONTROL_RERESOLVE},
+ {.name = {.begin = "/recompile", .len = sizeof("/recompile") - 1}, .type = RSPAMD_CONTROL_RECOMPILE},
+ {.name = {.begin = "/fuzzystat", .len = sizeof("/fuzzystat") - 1}, .type = RSPAMD_CONTROL_FUZZY_STAT},
+ {.name = {.begin = "/fuzzysync", .len = sizeof("/fuzzysync") - 1}, .type = RSPAMD_CONTROL_FUZZY_SYNC},
+};
+
+static void rspamd_control_ignore_io_handler(int fd, short what, void *ud);
+
+static void
+rspamd_control_stop_pending(struct rspamd_control_reply_elt *elt)
+{
+ GHashTable *htb;
+ /* It stops event and frees hash */
+ htb = elt->pending_elts;
+ g_hash_table_remove(elt->pending_elts, elt);
+ /* Release hash reference */
+ g_hash_table_unref(htb);
+}
+
+void rspamd_control_send_error(struct rspamd_control_session *session,
+ gint code, const gchar *error_msg, ...)
+{
+ struct rspamd_http_message *msg;
+ rspamd_fstring_t *reply;
+ va_list args;
+
+ msg = rspamd_http_new_message(HTTP_RESPONSE);
+
+ va_start(args, error_msg);
+ msg->status = rspamd_fstring_new();
+ rspamd_vprintf_fstring(&msg->status, error_msg, args);
+ va_end(args);
+
+ msg->date = time(NULL);
+ msg->code = code;
+ reply = rspamd_fstring_sized_new(msg->status->len + 16);
+ rspamd_printf_fstring(&reply, "{\"error\":\"%V\"}", msg->status);
+ rspamd_http_message_set_body_from_fstring_steal(msg, reply);
+ rspamd_http_connection_reset(session->conn);
+ rspamd_http_connection_write_message(session->conn,
+ msg,
+ NULL,
+ "application/json",
+ session,
+ io_timeout);
+}
+
+static void
+rspamd_control_send_ucl(struct rspamd_control_session *session,
+ ucl_object_t *obj)
+{
+ struct rspamd_http_message *msg;
+ rspamd_fstring_t *reply;
+
+ msg = rspamd_http_new_message(HTTP_RESPONSE);
+ msg->date = time(NULL);
+ msg->code = 200;
+ msg->status = rspamd_fstring_new_init("OK", 2);
+ reply = rspamd_fstring_sized_new(BUFSIZ);
+ rspamd_ucl_emit_fstring(obj, UCL_EMIT_JSON_COMPACT, &reply);
+ rspamd_http_message_set_body_from_fstring_steal(msg, reply);
+ rspamd_http_connection_reset(session->conn);
+ rspamd_http_connection_write_message(session->conn,
+ msg,
+ NULL,
+ "application/json",
+ session,
+ io_timeout);
+}
+
+static void
+rspamd_control_connection_close(struct rspamd_control_session *session)
+{
+ struct rspamd_control_reply_elt *elt, *telt;
+ struct rspamd_main *rspamd_main;
+
+ rspamd_main = session->rspamd_main;
+ msg_info_main("finished connection from %s",
+ rspamd_inet_address_to_string(session->addr));
+
+ DL_FOREACH_SAFE(session->replies, elt, telt)
+ {
+ rspamd_control_stop_pending(elt);
+ }
+
+ rspamd_inet_address_free(session->addr);
+ rspamd_http_connection_unref(session->conn);
+ close(session->fd);
+ g_free(session);
+}
+
+static void
+rspamd_control_write_reply(struct rspamd_control_session *session)
+{
+ ucl_object_t *rep, *cur, *workers;
+ struct rspamd_control_reply_elt *elt;
+ gchar tmpbuf[64];
+ gdouble total_utime = 0, total_systime = 0;
+ struct ucl_parser *parser;
+ guint total_conns = 0;
+
+ rep = ucl_object_typed_new(UCL_OBJECT);
+ workers = ucl_object_typed_new(UCL_OBJECT);
+
+ DL_FOREACH(session->replies, elt)
+ {
+ /* Skip incompatible worker for fuzzy_stat */
+ if ((session->cmd.type == RSPAMD_CONTROL_FUZZY_STAT ||
+ session->cmd.type == RSPAMD_CONTROL_FUZZY_SYNC) &&
+ elt->wrk_type != g_quark_from_static_string("fuzzy")) {
+ continue;
+ }
+
+ rspamd_snprintf(tmpbuf, sizeof(tmpbuf), "%P", elt->wrk_pid);
+ cur = ucl_object_typed_new(UCL_OBJECT);
+
+ ucl_object_insert_key(cur, ucl_object_fromstring(g_quark_to_string(elt->wrk_type)), "type", 0, false);
+
+ switch (session->cmd.type) {
+ case RSPAMD_CONTROL_STAT:
+ ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.stat.conns), "conns", 0, false);
+ ucl_object_insert_key(cur, ucl_object_fromdouble(elt->reply.reply.stat.utime), "utime", 0, false);
+ ucl_object_insert_key(cur, ucl_object_fromdouble(elt->reply.reply.stat.systime), "systime", 0, false);
+ ucl_object_insert_key(cur, ucl_object_fromdouble(elt->reply.reply.stat.uptime), "uptime", 0, false);
+ ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.stat.maxrss), "maxrss", 0, false);
+
+ total_utime += elt->reply.reply.stat.utime;
+ total_systime += elt->reply.reply.stat.systime;
+ total_conns += elt->reply.reply.stat.conns;
+
+ break;
+
+ case RSPAMD_CONTROL_RELOAD:
+ ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.reload.status), "status", 0, false);
+ break;
+ case RSPAMD_CONTROL_RECOMPILE:
+ ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.recompile.status), "status", 0, false);
+ break;
+ case RSPAMD_CONTROL_RERESOLVE:
+ ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.reresolve.status), "status", 0, false);
+ break;
+ case RSPAMD_CONTROL_FUZZY_STAT:
+ if (elt->attached_fd != -1) {
+ /* We have some data to parse */
+ parser = ucl_parser_new(0);
+ ucl_object_insert_key(cur,
+ ucl_object_fromint(
+ elt->reply.reply.fuzzy_stat.status),
+ "status",
+ 0,
+ false);
+
+ if (ucl_parser_add_fd(parser, elt->attached_fd)) {
+ ucl_object_insert_key(cur, ucl_parser_get_object(parser),
+ "data", 0, false);
+ ucl_parser_free(parser);
+ }
+ else {
+
+ ucl_object_insert_key(cur, ucl_object_fromstring(ucl_parser_get_error(parser)), "error", 0, false);
+
+ ucl_parser_free(parser);
+ }
+
+ ucl_object_insert_key(cur,
+ ucl_object_fromlstring(
+ elt->reply.reply.fuzzy_stat.storage_id,
+ MEMPOOL_UID_LEN - 1),
+ "id",
+ 0,
+ false);
+ }
+ else {
+ ucl_object_insert_key(cur,
+ ucl_object_fromstring("missing file"),
+ "error",
+ 0,
+ false);
+ ucl_object_insert_key(cur,
+ ucl_object_fromint(
+ elt->reply.reply.fuzzy_stat.status),
+ "status",
+ 0,
+ false);
+ }
+ break;
+ case RSPAMD_CONTROL_FUZZY_SYNC:
+ ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.fuzzy_sync.status), "status", 0, false);
+ break;
+ default:
+ break;
+ }
+
+ if (elt->attached_fd != -1) {
+ close(elt->attached_fd);
+ elt->attached_fd = -1;
+ }
+
+ ucl_object_insert_key(workers, cur, tmpbuf, 0, true);
+ }
+
+ ucl_object_insert_key(rep, workers, "workers", 0, false);
+
+ if (session->cmd.type == RSPAMD_CONTROL_STAT) {
+ /* Total stats */
+ cur = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(cur, ucl_object_fromint(total_conns), "conns", 0, false);
+ ucl_object_insert_key(cur, ucl_object_fromdouble(total_utime), "utime", 0, false);
+ ucl_object_insert_key(cur, ucl_object_fromdouble(total_systime), "systime", 0, false);
+
+ ucl_object_insert_key(rep, cur, "total", 0, false);
+ }
+
+ rspamd_control_send_ucl(session, rep);
+ ucl_object_unref(rep);
+}
+
+static void
+rspamd_control_wrk_io(gint fd, short what, gpointer ud)
+{
+ struct rspamd_control_reply_elt *elt = ud;
+ struct rspamd_control_session *session;
+ guchar fdspace[CMSG_SPACE(sizeof(int))];
+ struct iovec iov;
+ struct msghdr msg;
+ gssize r;
+
+ session = elt->ud;
+ elt->attached_fd = -1;
+
+ if (what == EV_READ) {
+ iov.iov_base = &elt->reply;
+ iov.iov_len = sizeof(elt->reply);
+ memset(&msg, 0, sizeof(msg));
+ msg.msg_control = fdspace;
+ msg.msg_controllen = sizeof(fdspace);
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+
+ r = recvmsg(fd, &msg, 0);
+ if (r == -1) {
+ msg_err("cannot read reply from the worker %P (%s): %s",
+ elt->wrk_pid, g_quark_to_string(elt->wrk_type),
+ strerror(errno));
+ }
+ else if (r >= (gssize) sizeof(elt->reply)) {
+ if (msg.msg_controllen >= CMSG_LEN(sizeof(int))) {
+ elt->attached_fd = *(int *) CMSG_DATA(CMSG_FIRSTHDR(&msg));
+ }
+ }
+ }
+ else {
+ /* Timeout waiting */
+ msg_warn("timeout waiting reply from %P (%s)",
+ elt->wrk_pid, g_quark_to_string(elt->wrk_type));
+ }
+
+ session->replies_remain--;
+ rspamd_ev_watcher_stop(session->event_loop,
+ &elt->ev);
+
+ if (session->replies_remain == 0) {
+ rspamd_control_write_reply(session);
+ }
+}
+
+static void
+rspamd_control_error_handler(struct rspamd_http_connection *conn, GError *err)
+{
+ struct rspamd_control_session *session = conn->ud;
+ struct rspamd_main *rspamd_main;
+
+ rspamd_main = session->rspamd_main;
+
+ if (!session->is_reply) {
+ msg_info_main("abnormally closing control connection: %e", err);
+ session->is_reply = TRUE;
+ rspamd_control_send_error(session, err->code, "%s", err->message);
+ }
+ else {
+ rspamd_control_connection_close(session);
+ }
+}
+
+void rspamd_pending_control_free(gpointer p)
+{
+ struct rspamd_control_reply_elt *rep_elt = (struct rspamd_control_reply_elt *) p;
+
+ rspamd_ev_watcher_stop(rep_elt->event_loop, &rep_elt->ev);
+ g_free(rep_elt);
+}
+
+static struct rspamd_control_reply_elt *
+rspamd_control_broadcast_cmd(struct rspamd_main *rspamd_main,
+ struct rspamd_control_command *cmd,
+ gint attached_fd,
+ rspamd_ev_cb handler,
+ gpointer ud,
+ pid_t except_pid)
+{
+ GHashTableIter it;
+ struct rspamd_worker *wrk;
+ struct rspamd_control_reply_elt *rep_elt, *res = NULL;
+ gpointer k, v;
+ struct msghdr msg;
+ struct cmsghdr *cmsg;
+ struct iovec iov;
+ guchar fdspace[CMSG_SPACE(sizeof(int))];
+ gssize r;
+
+ g_hash_table_iter_init(&it, rspamd_main->workers);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ wrk = v;
+
+ /* No control pipe */
+ if (wrk->control_pipe[0] == -1) {
+ continue;
+ }
+
+ if (except_pid != 0 && wrk->pid == except_pid) {
+ continue;
+ }
+
+ /* Worker is terminating, do not bother sending stuff */
+ if (wrk->state == rspamd_worker_state_terminating) {
+ continue;
+ }
+
+ memset(&msg, 0, sizeof(msg));
+
+ /* Attach fd to the message */
+ if (attached_fd != -1) {
+ memset(fdspace, 0, sizeof(fdspace));
+ msg.msg_control = fdspace;
+ msg.msg_controllen = sizeof(fdspace);
+ cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+ memcpy(CMSG_DATA(cmsg), &attached_fd, sizeof(int));
+ }
+
+ iov.iov_base = cmd;
+ iov.iov_len = sizeof(*cmd);
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+
+ r = sendmsg(wrk->control_pipe[0], &msg, 0);
+
+ if (r == sizeof(*cmd)) {
+ rep_elt = g_malloc0(sizeof(*rep_elt));
+ rep_elt->wrk_pid = wrk->pid;
+ rep_elt->wrk_type = wrk->type;
+ rep_elt->event_loop = rspamd_main->event_loop;
+ rep_elt->ud = ud;
+ rep_elt->pending_elts = g_hash_table_ref(wrk->control_events_pending);
+ rspamd_ev_watcher_init(&rep_elt->ev,
+ wrk->control_pipe[0],
+ EV_READ, handler,
+ rep_elt);
+ rspamd_ev_watcher_start(rspamd_main->event_loop,
+ &rep_elt->ev, worker_io_timeout);
+ g_hash_table_insert(wrk->control_events_pending, rep_elt, rep_elt);
+
+ DL_APPEND(res, rep_elt);
+ }
+ else {
+ msg_err_main("cannot write command %d(%z) to the worker %P(%s), fd: %d: %s",
+ (int) cmd->type, iov.iov_len,
+ wrk->pid,
+ g_quark_to_string(wrk->type),
+ wrk->control_pipe[0],
+ strerror(errno));
+ }
+ }
+
+ return res;
+}
+
+void rspamd_control_broadcast_srv_cmd(struct rspamd_main *rspamd_main,
+ struct rspamd_control_command *cmd,
+ pid_t except_pid)
+{
+ rspamd_control_broadcast_cmd(rspamd_main, cmd, -1,
+ rspamd_control_ignore_io_handler, NULL, except_pid);
+}
+
+static gint
+rspamd_control_finish_handler(struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg)
+{
+ struct rspamd_control_session *session = conn->ud;
+ rspamd_ftok_t srch;
+ guint i;
+ gboolean found = FALSE;
+ struct rspamd_control_reply_elt *cur;
+
+
+ if (!session->is_reply) {
+ if (msg->url == NULL) {
+ rspamd_control_connection_close(session);
+
+ return 0;
+ }
+
+ srch.begin = msg->url->str;
+ srch.len = msg->url->len;
+
+ session->is_reply = TRUE;
+
+ for (i = 0; i < G_N_ELEMENTS(cmd_matches); i++) {
+ if (rspamd_ftok_casecmp(&srch, &cmd_matches[i].name) == 0) {
+ session->cmd.type = cmd_matches[i].type;
+ found = TRUE;
+ break;
+ }
+ }
+
+ if (!found) {
+ rspamd_control_send_error(session, 404, "Command not defined");
+ }
+ else {
+ /* Send command to all workers */
+ session->replies = rspamd_control_broadcast_cmd(
+ session->rspamd_main, &session->cmd, -1,
+ rspamd_control_wrk_io, session, 0);
+
+ DL_FOREACH(session->replies, cur)
+ {
+ session->replies_remain++;
+ }
+ }
+ }
+ else {
+ rspamd_control_connection_close(session);
+ }
+
+
+ return 0;
+}
+
+void rspamd_control_process_client_socket(struct rspamd_main *rspamd_main,
+ gint fd, rspamd_inet_addr_t *addr)
+{
+ struct rspamd_control_session *session;
+
+ session = g_malloc0(sizeof(*session));
+
+ session->fd = fd;
+ session->conn = rspamd_http_connection_new_server(rspamd_main->http_ctx,
+ fd,
+ NULL,
+ rspamd_control_error_handler,
+ rspamd_control_finish_handler,
+ 0);
+ session->rspamd_main = rspamd_main;
+ session->addr = addr;
+ session->event_loop = rspamd_main->event_loop;
+ rspamd_http_connection_read_message(session->conn, session,
+ io_timeout);
+}
+
+struct rspamd_worker_control_data {
+ ev_io io_ev;
+ struct rspamd_worker *worker;
+ struct ev_loop *ev_base;
+ struct {
+ rspamd_worker_control_handler handler;
+ gpointer ud;
+ } handlers[RSPAMD_CONTROL_MAX];
+};
+
+static void
+rspamd_control_default_cmd_handler(gint fd,
+ gint attached_fd,
+ struct rspamd_worker_control_data *cd,
+ struct rspamd_control_command *cmd)
+{
+ struct rspamd_control_reply rep;
+ gssize r;
+ struct rusage rusg;
+ struct rspamd_config *cfg;
+ struct rspamd_main *rspamd_main;
+
+ memset(&rep, 0, sizeof(rep));
+ rep.type = cmd->type;
+ rspamd_main = cd->worker->srv;
+
+ switch (cmd->type) {
+ case RSPAMD_CONTROL_STAT:
+ if (getrusage(RUSAGE_SELF, &rusg) == -1) {
+ msg_err_main("cannot get rusage stats: %s",
+ strerror(errno));
+ }
+ else {
+ rep.reply.stat.utime = tv_to_double(&rusg.ru_utime);
+ rep.reply.stat.systime = tv_to_double(&rusg.ru_stime);
+ rep.reply.stat.maxrss = rusg.ru_maxrss;
+ }
+
+ rep.reply.stat.conns = cd->worker->nconns;
+ rep.reply.stat.uptime = rspamd_get_calendar_ticks() - cd->worker->start_time;
+ break;
+ case RSPAMD_CONTROL_RELOAD:
+ case RSPAMD_CONTROL_RECOMPILE:
+ case RSPAMD_CONTROL_HYPERSCAN_LOADED:
+ case RSPAMD_CONTROL_MONITORED_CHANGE:
+ case RSPAMD_CONTROL_FUZZY_STAT:
+ case RSPAMD_CONTROL_FUZZY_SYNC:
+ case RSPAMD_CONTROL_LOG_PIPE:
+ case RSPAMD_CONTROL_CHILD_CHANGE:
+ case RSPAMD_CONTROL_FUZZY_BLOCKED:
+ break;
+ case RSPAMD_CONTROL_RERESOLVE:
+ if (cd->worker->srv->cfg) {
+ REF_RETAIN(cd->worker->srv->cfg);
+ cfg = cd->worker->srv->cfg;
+
+ if (cfg->ups_ctx) {
+ msg_info_config("reresolving upstreams");
+ rspamd_upstream_reresolve(cfg->ups_ctx);
+ }
+
+ rep.reply.reresolve.status = 0;
+ REF_RELEASE(cfg);
+ }
+ else {
+ rep.reply.reresolve.status = EINVAL;
+ }
+ break;
+ default:
+ break;
+ }
+
+ r = write(fd, &rep, sizeof(rep));
+
+ if (r != sizeof(rep)) {
+ msg_err_main("cannot write reply to the control socket: %s",
+ strerror(errno));
+ }
+
+ if (attached_fd != -1) {
+ close(attached_fd);
+ }
+}
+
+static void
+rspamd_control_default_worker_handler(EV_P_ ev_io *w, int revents)
+{
+ struct rspamd_worker_control_data *cd =
+ (struct rspamd_worker_control_data *) w->data;
+ static struct rspamd_control_command cmd;
+ static struct msghdr msg;
+ static struct iovec iov;
+ static guchar fdspace[CMSG_SPACE(sizeof(int))];
+ gint rfd = -1;
+ gssize r;
+
+ iov.iov_base = &cmd;
+ iov.iov_len = sizeof(cmd);
+ memset(&msg, 0, sizeof(msg));
+ msg.msg_control = fdspace;
+ msg.msg_controllen = sizeof(fdspace);
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+
+ r = recvmsg(w->fd, &msg, 0);
+
+ if (r == -1) {
+ if (errno != EAGAIN && errno != EINTR) {
+ if (errno != ECONNRESET) {
+ /*
+ * In case of connection reset it means that main process
+ * has died, so do not pollute logs
+ */
+ msg_err("cannot read request from the control socket: %s",
+ strerror(errno));
+ }
+ ev_io_stop(cd->ev_base, &cd->io_ev);
+ close(w->fd);
+ }
+ }
+ else if (r < (gint) sizeof(cmd)) {
+ msg_err("short read of control command: %d of %d", (gint) r,
+ (gint) sizeof(cmd));
+
+ if (r == 0) {
+ ev_io_stop(cd->ev_base, &cd->io_ev);
+ close(w->fd);
+ }
+ }
+ else if ((gint) cmd.type >= 0 && cmd.type < RSPAMD_CONTROL_MAX) {
+
+ if (msg.msg_controllen >= CMSG_LEN(sizeof(int))) {
+ rfd = *(int *) CMSG_DATA(CMSG_FIRSTHDR(&msg));
+ }
+
+ if (cd->handlers[cmd.type].handler) {
+ cd->handlers[cmd.type].handler(cd->worker->srv,
+ cd->worker,
+ w->fd,
+ rfd,
+ &cmd,
+ cd->handlers[cmd.type].ud);
+ }
+ else {
+ rspamd_control_default_cmd_handler(w->fd, rfd, cd, &cmd);
+ }
+ }
+ else {
+ msg_err("unknown command: %d", (gint) cmd.type);
+ }
+}
+
+void rspamd_control_worker_add_default_cmd_handlers(struct rspamd_worker *worker,
+ struct ev_loop *ev_base)
+{
+ struct rspamd_worker_control_data *cd;
+
+ cd = g_malloc0(sizeof(*cd));
+ cd->worker = worker;
+ cd->ev_base = ev_base;
+
+ cd->io_ev.data = cd;
+ ev_io_init(&cd->io_ev, rspamd_control_default_worker_handler,
+ worker->control_pipe[1], EV_READ);
+ ev_io_start(ev_base, &cd->io_ev);
+
+ worker->control_data = cd;
+}
+
+/**
+ * Register custom handler for a specific control command for this worker
+ */
+void rspamd_control_worker_add_cmd_handler(struct rspamd_worker *worker,
+ enum rspamd_control_type type,
+ rspamd_worker_control_handler handler,
+ gpointer ud)
+{
+ struct rspamd_worker_control_data *cd;
+
+ g_assert(type >= 0 && type < RSPAMD_CONTROL_MAX);
+ g_assert(handler != NULL);
+ g_assert(worker->control_data != NULL);
+
+ cd = worker->control_data;
+ cd->handlers[type].handler = handler;
+ cd->handlers[type].ud = ud;
+}
+
+struct rspamd_srv_reply_data {
+ struct rspamd_worker *worker;
+ struct rspamd_main *srv;
+ gint fd;
+ struct rspamd_srv_reply rep;
+};
+
+static void
+rspamd_control_ignore_io_handler(int fd, short what, void *ud)
+{
+ struct rspamd_control_reply_elt *elt =
+ (struct rspamd_control_reply_elt *) ud;
+
+ struct rspamd_control_reply rep;
+
+ /* At this point we just ignore replies from the workers */
+ if (read(fd, &rep, sizeof(rep)) == -1) {
+ msg_debug("cannot read %d bytes: %s", (int) sizeof(rep), strerror(errno));
+ }
+ rspamd_control_stop_pending(elt);
+}
+
+static void
+rspamd_control_log_pipe_io_handler(int fd, short what, void *ud)
+{
+ struct rspamd_control_reply_elt *elt =
+ (struct rspamd_control_reply_elt *) ud;
+ struct rspamd_control_reply rep;
+
+ /* At this point we just ignore replies from the workers */
+ (void) !read(fd, &rep, sizeof(rep));
+ rspamd_control_stop_pending(elt);
+}
+
+static void
+rspamd_control_handle_on_fork(struct rspamd_srv_command *cmd,
+ struct rspamd_main *srv)
+{
+ struct rspamd_worker *parent, *child;
+
+ parent = g_hash_table_lookup(srv->workers,
+ GSIZE_TO_POINTER(cmd->cmd.on_fork.ppid));
+
+ if (parent == NULL) {
+ msg_err("cannot find parent for a forked process %P (%P child)",
+ cmd->cmd.on_fork.ppid, cmd->cmd.on_fork.cpid);
+
+ return;
+ }
+
+ if (cmd->cmd.on_fork.state == child_dead) {
+ /* We need to remove stale worker */
+ child = g_hash_table_lookup(srv->workers,
+ GSIZE_TO_POINTER(cmd->cmd.on_fork.cpid));
+
+ if (child == NULL) {
+ msg_err("cannot find child for a forked process %P (%P parent)",
+ cmd->cmd.on_fork.cpid, cmd->cmd.on_fork.ppid);
+
+ return;
+ }
+
+ REF_RELEASE(child->cf);
+ g_hash_table_remove(srv->workers,
+ GSIZE_TO_POINTER(cmd->cmd.on_fork.cpid));
+ g_hash_table_unref(child->control_events_pending);
+ g_free(child);
+ }
+ else {
+ child = g_malloc0(sizeof(struct rspamd_worker));
+ child->srv = srv;
+ child->type = parent->type;
+ child->pid = cmd->cmd.on_fork.cpid;
+ child->srv_pipe[0] = -1;
+ child->srv_pipe[1] = -1;
+ child->control_pipe[0] = -1;
+ child->control_pipe[1] = -1;
+ child->cf = parent->cf;
+ child->ppid = parent->pid;
+ REF_RETAIN(child->cf);
+ child->control_events_pending = g_hash_table_new_full(g_direct_hash, g_direct_equal,
+ NULL, rspamd_pending_control_free);
+ g_hash_table_insert(srv->workers,
+ GSIZE_TO_POINTER(cmd->cmd.on_fork.cpid), child);
+ }
+}
+
+static void
+rspamd_fill_health_reply(struct rspamd_main *srv, struct rspamd_srv_reply *rep)
+{
+ GHashTableIter it;
+ gpointer k, v;
+
+ memset(&rep->reply.health, 0, sizeof(rep->reply));
+ g_hash_table_iter_init(&it, srv->workers);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ struct rspamd_worker *wrk = (struct rspamd_worker *) v;
+
+ if (wrk->hb.nbeats < 0) {
+ rep->reply.health.workers_hb_lost++;
+ }
+ else if (rspamd_worker_is_scanner(wrk)) {
+ rep->reply.health.scanners_count++;
+ }
+
+ rep->reply.health.workers_count++;
+ }
+
+ rep->reply.status = (g_hash_table_size(srv->workers) > 0);
+}
+
+
+static void
+rspamd_srv_handler(EV_P_ ev_io *w, int revents)
+{
+ struct rspamd_worker *worker;
+ static struct rspamd_srv_command cmd;
+ struct rspamd_main *rspamd_main;
+ struct rspamd_srv_reply_data *rdata;
+ struct msghdr msg;
+ struct cmsghdr *cmsg;
+ static struct iovec iov;
+ static guchar fdspace[CMSG_SPACE(sizeof(int))];
+ gint *spair, rfd = -1;
+ gchar *nid;
+ struct rspamd_control_command wcmd;
+ gssize r;
+
+ if (revents == EV_READ) {
+ worker = (struct rspamd_worker *) w->data;
+ rspamd_main = worker->srv;
+ iov.iov_base = &cmd;
+ iov.iov_len = sizeof(cmd);
+ memset(&msg, 0, sizeof(msg));
+ msg.msg_control = fdspace;
+ msg.msg_controllen = sizeof(fdspace);
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+
+ r = recvmsg(w->fd, &msg, 0);
+
+ if (r == -1) {
+ if (errno != EAGAIN) {
+ msg_err_main("cannot read from worker's srv pipe: %s",
+ strerror(errno));
+ }
+ else {
+ return;
+ }
+ }
+ else if (r == 0) {
+ /*
+ * Usually this means that a worker is dead, so do not try to read
+ * anything
+ */
+ msg_err_main("cannot read from worker's srv pipe connection closed; command = %s",
+ rspamd_srv_command_to_string(cmd.type));
+ ev_io_stop(EV_A_ w);
+ }
+ else if (r != sizeof(cmd)) {
+ msg_err_main("cannot read from worker's srv pipe incomplete command: %d != %d; command = %s",
+ (gint) r, (gint) sizeof(cmd), rspamd_srv_command_to_string(cmd.type));
+ }
+ else {
+ rdata = g_malloc0(sizeof(*rdata));
+ rdata->worker = worker;
+ rdata->srv = rspamd_main;
+ rdata->rep.id = cmd.id;
+ rdata->rep.type = cmd.type;
+ rdata->fd = -1;
+ worker->tmp_data = rdata;
+
+ if (msg.msg_controllen >= CMSG_LEN(sizeof(int))) {
+ rfd = *(int *) CMSG_DATA(CMSG_FIRSTHDR(&msg));
+ }
+
+ switch (cmd.type) {
+ case RSPAMD_SRV_SOCKETPAIR:
+ spair = g_hash_table_lookup(rspamd_main->spairs, cmd.cmd.spair.pair_id);
+ if (spair == NULL) {
+ spair = g_malloc(sizeof(gint) * 2);
+
+ if (rspamd_socketpair(spair, cmd.cmd.spair.af) == -1) {
+ rdata->rep.reply.spair.code = errno;
+ msg_err_main("cannot create socket pair: %s", strerror(errno));
+ }
+ else {
+ nid = g_malloc(sizeof(cmd.cmd.spair.pair_id));
+ memcpy(nid, cmd.cmd.spair.pair_id,
+ sizeof(cmd.cmd.spair.pair_id));
+ g_hash_table_insert(rspamd_main->spairs, nid, spair);
+ rdata->rep.reply.spair.code = 0;
+ rdata->fd = cmd.cmd.spair.pair_num ? spair[1] : spair[0];
+ }
+ }
+ else {
+ rdata->rep.reply.spair.code = 0;
+ rdata->fd = cmd.cmd.spair.pair_num ? spair[1] : spair[0];
+ }
+ break;
+ case RSPAMD_SRV_HYPERSCAN_LOADED:
+#ifdef WITH_HYPERSCAN
+ /* Load RE cache to provide it for new forks */
+ if (rspamd_re_cache_is_hs_loaded(rspamd_main->cfg->re_cache) != RSPAMD_HYPERSCAN_LOADED_FULL ||
+ cmd.cmd.hs_loaded.forced) {
+ rspamd_re_cache_load_hyperscan(
+ rspamd_main->cfg->re_cache,
+ cmd.cmd.hs_loaded.cache_dir,
+ false);
+ }
+
+ /* After getting this notice, we can clean up old hyperscan files */
+
+ rspamd_hyperscan_notice_loaded();
+
+ msg_info_main("received hyperscan cache loaded from %s",
+ cmd.cmd.hs_loaded.cache_dir);
+
+ /* Broadcast command to all workers */
+ memset(&wcmd, 0, sizeof(wcmd));
+ wcmd.type = RSPAMD_CONTROL_HYPERSCAN_LOADED;
+ rspamd_strlcpy(wcmd.cmd.hs_loaded.cache_dir,
+ cmd.cmd.hs_loaded.cache_dir,
+ sizeof(wcmd.cmd.hs_loaded.cache_dir));
+ wcmd.cmd.hs_loaded.forced = cmd.cmd.hs_loaded.forced;
+ rspamd_control_broadcast_cmd(rspamd_main, &wcmd, rfd,
+ rspamd_control_ignore_io_handler, NULL, worker->pid);
+#endif
+ break;
+ case RSPAMD_SRV_MONITORED_CHANGE:
+ /* Broadcast command to all workers */
+ memset(&wcmd, 0, sizeof(wcmd));
+ wcmd.type = RSPAMD_CONTROL_MONITORED_CHANGE;
+ rspamd_strlcpy(wcmd.cmd.monitored_change.tag,
+ cmd.cmd.monitored_change.tag,
+ sizeof(wcmd.cmd.monitored_change.tag));
+ wcmd.cmd.monitored_change.alive = cmd.cmd.monitored_change.alive;
+ wcmd.cmd.monitored_change.sender = cmd.cmd.monitored_change.sender;
+ rspamd_control_broadcast_cmd(rspamd_main, &wcmd, rfd,
+ rspamd_control_ignore_io_handler, NULL, 0);
+ break;
+ case RSPAMD_SRV_LOG_PIPE:
+ memset(&wcmd, 0, sizeof(wcmd));
+ wcmd.type = RSPAMD_CONTROL_LOG_PIPE;
+ wcmd.cmd.log_pipe.type = cmd.cmd.log_pipe.type;
+ rspamd_control_broadcast_cmd(rspamd_main, &wcmd, rfd,
+ rspamd_control_log_pipe_io_handler, NULL, 0);
+ break;
+ case RSPAMD_SRV_ON_FORK:
+ rdata->rep.reply.on_fork.status = 0;
+ rspamd_control_handle_on_fork(&cmd, rspamd_main);
+ break;
+ case RSPAMD_SRV_HEARTBEAT:
+ worker->hb.last_event = ev_time();
+ rdata->rep.reply.heartbeat.status = 0;
+ break;
+ case RSPAMD_SRV_HEALTH:
+ rspamd_fill_health_reply(rspamd_main, &rdata->rep);
+ break;
+ case RSPAMD_SRV_NOTICE_HYPERSCAN_CACHE:
+#ifdef WITH_HYPERSCAN
+ rspamd_hyperscan_notice_known(cmd.cmd.hyperscan_cache_file.path);
+#endif
+ rdata->rep.reply.hyperscan_cache_file.unused = 0;
+ break;
+ case RSPAMD_SRV_FUZZY_BLOCKED:
+ /* Broadcast command to all workers */
+ memset(&wcmd, 0, sizeof(wcmd));
+ wcmd.type = RSPAMD_CONTROL_FUZZY_BLOCKED;
+ /* Ensure that memcpy is safe */
+ G_STATIC_ASSERT(sizeof(wcmd.cmd.fuzzy_blocked) == sizeof(cmd.cmd.fuzzy_blocked));
+ memcpy(&wcmd.cmd.fuzzy_blocked, &cmd.cmd.fuzzy_blocked, sizeof(wcmd.cmd.fuzzy_blocked));
+ rspamd_control_broadcast_cmd(rspamd_main, &wcmd, rfd,
+ rspamd_control_ignore_io_handler, NULL, worker->pid);
+ break;
+ default:
+ msg_err_main("unknown command type: %d", cmd.type);
+ break;
+ }
+
+ if (rfd != -1) {
+ /* Close our copy to avoid descriptors leak */
+ close(rfd);
+ }
+
+ /* Now plan write event and send data back */
+ w->data = rdata;
+ ev_io_stop(EV_A_ w);
+ ev_io_set(w, worker->srv_pipe[0], EV_WRITE);
+ ev_io_start(EV_A_ w);
+ }
+ }
+ else if (revents == EV_WRITE) {
+ rdata = (struct rspamd_srv_reply_data *) w->data;
+ worker = rdata->worker;
+ worker->tmp_data = NULL; /* Avoid race */
+ rspamd_main = rdata->srv;
+
+ memset(&msg, 0, sizeof(msg));
+
+ /* Attach fd to the message */
+ if (rdata->fd != -1) {
+ memset(fdspace, 0, sizeof(fdspace));
+ msg.msg_control = fdspace;
+ msg.msg_controllen = sizeof(fdspace);
+ cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+ memcpy(CMSG_DATA(cmsg), &rdata->fd, sizeof(int));
+ }
+
+ iov.iov_base = &rdata->rep;
+ iov.iov_len = sizeof(rdata->rep);
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+
+ r = sendmsg(w->fd, &msg, 0);
+
+ if (r == -1) {
+ msg_err_main("cannot write to worker's srv pipe when writing reply: %s; command = %s",
+ strerror(errno), rspamd_srv_command_to_string(rdata->rep.type));
+ }
+ else if (r != sizeof(rdata->rep)) {
+ msg_err_main("cannot write to worker's srv pipe: %d != %d; command = %s",
+ (int) r, (int) sizeof(rdata->rep),
+ rspamd_srv_command_to_string(rdata->rep.type));
+ }
+
+ g_free(rdata);
+ w->data = worker;
+ ev_io_stop(EV_A_ w);
+ ev_io_set(w, worker->srv_pipe[0], EV_READ);
+ ev_io_start(EV_A_ w);
+ }
+}
+
+void rspamd_srv_start_watching(struct rspamd_main *srv,
+ struct rspamd_worker *worker,
+ struct ev_loop *ev_base)
+{
+ g_assert(worker != NULL);
+
+ worker->tmp_data = NULL;
+ worker->srv_ev.data = worker;
+ ev_io_init(&worker->srv_ev, rspamd_srv_handler, worker->srv_pipe[0], EV_READ);
+ ev_io_start(ev_base, &worker->srv_ev);
+}
+
+struct rspamd_srv_request_data {
+ struct rspamd_worker *worker;
+ struct rspamd_srv_command cmd;
+ gint attached_fd;
+ struct rspamd_srv_reply rep;
+ rspamd_srv_reply_handler handler;
+ ev_io io_ev;
+ gpointer ud;
+};
+
+static void
+rspamd_srv_request_handler(EV_P_ ev_io *w, int revents)
+{
+ struct rspamd_srv_request_data *rd = (struct rspamd_srv_request_data *) w->data;
+ struct msghdr msg;
+ struct iovec iov;
+ guchar fdspace[CMSG_SPACE(sizeof(int))];
+ struct cmsghdr *cmsg;
+ gssize r;
+ gint rfd = -1;
+
+ if (revents == EV_WRITE) {
+ /* Send request to server */
+ memset(&msg, 0, sizeof(msg));
+
+ /* Attach fd to the message */
+ if (rd->attached_fd != -1) {
+ memset(fdspace, 0, sizeof(fdspace));
+ msg.msg_control = fdspace;
+ msg.msg_controllen = sizeof(fdspace);
+ cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+ memcpy(CMSG_DATA(cmsg), &rd->attached_fd, sizeof(int));
+ }
+
+ iov.iov_base = &rd->cmd;
+ iov.iov_len = sizeof(rd->cmd);
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+
+ r = sendmsg(w->fd, &msg, 0);
+
+ if (r == -1) {
+ if (r == ENOBUFS) {
+ /* On BSD derived systems we can have this error when trying to send
+ * requests too fast.
+ * It might be good to retry...
+ */
+ msg_info("cannot write to server pipe: %s; command = %s; retrying sending",
+ strerror(errno),
+ rspamd_srv_command_to_string(rd->cmd.type));
+ return;
+ }
+ msg_err("cannot write to server pipe: %s; command = %s", strerror(errno),
+ rspamd_srv_command_to_string(rd->cmd.type));
+ goto cleanup;
+ }
+ else if (r != sizeof(rd->cmd)) {
+ msg_err("incomplete write to the server pipe: %d != %d, command = %s",
+ (int) r, (int) sizeof(rd->cmd), rspamd_srv_command_to_string(rd->cmd.type));
+ goto cleanup;
+ }
+
+ ev_io_stop(EV_A_ w);
+ ev_io_set(w, rd->worker->srv_pipe[1], EV_READ);
+ ev_io_start(EV_A_ w);
+ }
+ else {
+ iov.iov_base = &rd->rep;
+ iov.iov_len = sizeof(rd->rep);
+ memset(&msg, 0, sizeof(msg));
+ msg.msg_control = fdspace;
+ msg.msg_controllen = sizeof(fdspace);
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+
+ r = recvmsg(w->fd, &msg, 0);
+
+ if (r == -1) {
+ msg_err("cannot read from server pipe: %s; command = %s", strerror(errno),
+ rspamd_srv_command_to_string(rd->cmd.type));
+ goto cleanup;
+ }
+
+ if (r != (gint) sizeof(rd->rep)) {
+ msg_err("cannot read from server pipe, invalid length: %d != %d; command = %s",
+ (gint) r, (int) sizeof(rd->rep), rspamd_srv_command_to_string(rd->cmd.type));
+ goto cleanup;
+ }
+
+ if (msg.msg_controllen >= CMSG_LEN(sizeof(int))) {
+ rfd = *(int *) CMSG_DATA(CMSG_FIRSTHDR(&msg));
+ }
+
+ /* Reply has been received */
+ if (rd->handler) {
+ rd->handler(rd->worker, &rd->rep, rfd, rd->ud);
+ }
+
+ goto cleanup;
+ }
+
+ return;
+
+
+cleanup:
+ ev_io_stop(EV_A_ w);
+ g_free(rd);
+}
+
+void rspamd_srv_send_command(struct rspamd_worker *worker,
+ struct ev_loop *ev_base,
+ struct rspamd_srv_command *cmd,
+ gint attached_fd,
+ rspamd_srv_reply_handler handler,
+ gpointer ud)
+{
+ struct rspamd_srv_request_data *rd;
+
+ g_assert(cmd != NULL);
+ g_assert(worker != NULL);
+
+ rd = g_malloc0(sizeof(*rd));
+ cmd->id = ottery_rand_uint64();
+ memcpy(&rd->cmd, cmd, sizeof(rd->cmd));
+ rd->handler = handler;
+ rd->ud = ud;
+ rd->worker = worker;
+ rd->rep.id = cmd->id;
+ rd->rep.type = cmd->type;
+ rd->attached_fd = attached_fd;
+
+ rd->io_ev.data = rd;
+ ev_io_init(&rd->io_ev, rspamd_srv_request_handler,
+ rd->worker->srv_pipe[1], EV_WRITE);
+ ev_io_start(ev_base, &rd->io_ev);
+}
+
+enum rspamd_control_type
+rspamd_control_command_from_string(const gchar *str)
+{
+ enum rspamd_control_type ret = RSPAMD_CONTROL_MAX;
+
+ if (!str) {
+ return ret;
+ }
+
+ if (g_ascii_strcasecmp(str, "hyperscan_loaded") == 0) {
+ ret = RSPAMD_CONTROL_HYPERSCAN_LOADED;
+ }
+ else if (g_ascii_strcasecmp(str, "stat") == 0) {
+ ret = RSPAMD_CONTROL_STAT;
+ }
+ else if (g_ascii_strcasecmp(str, "reload") == 0) {
+ ret = RSPAMD_CONTROL_RELOAD;
+ }
+ else if (g_ascii_strcasecmp(str, "reresolve") == 0) {
+ ret = RSPAMD_CONTROL_RERESOLVE;
+ }
+ else if (g_ascii_strcasecmp(str, "recompile") == 0) {
+ ret = RSPAMD_CONTROL_RECOMPILE;
+ }
+ else if (g_ascii_strcasecmp(str, "log_pipe") == 0) {
+ ret = RSPAMD_CONTROL_LOG_PIPE;
+ }
+ else if (g_ascii_strcasecmp(str, "fuzzy_stat") == 0) {
+ ret = RSPAMD_CONTROL_FUZZY_STAT;
+ }
+ else if (g_ascii_strcasecmp(str, "fuzzy_sync") == 0) {
+ ret = RSPAMD_CONTROL_FUZZY_SYNC;
+ }
+ else if (g_ascii_strcasecmp(str, "monitored_change") == 0) {
+ ret = RSPAMD_CONTROL_MONITORED_CHANGE;
+ }
+ else if (g_ascii_strcasecmp(str, "child_change") == 0) {
+ ret = RSPAMD_CONTROL_CHILD_CHANGE;
+ }
+
+ return ret;
+}
+
+const gchar *
+rspamd_control_command_to_string(enum rspamd_control_type cmd)
+{
+ const gchar *reply = "unknown";
+
+ switch (cmd) {
+ case RSPAMD_CONTROL_STAT:
+ reply = "stat";
+ break;
+ case RSPAMD_CONTROL_RELOAD:
+ reply = "reload";
+ break;
+ case RSPAMD_CONTROL_RERESOLVE:
+ reply = "reresolve";
+ break;
+ case RSPAMD_CONTROL_RECOMPILE:
+ reply = "recompile";
+ break;
+ case RSPAMD_CONTROL_HYPERSCAN_LOADED:
+ reply = "hyperscan_loaded";
+ break;
+ case RSPAMD_CONTROL_LOG_PIPE:
+ reply = "log_pipe";
+ break;
+ case RSPAMD_CONTROL_FUZZY_STAT:
+ reply = "fuzzy_stat";
+ break;
+ case RSPAMD_CONTROL_FUZZY_SYNC:
+ reply = "fuzzy_sync";
+ break;
+ case RSPAMD_CONTROL_MONITORED_CHANGE:
+ reply = "monitored_change";
+ break;
+ case RSPAMD_CONTROL_CHILD_CHANGE:
+ reply = "child_change";
+ break;
+ default:
+ break;
+ }
+
+ return reply;
+}
+
+const gchar *rspamd_srv_command_to_string(enum rspamd_srv_type cmd)
+{
+ const gchar *reply = "unknown";
+
+ switch (cmd) {
+ case RSPAMD_SRV_SOCKETPAIR:
+ reply = "socketpair";
+ break;
+ case RSPAMD_SRV_HYPERSCAN_LOADED:
+ reply = "hyperscan_loaded";
+ break;
+ case RSPAMD_SRV_MONITORED_CHANGE:
+ reply = "monitored_change";
+ break;
+ case RSPAMD_SRV_LOG_PIPE:
+ reply = "log_pipe";
+ break;
+ case RSPAMD_SRV_ON_FORK:
+ reply = "on_fork";
+ break;
+ case RSPAMD_SRV_HEARTBEAT:
+ reply = "heartbeat";
+ break;
+ case RSPAMD_SRV_HEALTH:
+ reply = "health";
+ break;
+ case RSPAMD_SRV_NOTICE_HYPERSCAN_CACHE:
+ reply = "notice_hyperscan_cache";
+ break;
+ case RSPAMD_SRV_FUZZY_BLOCKED:
+ reply = "fuzzy_blocked";
+ break;
+ }
+
+ return reply;
+}
diff --git a/src/libserver/rspamd_control.h b/src/libserver/rspamd_control.h
new file mode 100644
index 0000000..c3c861f
--- /dev/null
+++ b/src/libserver/rspamd_control.h
@@ -0,0 +1,328 @@
+/*-
+ * 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_RSPAMD_CONTROL_H
+#define RSPAMD_RSPAMD_CONTROL_H
+
+#include "config.h"
+#include "mem_pool.h"
+#include "contrib/libev/ev.h"
+
+G_BEGIN_DECLS
+
+struct rspamd_main;
+struct rspamd_worker;
+
+enum rspamd_control_type {
+ RSPAMD_CONTROL_STAT = 0,
+ RSPAMD_CONTROL_RELOAD,
+ RSPAMD_CONTROL_RERESOLVE,
+ RSPAMD_CONTROL_RECOMPILE,
+ RSPAMD_CONTROL_HYPERSCAN_LOADED,
+ RSPAMD_CONTROL_LOG_PIPE,
+ RSPAMD_CONTROL_FUZZY_STAT,
+ RSPAMD_CONTROL_FUZZY_SYNC,
+ RSPAMD_CONTROL_MONITORED_CHANGE,
+ RSPAMD_CONTROL_CHILD_CHANGE,
+ RSPAMD_CONTROL_FUZZY_BLOCKED,
+ RSPAMD_CONTROL_MAX
+};
+
+enum rspamd_srv_type {
+ RSPAMD_SRV_SOCKETPAIR = 0,
+ RSPAMD_SRV_HYPERSCAN_LOADED,
+ RSPAMD_SRV_MONITORED_CHANGE,
+ RSPAMD_SRV_LOG_PIPE,
+ RSPAMD_SRV_ON_FORK,
+ RSPAMD_SRV_HEARTBEAT,
+ RSPAMD_SRV_HEALTH,
+ RSPAMD_SRV_NOTICE_HYPERSCAN_CACHE,
+ RSPAMD_SRV_FUZZY_BLOCKED, /* Used to notify main process about a blocked ip */
+};
+
+enum rspamd_log_pipe_type {
+ RSPAMD_LOG_PIPE_SYMBOLS = 0,
+};
+#define CONTROL_PATHLEN MIN(PATH_MAX, PIPE_BUF - sizeof(int) * 2 - sizeof(gint64) * 2)
+struct rspamd_control_command {
+ enum rspamd_control_type type;
+ union {
+ struct {
+ guint unused;
+ } stat;
+ struct {
+ guint unused;
+ } reload;
+ struct {
+ guint unused;
+ } reresolve;
+ struct {
+ guint unused;
+ } recompile;
+ struct {
+ gboolean forced;
+ gchar cache_dir[CONTROL_PATHLEN];
+ } hs_loaded;
+ struct {
+ gchar tag[32];
+ gboolean alive;
+ pid_t sender;
+ } monitored_change;
+ struct {
+ enum rspamd_log_pipe_type type;
+ } log_pipe;
+ struct {
+ guint unused;
+ } fuzzy_stat;
+ struct {
+ guint unused;
+ } fuzzy_sync;
+ struct {
+ enum {
+ rspamd_child_offline,
+ rspamd_child_online,
+ rspamd_child_terminated,
+ } what;
+ pid_t pid;
+ guint additional;
+ } child_change;
+ struct {
+ union {
+ struct sockaddr sa;
+ struct sockaddr_in s4;
+ struct sockaddr_in6 s6;
+ } addr;
+ sa_family_t af;
+ } fuzzy_blocked;
+ } cmd;
+};
+
+struct rspamd_control_reply {
+ enum rspamd_control_type type;
+ union {
+ struct {
+ guint conns;
+ gdouble uptime;
+ gdouble utime;
+ gdouble systime;
+ gulong maxrss;
+ } stat;
+ struct {
+ guint status;
+ } reload;
+ struct {
+ guint status;
+ } reresolve;
+ struct {
+ guint status;
+ } recompile;
+ struct {
+ guint status;
+ } hs_loaded;
+ struct {
+ guint status;
+ } monitored_change;
+ struct {
+ guint status;
+ } log_pipe;
+ struct {
+ guint status;
+ gchar storage_id[MEMPOOL_UID_LEN];
+ } fuzzy_stat;
+ struct {
+ guint status;
+ } fuzzy_sync;
+ struct {
+ guint status;
+ } fuzzy_blocked;
+ } reply;
+};
+
+#define PAIR_ID_LEN 16
+
+struct rspamd_srv_command {
+ enum rspamd_srv_type type;
+ guint64 id;
+ union {
+ struct {
+ gint af;
+ gchar pair_id[PAIR_ID_LEN];
+ guint pair_num;
+ } spair;
+ struct {
+ gboolean forced;
+ gchar cache_dir[CONTROL_PATHLEN];
+ } hs_loaded;
+ struct {
+ gchar tag[32];
+ gboolean alive;
+ pid_t sender;
+ } monitored_change;
+ struct {
+ enum rspamd_log_pipe_type type;
+ } log_pipe;
+ struct {
+ pid_t ppid;
+ pid_t cpid;
+ enum {
+ child_create = 0,
+ child_dead,
+ } state;
+ } on_fork;
+ struct {
+ guint status;
+ /* TODO: add more fields */
+ } heartbeat;
+ struct {
+ guint status;
+ } health;
+ /* Used when a worker loads a valid hyperscan file */
+ struct {
+ char path[CONTROL_PATHLEN];
+ } hyperscan_cache_file;
+ /* Send when one worker has blocked some IP address */
+ struct {
+ union {
+ struct sockaddr sa;
+ struct sockaddr_in s4;
+ struct sockaddr_in6 s6;
+ } addr;
+ sa_family_t af;
+ } fuzzy_blocked;
+ } cmd;
+};
+
+struct rspamd_srv_reply {
+ enum rspamd_srv_type type;
+ guint64 id;
+ union {
+ struct {
+ gint code;
+ } spair;
+ struct {
+ gint forced;
+ } hs_loaded;
+ struct {
+ gint status;
+ };
+ struct {
+ enum rspamd_log_pipe_type type;
+ } log_pipe;
+ struct {
+ gint status;
+ } on_fork;
+ struct {
+ gint status;
+ } heartbeat;
+ struct {
+ guint status;
+ guint workers_count;
+ guint scanners_count;
+ guint workers_hb_lost;
+ } health;
+ struct {
+ int unused;
+ } hyperscan_cache_file;
+ struct {
+ int unused;
+ } fuzzy_blocked;
+ } reply;
+};
+
+typedef gboolean (*rspamd_worker_control_handler)(struct rspamd_main *rspamd_main,
+ struct rspamd_worker *worker,
+ gint fd,
+ gint attached_fd,
+ struct rspamd_control_command *cmd,
+ gpointer ud);
+
+typedef void (*rspamd_srv_reply_handler)(struct rspamd_worker *worker,
+ struct rspamd_srv_reply *rep, gint rep_fd,
+ gpointer ud);
+
+/**
+ * Process client socket connection
+ */
+void rspamd_control_process_client_socket(struct rspamd_main *rspamd_main,
+ gint fd, rspamd_inet_addr_t *addr);
+
+/**
+ * Register default handlers for a worker
+ */
+void rspamd_control_worker_add_default_cmd_handlers(struct rspamd_worker *worker,
+ struct ev_loop *ev_base);
+
+/**
+ * Register custom handler for a specific control command for this worker
+ */
+void rspamd_control_worker_add_cmd_handler(struct rspamd_worker *worker,
+ enum rspamd_control_type type,
+ rspamd_worker_control_handler handler,
+ gpointer ud);
+
+/**
+ * Start watching on srv pipe
+ */
+void rspamd_srv_start_watching(struct rspamd_main *srv,
+ struct rspamd_worker *worker,
+ struct ev_loop *ev_base);
+
+
+/**
+ * Send command to srv pipe and read reply calling the specified callback at the
+ * end
+ */
+void rspamd_srv_send_command(struct rspamd_worker *worker,
+ struct ev_loop *ev_base,
+ struct rspamd_srv_command *cmd,
+ gint attached_fd,
+ rspamd_srv_reply_handler handler,
+ gpointer ud);
+
+/**
+ * Broadcast srv cmd from rspamd_main to workers
+ * @param rspamd_main
+ * @param cmd
+ * @param except_pid
+ */
+void rspamd_control_broadcast_srv_cmd(struct rspamd_main *rspamd_main,
+ struct rspamd_control_command *cmd,
+ pid_t except_pid);
+
+/**
+ * Returns command from a specified string (case insensitive)
+ * @param str
+ * @return
+ */
+enum rspamd_control_type rspamd_control_command_from_string(const gchar *str);
+
+/**
+ * Returns command name from it's type
+ * @param cmd
+ * @return
+ */
+const gchar *rspamd_control_command_to_string(enum rspamd_control_type cmd);
+
+const gchar *rspamd_srv_command_to_string(enum rspamd_srv_type cmd);
+
+/**
+ * Used to cleanup pending events
+ * @param p
+ */
+void rspamd_pending_control_free(gpointer p);
+
+G_END_DECLS
+
+#endif
diff --git a/src/libserver/rspamd_symcache.h b/src/libserver/rspamd_symcache.h
new file mode 100644
index 0000000..2c67cba
--- /dev/null
+++ b/src/libserver/rspamd_symcache.h
@@ -0,0 +1,578 @@
+/*
+ * 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.
+ */
+#ifndef RSPAMD_SYMBOLS_CACHE_H
+#define RSPAMD_SYMBOLS_CACHE_H
+
+#include "config.h"
+#include "ucl.h"
+#include "cfg_file.h"
+#include "contrib/libev/ev.h"
+
+#include <lua.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_task;
+struct rspamd_config;
+struct rspamd_symcache;
+struct rspamd_worker;
+struct rspamd_symcache_dynamic_item;
+struct rspamd_symcache_item;
+struct rspamd_config_settings_elt;
+
+typedef void (*symbol_func_t)(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *item,
+ gpointer user_data);
+
+enum rspamd_symbol_type {
+ SYMBOL_TYPE_NORMAL = (1u << 0u),
+ SYMBOL_TYPE_VIRTUAL = (1u << 1u),
+ SYMBOL_TYPE_CALLBACK = (1u << 2u),
+ SYMBOL_TYPE_GHOST = (1u << 3u),
+ SYMBOL_TYPE_SKIPPED = (1u << 4u),
+ SYMBOL_TYPE_COMPOSITE = (1u << 5u),
+ SYMBOL_TYPE_CLASSIFIER = (1u << 6u),
+ SYMBOL_TYPE_FINE = (1u << 7u),
+ SYMBOL_TYPE_EMPTY = (1u << 8u), /* Allow execution on empty tasks */
+ SYMBOL_TYPE_CONNFILTER = (1u << 9u), /* Connection stage filter */
+ SYMBOL_TYPE_PREFILTER = (1u << 10u),
+ SYMBOL_TYPE_POSTFILTER = (1u << 11u),
+ SYMBOL_TYPE_NOSTAT = (1u << 12u), /* Skip as statistical symbol */
+ SYMBOL_TYPE_IDEMPOTENT = (1u << 13u), /* Symbol cannot change metric */
+ SYMBOL_TYPE_TRIVIAL = (1u << 14u), /* Symbol is trivial */
+ SYMBOL_TYPE_MIME_ONLY = (1u << 15u), /* Symbol is mime only */
+ SYMBOL_TYPE_EXPLICIT_DISABLE = (1u << 16u), /* Symbol should be disabled explicitly only */
+ SYMBOL_TYPE_IGNORE_PASSTHROUGH = (1u << 17u), /* Symbol ignores passthrough result */
+ SYMBOL_TYPE_EXPLICIT_ENABLE = (1u << 18u), /* Symbol should be enabled explicitly only */
+ SYMBOL_TYPE_USE_CORO = (1u << 19u), /* Symbol uses lua coroutines */
+};
+
+/**
+ * Abstract structure for saving callback data for symbols
+ */
+struct rspamd_abstract_callback_data {
+ guint64 magic;
+ char data[];
+};
+
+/**
+ * Shared memory block specific for each symbol
+ */
+struct rspamd_symcache_item_stat {
+ struct rspamd_counter_data time_counter;
+ gdouble avg_time;
+ gdouble weight;
+ guint hits;
+ guint64 total_hits;
+ struct rspamd_counter_data frequency_counter;
+ gdouble avg_frequency;
+ gdouble stddev_frequency;
+};
+
+/**
+ * Creates new cache structure
+ * @return
+ */
+struct rspamd_symcache *rspamd_symcache_new(struct rspamd_config *cfg);
+
+/**
+ * Remove the cache structure syncing data if needed
+ * @param cache
+ */
+void rspamd_symcache_destroy(struct rspamd_symcache *cache);
+
+/**
+ * Saves symbols cache to disk if possible
+ * @param cache
+ */
+void rspamd_symcache_save(struct rspamd_symcache *cache);
+
+/**
+ * Load symbols cache from file, must be called _after_ init_symbols_cache
+ */
+gboolean rspamd_symcache_init(struct rspamd_symcache *cache);
+
+/**
+ * Generic function to register a symbol
+ * @param cache
+ * @param name
+ * @param weight
+ * @param priority
+ * @param func
+ * @param user_data
+ * @param type
+ * @param parent
+ */
+gint rspamd_symcache_add_symbol(struct rspamd_symcache *cache,
+ const gchar *name,
+ gint priority,
+ symbol_func_t func,
+ gpointer user_data,
+ int type,
+ gint parent);
+
+/**
+ * Adds augmentation to the symbol
+ * @param cache
+ * @param sym_id
+ * @param augmentation
+ * @return
+ */
+bool rspamd_symcache_add_symbol_augmentation(struct rspamd_symcache *cache,
+ int sym_id,
+ const char *augmentation,
+ const char *value);
+
+/**
+ * Add callback to be executed whenever symbol has peak value
+ * @param cache
+ * @param cbref
+ */
+void rspamd_symcache_set_peak_callback(struct rspamd_symcache *cache,
+ gint cbref);
+
+/**
+ * Add delayed condition to the specific symbol in cache. So symbol can be absent
+ * to the moment of addition
+ * @param cache
+ * @param id id of symbol
+ * @param L lua state pointer
+ * @param cbref callback reference (returned by luaL_ref)
+ * @return TRUE if condition has been added
+ */
+gboolean rspamd_symcache_add_condition_delayed(struct rspamd_symcache *cache,
+ const gchar *sym,
+ lua_State *L, gint cbref);
+
+/**
+ * Find symbol in cache by id and returns its id resolving virtual symbols if
+ * applicable
+ * @param cache
+ * @param name
+ * @return id of symbol or (-1) if a symbol has not been found
+ */
+gint rspamd_symcache_find_symbol(struct rspamd_symcache *cache,
+ const gchar *name);
+
+/**
+ * Get statistics for a specific symbol
+ * @param cache
+ * @param name
+ * @param frequency
+ * @param tm
+ * @return
+ */
+gboolean rspamd_symcache_stat_symbol(struct rspamd_symcache *cache,
+ const gchar *name,
+ gdouble *frequency,
+ gdouble *freq_stddev,
+ gdouble *tm,
+ guint *nhits);
+
+/**
+ * Returns number of symbols registered in symbols cache
+ * @param cache
+ * @return number of symbols in the cache
+ */
+guint rspamd_symcache_stats_symbols_count(struct rspamd_symcache *cache);
+
+/**
+ * Validate cache items against theirs weights defined in metrics
+ * @param cache symbols cache
+ * @param cfg configuration
+ * @param strict do strict checks - symbols MUST be described in metrics
+ */
+gboolean rspamd_symcache_validate(struct rspamd_symcache *cache,
+ struct rspamd_config *cfg,
+ gboolean strict);
+
+/**
+ * Call function for cached symbol using saved callback
+ * @param task task object
+ * @param cache symbols cache
+ * @param saved_item pointer to currently saved item
+ */
+gboolean rspamd_symcache_process_symbols(struct rspamd_task *task,
+ struct rspamd_symcache *cache,
+ guint stage);
+
+/**
+ * Return statistics about the cache as ucl object (array of objects one per item)
+ * @param cache
+ * @return
+ */
+ucl_object_t *rspamd_symcache_counters(struct rspamd_symcache *cache);
+
+/**
+ * Start cache reloading
+ * @param cache
+ * @param ev_base
+ */
+void *rspamd_symcache_start_refresh(struct rspamd_symcache *cache,
+ struct ev_loop *ev_base,
+ struct rspamd_worker *w);
+
+/**
+ * Increases counter for a specific symbol
+ * @param cache
+ * @param symbol
+ */
+void rspamd_symcache_inc_frequency(struct rspamd_symcache *_cache,
+ struct rspamd_symcache_item *item,
+ const gchar *sym_name);
+
+/**
+ * Add delayed dependency that is resolved on cache post-load routine
+ * @param cache
+ * @param from
+ * @param to
+ */
+void rspamd_symcache_add_delayed_dependency(struct rspamd_symcache *cache,
+ const gchar *from, const gchar *to);
+
+/**
+ * Get abstract callback data for a symbol (or its parent symbol)
+ * @param cache cache object
+ * @param symbol symbol name
+ * @return abstract callback data or NULL if symbol is absent or has no data attached
+ */
+struct rspamd_abstract_callback_data *rspamd_symcache_get_cbdata(
+ struct rspamd_symcache *cache, const gchar *symbol);
+
+/**
+ * Returns symbol's parent name (or symbol name itself)
+ * @param cache
+ * @param symbol
+ * @return
+ */
+const gchar *rspamd_symcache_get_parent(struct rspamd_symcache *cache,
+ const gchar *symbol);
+
+guint rspamd_symcache_get_symbol_flags(struct rspamd_symcache *cache,
+ const gchar *symbol);
+
+void rspamd_symcache_get_symbol_details(struct rspamd_symcache *cache,
+ const gchar *symbol,
+ ucl_object_t *this_sym_ucl);
+
+
+/**
+ * Process settings for task
+ * @param task
+ * @param cache
+ * @return
+ */
+gboolean rspamd_symcache_process_settings(struct rspamd_task *task,
+ struct rspamd_symcache *cache);
+
+
+/**
+ * Checks if a symbol specified has been checked (or disabled)
+ * @param task
+ * @param cache
+ * @param symbol
+ * @return
+ */
+gboolean rspamd_symcache_is_checked(struct rspamd_task *task,
+ struct rspamd_symcache *cache,
+ const gchar *symbol);
+
+/**
+ * Returns checksum for all cache items
+ * @param cache
+ * @return
+ */
+guint64 rspamd_symcache_get_cksum(struct rspamd_symcache *cache);
+
+/**
+ * Checks if a symbols is enabled (not checked and conditions return true if present)
+ * @param task
+ * @param cache
+ * @param symbol
+ * @return
+ */
+gboolean rspamd_symcache_is_symbol_enabled(struct rspamd_task *task,
+ struct rspamd_symcache *cache,
+ const gchar *symbol);
+
+/**
+ * Enable this symbol for task
+ * @param task
+ * @param cache
+ * @param symbol
+ * @return TRUE if a symbol has been enabled (not executed before)
+ */
+gboolean rspamd_symcache_enable_symbol(struct rspamd_task *task,
+ struct rspamd_symcache *cache,
+ const gchar *symbol);
+
+/**
+ * Enable this symbol for task
+ * @param task
+ * @param cache
+ * @param symbol
+ * @return TRUE if a symbol has been disabled (not executed before)
+ */
+gboolean rspamd_symcache_disable_symbol(struct rspamd_task *task,
+ struct rspamd_symcache *cache,
+ const gchar *symbol);
+
+/**
+ * Disable execution of a symbol or a pattern (a string enclosed in `//`) permanently
+ * @param task
+ * @param cache
+ * @param symbol
+ * @return
+ */
+void rspamd_symcache_disable_symbol_static(struct rspamd_symcache *cache,
+ const gchar *symbol);
+/**
+ * Add a symbol or a pattern to the list of explicitly and statically enabled symbols
+ * @param cache
+ * @param symbol
+ * @return
+ */
+void rspamd_symcache_enable_symbol_static(struct rspamd_symcache *cache,
+ const gchar *symbol);
+
+/**
+ * Process specific function for each cache element (in order they are added)
+ * @param cache
+ * @param func
+ * @param ud
+ */
+void rspamd_symcache_foreach(struct rspamd_symcache *cache,
+ void (*func)(struct rspamd_symcache_item *item, gpointer /* userdata */),
+ gpointer ud);
+
+/**
+ * Returns the current item being processed (if any)
+ * @param task
+ * @return
+ */
+struct rspamd_symcache_dynamic_item *rspamd_symcache_get_cur_item(struct rspamd_task *task);
+
+/**
+ * Replaces the current item being processed.
+ * Returns the current item being processed (if any)
+ * @param task
+ * @param item
+ * @return
+ */
+struct rspamd_symcache_dynamic_item *rspamd_symcache_set_cur_item(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *item);
+
+
+/**
+ * Finalize the current async element potentially calling its deps
+ */
+void rspamd_symcache_finalize_item(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *item);
+
+/*
+ * Increase number of async events pending for an item
+ */
+guint rspamd_symcache_item_async_inc_full(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *item,
+ const gchar *subsystem,
+ const gchar *loc);
+
+#define rspamd_symcache_item_async_inc(task, item, subsystem) \
+ rspamd_symcache_item_async_inc_full(task, item, subsystem, G_STRLOC)
+
+/*
+ * Decrease number of async events pending for an item, asserts if no events pending
+ */
+guint rspamd_symcache_item_async_dec_full(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *item,
+ const gchar *subsystem,
+ const gchar *loc);
+
+#define rspamd_symcache_item_async_dec(task, item, subsystem) \
+ rspamd_symcache_item_async_dec_full(task, item, subsystem, G_STRLOC)
+
+/**
+ * Decrease number of async events pending for an item, asserts if no events pending
+ * If no events are left, this function calls `rspamd_symbols_cache_finalize_item` and returns TRUE
+ * @param task
+ * @param item
+ * @return
+ */
+gboolean rspamd_symcache_item_async_dec_check_full(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *item,
+ const gchar *subsystem,
+ const gchar *loc);
+
+#define rspamd_symcache_item_async_dec_check(task, item, subsystem) \
+ rspamd_symcache_item_async_dec_check_full(task, item, subsystem, G_STRLOC)
+
+/**
+ * Disables execution of all symbols, excluding those specified in `skip_mask`
+ * @param task
+ * @param cache
+ * @param skip_mask
+ */
+void rspamd_symcache_disable_all_symbols(struct rspamd_task *task,
+ struct rspamd_symcache *cache,
+ guint skip_mask);
+
+/**
+ * Iterates over the list of the enabled composites calling specified function
+ * @param task
+ * @param cache
+ * @param func
+ * @param fd
+ */
+void rspamd_symcache_composites_foreach(struct rspamd_task *task,
+ struct rspamd_symcache *cache,
+ GHFunc func,
+ gpointer fd);
+
+/**
+ * Sets allowed settings ids for a symbol
+ * @param cache
+ * @param symbol
+ * @param ids
+ * @param nids
+ */
+bool rspamd_symcache_set_allowed_settings_ids(struct rspamd_symcache *cache,
+ const gchar *symbol,
+ const guint32 *ids,
+ guint nids);
+/**
+ * Sets denied settings ids for a symbol
+ * @param cache
+ * @param symbol
+ * @param ids
+ * @param nids
+ */
+bool rspamd_symcache_set_forbidden_settings_ids(struct rspamd_symcache *cache,
+ const gchar *symbol,
+ const guint32 *ids,
+ guint nids);
+
+/**
+ * Returns allowed ids for a symbol as a constant array
+ * @param cache
+ * @param symbol
+ * @param nids
+ * @return
+ */
+const guint32 *rspamd_symcache_get_allowed_settings_ids(struct rspamd_symcache *cache,
+ const gchar *symbol,
+ guint *nids);
+
+/**
+ * Returns denied ids for a symbol as a constant array
+ * @param cache
+ * @param symbol
+ * @param nids
+ * @return
+ */
+const guint32 *rspamd_symcache_get_forbidden_settings_ids(struct rspamd_symcache *cache,
+ const gchar *symbol,
+ guint *nids);
+
+
+/**
+ * Processes settings_elt in cache and converts it to a set of
+ * adjustments for forbidden/allowed settings_ids for each symbol
+ * @param cache
+ * @param elt
+ */
+void rspamd_symcache_process_settings_elt(struct rspamd_symcache *cache,
+ struct rspamd_config_settings_elt *elt);
+
+/**
+ * Check if a symbol is allowed for execution/insertion, this does not involve
+ * condition scripts to be checked (so it is intended to be fast).
+ * @param task
+ * @param item
+ * @param exec_only
+ * @return
+ */
+gboolean rspamd_symcache_is_item_allowed(struct rspamd_task *task,
+ struct rspamd_symcache_item *item,
+ gboolean exec_only);
+
+/**
+ * Returns symcache item flags
+ * @param item
+ * @return
+ */
+gint rspamd_symcache_dyn_item_flags(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *dyn_item);
+gint rspamd_symcache_item_flags(struct rspamd_symcache_item *item);
+
+/**
+ * Returns cache item name
+ * @param item
+ * @return
+ */
+const gchar *rspamd_symcache_dyn_item_name(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *dyn_item);
+const gchar *rspamd_symcache_item_name(struct rspamd_symcache_item *item);
+
+/**
+ * Returns the current item stat
+ * @param item
+ * @return
+ */
+const struct rspamd_symcache_item_stat *
+rspamd_symcache_item_stat(struct rspamd_symcache_item *item);
+
+/**
+ * Enable profiling for task (e.g. when a slow rule has been found)
+ * @param task
+ */
+void rspamd_symcache_enable_profile(struct rspamd_task *task);
+
+struct rspamd_symcache_timeout_item {
+ double timeout;
+ const struct rspamd_symcache_item *item;
+};
+
+struct rspamd_symcache_timeout_result {
+ double max_timeout;
+ struct rspamd_symcache_timeout_item *items;
+ size_t nitems;
+};
+/**
+ * Gets maximum timeout announced by symbols cache
+ * @param cache
+ * @return new symcache timeout_result structure, that should be freed by call
+ * `rspamd_symcache_timeout_result_free`
+ */
+struct rspamd_symcache_timeout_result *rspamd_symcache_get_max_timeout(struct rspamd_symcache *cache);
+
+/**
+ * Frees results obtained from the previous function
+ * @param res
+ */
+void rspamd_symcache_timeout_result_free(struct rspamd_symcache_timeout_result *res);
+
+/**
+ * Destroy internal state of the symcache runtime
+ * @param task
+ */
+void rspamd_symcache_runtime_destroy(struct rspamd_task *task);
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/spf.c b/src/libserver/spf.c
new file mode 100644
index 0000000..72d8b99
--- /dev/null
+++ b/src/libserver/spf.c
@@ -0,0 +1,2799 @@
+/*-
+ * 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 "dns.h"
+#include "spf.h"
+#include "rspamd.h"
+#include "message.h"
+#include "utlist.h"
+#include "libserver/mempool_vars_internal.h"
+#include "contrib/librdns/rdns.h"
+#include "contrib/mumhash/mum.h"
+
+#define SPF_VER1_STR "v=spf1"
+#define SPF_VER2_STR "spf2."
+#define SPF_SCOPE_PRA "pra"
+#define SPF_SCOPE_MFROM "mfrom"
+#define SPF_ALL "all"
+#define SPF_A "a"
+#define SPF_IP4 "ip4"
+#define SPF_IP4_ALT "ipv4"
+#define SPF_IP6 "ip6"
+#define SPF_IP6_ALT "ipv6"
+#define SPF_PTR "ptr"
+#define SPF_MX "mx"
+#define SPF_EXISTS "exists"
+#define SPF_INCLUDE "include"
+#define SPF_REDIRECT "redirect"
+#define SPF_EXP "exp"
+
+struct spf_resolved_element {
+ GPtrArray *elts;
+ gchar *cur_domain;
+ gboolean redirected; /* Ignore level, it's redirected */
+};
+
+struct spf_record {
+ gint nested;
+ gint dns_requests;
+ gint requests_inflight;
+
+ guint ttl;
+ GPtrArray *resolved;
+ /* Array of struct spf_resolved_element */
+ const gchar *sender;
+ const gchar *sender_domain;
+ const gchar *top_record;
+ gchar *local_part;
+ struct rspamd_task *task;
+ spf_cb_t callback;
+ gpointer cbdata;
+ gboolean done;
+};
+
+struct rspamd_spf_library_ctx {
+ guint max_dns_nesting;
+ guint max_dns_requests;
+ guint min_cache_ttl;
+ gboolean disable_ipv6;
+ rspamd_lru_hash_t *spf_hash;
+};
+
+struct rspamd_spf_library_ctx *spf_lib_ctx = NULL;
+
+/**
+ * BNF for SPF record:
+ *
+ * spf_mech ::= +|-|~|?
+ *
+ * spf_body ::= spf=v1 <spf_command> [<spf_command>]
+ * spf_command ::= [spf_mech]all|a|<ip4>|<ip6>|ptr|mx|<exists>|<include>|<redirect>
+ *
+ * spf_domain ::= [:domain][/mask]
+ * spf_ip4 ::= ip[/mask]
+ * ip4 ::= ip4:<spf_ip4>
+ * mx ::= mx<spf_domain>
+ * a ::= a<spf_domain>
+ * ptr ::= ptr[:domain]
+ * exists ::= exists:domain
+ * include ::= include:domain
+ * redirect ::= redirect:domain
+ * exp ::= exp:domain
+ *
+ */
+
+#undef SPF_DEBUG
+
+#define msg_err_spf(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "spf", rec->task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_spf(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ "spf", rec->task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_spf(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ "spf", rec->task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_notice_spf(...) rspamd_default_log_function(G_LOG_LEVEL_MESSAGE, \
+ "spf", rec->task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_spf(...) rspamd_conditional_debug_fast(NULL, rec->task->from_addr, \
+ rspamd_spf_log_id, "spf", rec->task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_spf_flatten(...) rspamd_conditional_debug_fast_num_id(NULL, NULL, \
+ rspamd_spf_log_id, "spf", (flat)->digest, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+INIT_LOG_MODULE(spf)
+
+struct spf_dns_cb {
+ struct spf_record *rec;
+ struct spf_addr *addr;
+ struct spf_resolved_element *resolved;
+ const gchar *ptr_host;
+ spf_action_t cur_action;
+ gboolean in_include;
+};
+
+#define CHECK_REC(rec) \
+ do { \
+ if (spf_lib_ctx->max_dns_nesting > 0 && \
+ (rec)->nested > spf_lib_ctx->max_dns_nesting) { \
+ msg_warn_spf("spf nesting limit: %d > %d is reached, domain: %s", \
+ (rec)->nested, spf_lib_ctx->max_dns_nesting, \
+ (rec)->sender_domain); \
+ return FALSE; \
+ } \
+ if (spf_lib_ctx->max_dns_requests > 0 && \
+ (rec)->dns_requests > spf_lib_ctx->max_dns_requests) { \
+ msg_warn_spf("spf dns requests limit: %d > %d is reached, domain: %s", \
+ (rec)->dns_requests, spf_lib_ctx->max_dns_requests, \
+ (rec)->sender_domain); \
+ return FALSE; \
+ } \
+ } while (0)
+
+RSPAMD_CONSTRUCTOR(rspamd_spf_lib_ctx_ctor)
+{
+ spf_lib_ctx = g_malloc0(sizeof(*spf_lib_ctx));
+ spf_lib_ctx->max_dns_nesting = SPF_MAX_NESTING;
+ spf_lib_ctx->max_dns_requests = SPF_MAX_DNS_REQUESTS;
+ spf_lib_ctx->min_cache_ttl = SPF_MIN_CACHE_TTL;
+ spf_lib_ctx->disable_ipv6 = FALSE;
+}
+
+RSPAMD_DESTRUCTOR(rspamd_spf_lib_ctx_dtor)
+{
+ if (spf_lib_ctx->spf_hash) {
+ rspamd_lru_hash_destroy(spf_lib_ctx->spf_hash);
+ }
+ g_free(spf_lib_ctx);
+ spf_lib_ctx = NULL;
+}
+
+static void
+spf_record_cached_unref_dtor(gpointer p)
+{
+ struct spf_resolved *flat = (struct spf_resolved *) p;
+
+ _spf_record_unref(flat, "LRU cache");
+}
+
+void spf_library_config(const ucl_object_t *obj)
+{
+ const ucl_object_t *value;
+ gint64 ival;
+ bool bval;
+
+ if (obj == NULL) {
+ /* No specific config */
+ return;
+ }
+
+ if ((value = ucl_object_find_key(obj, "min_cache_ttl")) != NULL) {
+ if (ucl_object_toint_safe(value, &ival) && ival >= 0) {
+ spf_lib_ctx->min_cache_ttl = ival;
+ }
+ }
+
+ if ((value = ucl_object_find_key(obj, "max_dns_nesting")) != NULL) {
+ if (ucl_object_toint_safe(value, &ival) && ival >= 0) {
+ spf_lib_ctx->max_dns_nesting = ival;
+ }
+ }
+
+ if ((value = ucl_object_find_key(obj, "max_dns_requests")) != NULL) {
+ if (ucl_object_toint_safe(value, &ival) && ival >= 0) {
+ spf_lib_ctx->max_dns_requests = ival;
+ }
+ }
+ if ((value = ucl_object_find_key(obj, "disable_ipv6")) != NULL) {
+ if (ucl_object_toboolean_safe(value, &bval)) {
+ spf_lib_ctx->disable_ipv6 = bval;
+ }
+ }
+
+ if (spf_lib_ctx->spf_hash) {
+ rspamd_lru_hash_destroy(spf_lib_ctx->spf_hash);
+ spf_lib_ctx->spf_hash = NULL;
+ }
+
+ if ((value = ucl_object_find_key(obj, "spf_cache_size")) != NULL) {
+ if (ucl_object_toint_safe(value, &ival) && ival > 0) {
+ spf_lib_ctx->spf_hash = rspamd_lru_hash_new(
+ ival,
+ g_free,
+ spf_record_cached_unref_dtor);
+ }
+ }
+ else {
+ /* Preserve compatibility */
+ spf_lib_ctx->spf_hash = rspamd_lru_hash_new(
+ 2048,
+ g_free,
+ spf_record_cached_unref_dtor);
+ }
+}
+
+static gboolean start_spf_parse(struct spf_record *rec,
+ struct spf_resolved_element *resolved, gchar *begin);
+
+/* Determine spf mech */
+static spf_mech_t
+check_spf_mech(const gchar *elt, gboolean *need_shift)
+{
+ g_assert(elt != NULL);
+
+ *need_shift = TRUE;
+
+ switch (*elt) {
+ case '-':
+ return SPF_FAIL;
+ case '~':
+ return SPF_SOFT_FAIL;
+ case '+':
+ return SPF_PASS;
+ case '?':
+ return SPF_NEUTRAL;
+ default:
+ *need_shift = FALSE;
+ return SPF_PASS;
+ }
+}
+
+static const gchar *
+rspamd_spf_dns_action_to_str(spf_action_t act)
+{
+ const char *ret = "unknown";
+
+ switch (act) {
+ case SPF_RESOLVE_MX:
+ ret = "MX";
+ break;
+ case SPF_RESOLVE_A:
+ ret = "A";
+ break;
+ case SPF_RESOLVE_PTR:
+ ret = "PTR";
+ break;
+ case SPF_RESOLVE_AAA:
+ ret = "AAAA";
+ break;
+ case SPF_RESOLVE_REDIRECT:
+ ret = "REDIRECT";
+ break;
+ case SPF_RESOLVE_INCLUDE:
+ ret = "INCLUDE";
+ break;
+ case SPF_RESOLVE_EXISTS:
+ ret = "EXISTS";
+ break;
+ case SPF_RESOLVE_EXP:
+ ret = "EXP";
+ break;
+ }
+
+ return ret;
+}
+
+static struct spf_addr *
+rspamd_spf_new_addr(struct spf_record *rec,
+ struct spf_resolved_element *resolved, const gchar *elt)
+{
+ gboolean need_shift = FALSE;
+ struct spf_addr *naddr;
+
+ naddr = g_malloc0(sizeof(*naddr));
+ naddr->mech = check_spf_mech(elt, &need_shift);
+
+ if (need_shift) {
+ naddr->spf_string = g_strdup(elt + 1);
+ }
+ else {
+ naddr->spf_string = g_strdup(elt);
+ }
+
+ g_ptr_array_add(resolved->elts, naddr);
+ naddr->prev = naddr;
+ naddr->next = NULL;
+
+ return naddr;
+}
+
+static void
+rspamd_spf_free_addr(gpointer a)
+{
+ struct spf_addr *addr = a, *tmp, *cur;
+
+ if (addr) {
+ g_free(addr->spf_string);
+ DL_FOREACH_SAFE(addr, cur, tmp)
+ {
+ g_free(cur);
+ }
+ }
+}
+
+static struct spf_resolved_element *
+rspamd_spf_new_addr_list(struct spf_record *rec, const gchar *domain)
+{
+ struct spf_resolved_element *resolved;
+
+ resolved = g_malloc0(sizeof(*resolved));
+ resolved->redirected = FALSE;
+ resolved->cur_domain = g_strdup(domain);
+ resolved->elts = g_ptr_array_new_full(8, rspamd_spf_free_addr);
+
+ g_ptr_array_add(rec->resolved, resolved);
+
+ return g_ptr_array_index(rec->resolved, rec->resolved->len - 1);
+}
+
+/*
+ * Destructor for spf record
+ */
+static void
+spf_record_destructor(gpointer r)
+{
+ struct spf_record *rec = r;
+ struct spf_resolved_element *elt;
+ guint i;
+
+ if (rec) {
+ for (i = 0; i < rec->resolved->len; i++) {
+ elt = g_ptr_array_index(rec->resolved, i);
+ g_ptr_array_free(elt->elts, TRUE);
+ g_free(elt->cur_domain);
+ g_free(elt);
+ }
+
+ g_ptr_array_free(rec->resolved, TRUE);
+ }
+}
+
+static void
+rspamd_flatten_record_dtor(struct spf_resolved *r)
+{
+ struct spf_addr *addr;
+ guint i;
+
+ for (i = 0; i < r->elts->len; i++) {
+ addr = &g_array_index(r->elts, struct spf_addr, i);
+ g_free(addr->spf_string);
+ }
+
+ g_free(r->top_record);
+ g_free(r->domain);
+ g_array_free(r->elts, TRUE);
+ g_free(r);
+}
+
+static void
+rspamd_spf_process_reference(struct spf_resolved *target,
+ struct spf_addr *addr, struct spf_record *rec, gboolean top)
+{
+ struct spf_resolved_element *elt, *relt;
+ struct spf_addr *cur = NULL, taddr, *cur_addr;
+ guint i;
+
+ if (addr) {
+ g_assert(addr->m.idx < rec->resolved->len);
+
+ elt = g_ptr_array_index(rec->resolved, addr->m.idx);
+ }
+ else {
+ elt = g_ptr_array_index(rec->resolved, 0);
+ }
+
+ if (rec->ttl < target->ttl) {
+ msg_debug_spf("reducing ttl from %d to %d after subrecord processing %s",
+ target->ttl, rec->ttl, rec->sender_domain);
+ target->ttl = rec->ttl;
+ }
+
+ if (elt->redirected) {
+ g_assert(elt->elts->len > 0);
+
+ for (i = 0; i < elt->elts->len; i++) {
+ cur = g_ptr_array_index(elt->elts, i);
+ if (cur->flags & RSPAMD_SPF_FLAG_REDIRECT) {
+ break;
+ }
+ }
+
+ g_assert(cur != NULL);
+ if (!(cur->flags & (RSPAMD_SPF_FLAG_PARSED | RSPAMD_SPF_FLAG_RESOLVED))) {
+ /* Unresolved redirect */
+ msg_info_spf("redirect to %s cannot be resolved for domain %s", cur->spf_string, rec->sender_domain);
+ }
+ else {
+ g_assert(cur->flags & RSPAMD_SPF_FLAG_REFERENCE);
+ g_assert(cur->m.idx < rec->resolved->len);
+ relt = g_ptr_array_index(rec->resolved, cur->m.idx);
+ msg_debug_spf("domain %s is redirected to %s", elt->cur_domain,
+ relt->cur_domain);
+ }
+ }
+
+ for (i = 0; i < elt->elts->len; i++) {
+ cur = g_ptr_array_index(elt->elts, i);
+
+ if (cur->flags & RSPAMD_SPF_FLAG_TEMPFAIL) {
+ target->flags |= RSPAMD_SPF_RESOLVED_TEMP_FAILED;
+ continue;
+ }
+ if (cur->flags & RSPAMD_SPF_FLAG_PERMFAIL) {
+ if (cur->flags & RSPAMD_SPF_FLAG_REDIRECT) {
+ target->flags |= RSPAMD_SPF_RESOLVED_PERM_FAILED;
+ }
+ continue;
+ }
+ if (cur->flags & RSPAMD_SPF_FLAG_NA) {
+ target->flags |= RSPAMD_SPF_RESOLVED_NA;
+ continue;
+ }
+ if (cur->flags & RSPAMD_SPF_FLAG_INVALID) {
+ /* Ignore invalid elements */
+ continue;
+ }
+ if ((cur->flags & (RSPAMD_SPF_FLAG_PARSED | RSPAMD_SPF_FLAG_RESOLVED)) !=
+ (RSPAMD_SPF_FLAG_RESOLVED | RSPAMD_SPF_FLAG_PARSED)) {
+ /* Ignore unparsed addrs */
+ continue;
+ }
+ if (cur->flags & RSPAMD_SPF_FLAG_REFERENCE) {
+ /* Process reference */
+ if (cur->flags & RSPAMD_SPF_FLAG_REDIRECT) {
+ /* Stop on redirected domain */
+ rspamd_spf_process_reference(target, cur, rec, top);
+ break;
+ }
+ else {
+ rspamd_spf_process_reference(target, cur, rec, FALSE);
+ }
+ }
+ else {
+ if ((cur->flags & RSPAMD_SPF_FLAG_ANY) && !top) {
+ /* Ignore wide policies in includes */
+ continue;
+ }
+
+ DL_FOREACH(cur, cur_addr)
+ {
+ memcpy(&taddr, cur_addr, sizeof(taddr));
+ taddr.spf_string = g_strdup(cur_addr->spf_string);
+ g_array_append_val(target->elts, taddr);
+ }
+ }
+ }
+}
+
+/*
+ * Parse record and flatten it to a simple structure
+ */
+static struct spf_resolved *
+rspamd_spf_record_flatten(struct spf_record *rec)
+{
+ struct spf_resolved *res;
+
+ g_assert(rec != NULL);
+
+ res = g_malloc0(sizeof(*res));
+ res->domain = g_strdup(rec->sender_domain);
+ res->ttl = rec->ttl;
+ /* Not precise but okay */
+ res->timestamp = rec->task->task_timestamp;
+ res->digest = mum_hash_init(0xa4aa40bbeec59e2bULL);
+ res->top_record = g_strdup(rec->top_record);
+ REF_INIT_RETAIN(res, rspamd_flatten_record_dtor);
+
+ if (rec->resolved) {
+ res->elts = g_array_sized_new(FALSE, FALSE, sizeof(struct spf_addr),
+ rec->resolved->len);
+
+ if (rec->resolved->len > 0) {
+ rspamd_spf_process_reference(res, NULL, rec, TRUE);
+ }
+ }
+ else {
+ res->elts = g_array_new(FALSE, FALSE, sizeof(struct spf_addr));
+ }
+
+ return res;
+}
+
+static gint
+rspamd_spf_elts_cmp(gconstpointer a, gconstpointer b)
+{
+ struct spf_addr *addr_a, *addr_b;
+
+ addr_a = (struct spf_addr *) a;
+ addr_b = (struct spf_addr *) b;
+
+ if (addr_a->flags == addr_b->flags) {
+ if (addr_a->flags & RSPAMD_SPF_FLAG_ANY) {
+ return 0;
+ }
+ else if (addr_a->flags & RSPAMD_SPF_FLAG_IPV4) {
+ return (addr_a->m.dual.mask_v4 - addr_b->m.dual.mask_v4) ||
+ memcmp(addr_a->addr4, addr_b->addr4, sizeof(addr_a->addr4));
+ }
+ else if (addr_a->flags & RSPAMD_SPF_FLAG_IPV6) {
+ return (addr_a->m.dual.mask_v6 - addr_b->m.dual.mask_v6) ||
+ memcmp(addr_a->addr6, addr_b->addr6, sizeof(addr_a->addr6));
+ }
+ else {
+ return 0;
+ }
+ }
+ else {
+ if (addr_a->flags & RSPAMD_SPF_FLAG_ANY) {
+ return 1;
+ }
+ else if (addr_b->flags & RSPAMD_SPF_FLAG_ANY) {
+ return -1;
+ }
+ else if (addr_a->flags & RSPAMD_SPF_FLAG_IPV4) {
+ return -1;
+ }
+
+ return 1;
+ }
+}
+
+static void
+rspamd_spf_record_postprocess(struct spf_resolved *rec, struct rspamd_task *task)
+{
+ g_array_sort(rec->elts, rspamd_spf_elts_cmp);
+
+ for (guint i = 0; i < rec->elts->len; i++) {
+ struct spf_addr *cur_addr = &g_array_index(rec->elts, struct spf_addr, i);
+
+ if (cur_addr->flags & RSPAMD_SPF_FLAG_IPV6) {
+ guint64 t[3];
+
+ /*
+ * Fill hash entry for ipv6 addr with 2 int64 from ipv6 address,
+ * the remaining int64 has mech + mask
+ */
+ memcpy(t, cur_addr->addr6, sizeof(guint64) * 2);
+ t[2] = ((guint64) (cur_addr->mech)) << 48u;
+ t[2] |= cur_addr->m.dual.mask_v6;
+
+ for (guint j = 0; j < G_N_ELEMENTS(t); j++) {
+ rec->digest = mum_hash_step(rec->digest, t[j]);
+ }
+ }
+ else if (cur_addr->flags & RSPAMD_SPF_FLAG_IPV4) {
+ guint64 t = 0;
+
+ memcpy(&t, cur_addr->addr4, sizeof(guint32));
+ t |= ((guint64) (cur_addr->mech)) << 48u;
+ t |= ((guint64) cur_addr->m.dual.mask_v4) << 32u;
+
+ rec->digest = mum_hash_step(rec->digest, t);
+ }
+ }
+
+ if (spf_lib_ctx->min_cache_ttl > 0) {
+ if (rec->ttl != 0 && rec->ttl < spf_lib_ctx->min_cache_ttl) {
+ msg_info_task("increasing ttl from %d to %d as it lower than a limit",
+ rec->ttl, spf_lib_ctx->min_cache_ttl);
+ rec->ttl = spf_lib_ctx->min_cache_ttl;
+ }
+ }
+}
+
+static void
+rspamd_spf_maybe_return(struct spf_record *rec)
+{
+ struct spf_resolved *flat;
+ struct rspamd_task *task = rec->task;
+ bool cached = false;
+
+ if (rec->requests_inflight == 0 && !rec->done) {
+ flat = rspamd_spf_record_flatten(rec);
+ rspamd_spf_record_postprocess(flat, rec->task);
+
+ if (flat->ttl > 0 && flat->flags == 0) {
+
+ if (spf_lib_ctx->spf_hash) {
+ rspamd_lru_hash_insert(spf_lib_ctx->spf_hash,
+ g_strdup(flat->domain),
+ spf_record_ref(flat),
+ flat->timestamp, flat->ttl);
+
+ msg_info_task("stored SPF record for %s (0x%xuL) in LRU cache for %d seconds, "
+ "%d/%d elements in the cache",
+ flat->domain,
+ flat->digest,
+ flat->ttl,
+ rspamd_lru_hash_size(spf_lib_ctx->spf_hash),
+ rspamd_lru_hash_capacity(spf_lib_ctx->spf_hash));
+ cached = true;
+ }
+ }
+
+ if (!cached) {
+ /* Still write a log line */
+ msg_info_task("not stored SPF record for %s (0x%xuL) in LRU cache; flags=%d; ttl=%d",
+ flat->domain,
+ flat->digest,
+ flat->flags,
+ flat->ttl);
+ }
+
+ rec->callback(flat, rec->task, rec->cbdata);
+ spf_record_unref(flat);
+ rec->done = TRUE;
+ }
+}
+
+static gboolean
+spf_check_ptr_host(struct spf_dns_cb *cb, const char *name)
+{
+ const char *dend, *nend, *dstart, *nstart;
+ struct spf_record *rec = cb->rec;
+
+ if (cb->ptr_host != NULL) {
+ dstart = cb->ptr_host;
+ }
+ else {
+ dstart = cb->resolved->cur_domain;
+ }
+
+ if (name == NULL || dstart == NULL) {
+ return FALSE;
+ }
+
+ msg_debug_spf("check ptr %s vs %s", name, dstart);
+
+ /* We need to check whether `cur_domain` is a subdomain for `name` */
+ dend = dstart + strlen(dstart) - 1;
+ nstart = name;
+ nend = nstart + strlen(nstart) - 1;
+
+ if (nend <= nstart || dend <= dstart) {
+ return FALSE;
+ }
+ /* Strip last '.' from names */
+ if (*nend == '.') {
+ nend--;
+ }
+ if (*dend == '.') {
+ dend--;
+ }
+ if (nend <= nstart || dend <= dstart) {
+ return FALSE;
+ }
+
+ /* Now compare from end to start */
+ for (;;) {
+ if (g_ascii_tolower(*dend) != g_ascii_tolower(*nend)) {
+ msg_debug_spf("ptr records mismatch: %s and %s", dend, nend);
+ return FALSE;
+ }
+
+ if (dend == dstart) {
+ break;
+ }
+ if (nend == nstart) {
+ /* Name is shorter than cur_domain */
+ return FALSE;
+ }
+ nend--;
+ dend--;
+ }
+
+ if (nend > nstart && *(nend - 1) != '.') {
+ /* Not a subdomain */
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+spf_record_process_addr(struct spf_record *rec, struct spf_addr *addr, struct rdns_reply_entry *reply)
+{
+ struct spf_addr *naddr;
+
+ if (!(addr->flags & RSPAMD_SPF_FLAG_PROCESSED)) {
+ /* That's the first address */
+ if (reply->type == RDNS_REQUEST_AAAA) {
+ memcpy(addr->addr6,
+ &reply->content.aaa.addr,
+ sizeof(addr->addr6));
+ addr->flags |= RSPAMD_SPF_FLAG_IPV6;
+ }
+ else if (reply->type == RDNS_REQUEST_A) {
+ memcpy(addr->addr4, &reply->content.a.addr, sizeof(addr->addr4));
+ addr->flags |= RSPAMD_SPF_FLAG_IPV4;
+ }
+ else {
+ msg_err_spf(
+ "internal error, bad DNS reply is treated as address: %s; domain: %s",
+ rdns_strtype(reply->type),
+ rec->sender_domain);
+ }
+
+ addr->flags |= RSPAMD_SPF_FLAG_PROCESSED;
+ }
+ else {
+ /* We need to create a new address */
+ naddr = g_malloc0(sizeof(*naddr));
+ memcpy(naddr, addr, sizeof(*naddr));
+ naddr->next = NULL;
+ naddr->prev = NULL;
+
+ if (reply->type == RDNS_REQUEST_AAAA) {
+ memcpy(naddr->addr6,
+ &reply->content.aaa.addr,
+ sizeof(addr->addr6));
+ naddr->flags |= RSPAMD_SPF_FLAG_IPV6;
+ }
+ else if (reply->type == RDNS_REQUEST_A) {
+ memcpy(naddr->addr4, &reply->content.a.addr, sizeof(addr->addr4));
+ naddr->flags |= RSPAMD_SPF_FLAG_IPV4;
+ }
+ else {
+ msg_err_spf(
+ "internal error, bad DNS reply is treated as address: %s; domain: %s",
+ rdns_strtype(reply->type),
+ rec->sender_domain);
+ }
+
+ DL_APPEND(addr, naddr);
+ }
+}
+
+static void
+spf_record_addr_set(struct spf_addr *addr, gboolean allow_any)
+{
+ guchar fill;
+
+ if (!(addr->flags & RSPAMD_SPF_FLAG_PROCESSED)) {
+ if (allow_any) {
+ fill = 0;
+ addr->m.dual.mask_v4 = 0;
+ addr->m.dual.mask_v6 = 0;
+ }
+ else {
+ fill = 0xff;
+ }
+
+ memset(addr->addr4, fill, sizeof(addr->addr4));
+ memset(addr->addr6, fill, sizeof(addr->addr6));
+
+
+ addr->flags |= RSPAMD_SPF_FLAG_IPV4;
+ addr->flags |= RSPAMD_SPF_FLAG_IPV6;
+ }
+}
+
+static gboolean
+spf_process_txt_record(struct spf_record *rec, struct spf_resolved_element *resolved,
+ struct rdns_reply *reply, struct rdns_reply_entry **pselected)
+{
+ struct rdns_reply_entry *elt, *selected = NULL;
+ gboolean ret = FALSE;
+
+ /*
+ * We prefer spf version 1 as other records are mostly likely garbage
+ * or incorrect records (e.g. spf2 records)
+ */
+ LL_FOREACH(reply->entries, elt)
+ {
+ if (elt->type == RDNS_REQUEST_TXT) {
+ if (strncmp(elt->content.txt.data, "v=spf1", sizeof("v=spf1") - 1) == 0) {
+ selected = elt;
+
+ if (pselected != NULL) {
+ *pselected = selected;
+ }
+
+ break;
+ }
+ }
+ }
+
+ if (!selected) {
+ LL_FOREACH(reply->entries, elt)
+ {
+ /*
+ * Rubbish spf record? Let's still try to process it, but merely for
+ * TXT RRs
+ */
+ if (elt->type == RDNS_REQUEST_TXT) {
+ if (start_spf_parse(rec, resolved, elt->content.txt.data)) {
+ ret = TRUE;
+ if (pselected != NULL) {
+ *pselected = elt;
+ }
+ break;
+ }
+ }
+ }
+ }
+ else {
+ ret = start_spf_parse(rec, resolved, selected->content.txt.data);
+ }
+
+ return ret;
+}
+
+static void
+spf_record_dns_callback(struct rdns_reply *reply, gpointer arg)
+{
+ struct spf_dns_cb *cb = arg;
+ struct rdns_reply_entry *elt_data;
+ struct rspamd_task *task;
+ struct spf_addr *addr;
+ struct spf_record *rec;
+ const struct rdns_request_name *req_name;
+ bool truncated = false;
+
+ rec = cb->rec;
+ task = rec->task;
+
+ cb->rec->requests_inflight--;
+ addr = cb->addr;
+ req_name = rdns_request_get_name(reply->request, NULL);
+
+ if (reply->flags & RDNS_TRUNCATED) {
+ /* Do not process truncated DNS replies */
+ truncated = true;
+
+ if (req_name) {
+ msg_notice_spf("got a truncated record when trying to resolve %s (%s type) for SPF domain %s",
+ req_name->name, rdns_str_from_type(req_name->type),
+ rec->sender_domain);
+ }
+ else {
+ msg_notice_spf("got a truncated record when trying to resolve ??? "
+ "(internal error) for SPF domain %s",
+ rec->sender_domain);
+ }
+ }
+
+ if (reply->code == RDNS_RC_NOERROR && !truncated) {
+
+ LL_FOREACH(reply->entries, elt_data)
+ {
+ /* Adjust ttl if a resolved record has lower ttl than spf record itself */
+ if ((guint) elt_data->ttl < rec->ttl) {
+ msg_debug_spf("reducing ttl from %d to %d after DNS resolving",
+ rec->ttl, elt_data->ttl);
+ rec->ttl = elt_data->ttl;
+ }
+
+ if (elt_data->type == RDNS_REQUEST_CNAME) {
+ /* Skip cname aliases - it must be handled by a recursor */
+ continue;
+ }
+
+ switch (cb->cur_action) {
+ case SPF_RESOLVE_MX:
+ if (elt_data->type == RDNS_REQUEST_MX) {
+ /* Now resolve A record for this MX */
+ msg_debug_spf("resolve %s after resolving of MX",
+ elt_data->content.mx.name);
+ if (rspamd_dns_resolver_request_task_forced(task,
+ spf_record_dns_callback, (void *) cb,
+ RDNS_REQUEST_A,
+ elt_data->content.mx.name)) {
+ cb->rec->requests_inflight++;
+ }
+
+ if (!spf_lib_ctx->disable_ipv6) {
+ if (rspamd_dns_resolver_request_task_forced(task,
+ spf_record_dns_callback, (void *) cb,
+ RDNS_REQUEST_AAAA,
+ elt_data->content.mx.name)) {
+ cb->rec->requests_inflight++;
+ }
+ }
+ else {
+ msg_debug_spf("skip AAAA request for MX resolution");
+ }
+ }
+ else {
+ cb->addr->flags |= RSPAMD_SPF_FLAG_RESOLVED;
+ cb->addr->flags &= ~RSPAMD_SPF_FLAG_PERMFAIL;
+ msg_debug_spf("resolved MX addr");
+ spf_record_process_addr(rec, addr, elt_data);
+ }
+ break;
+ case SPF_RESOLVE_A:
+ case SPF_RESOLVE_AAA:
+ cb->addr->flags |= RSPAMD_SPF_FLAG_RESOLVED;
+ cb->addr->flags &= ~RSPAMD_SPF_FLAG_PERMFAIL;
+ spf_record_process_addr(rec, addr, elt_data);
+ break;
+ case SPF_RESOLVE_PTR:
+ if (elt_data->type == RDNS_REQUEST_PTR) {
+ /* Validate returned records prior to making A requests */
+ if (spf_check_ptr_host(cb,
+ elt_data->content.ptr.name)) {
+ msg_debug_spf("resolve PTR %s after resolving of PTR",
+ elt_data->content.ptr.name);
+ if (rspamd_dns_resolver_request_task_forced(task,
+ spf_record_dns_callback, (void *) cb,
+ RDNS_REQUEST_A,
+ elt_data->content.ptr.name)) {
+ cb->rec->requests_inflight++;
+ }
+
+ if (!spf_lib_ctx->disable_ipv6) {
+ if (rspamd_dns_resolver_request_task_forced(task,
+ spf_record_dns_callback, (void *) cb,
+ RDNS_REQUEST_AAAA,
+ elt_data->content.ptr.name)) {
+ cb->rec->requests_inflight++;
+ }
+ }
+ else {
+ msg_debug_spf("skip AAAA request for PTR resolution");
+ }
+ }
+ else {
+ cb->addr->flags |= RSPAMD_SPF_FLAG_RESOLVED;
+ cb->addr->flags &= ~RSPAMD_SPF_FLAG_PERMFAIL;
+ }
+ }
+ else {
+ cb->addr->flags |= RSPAMD_SPF_FLAG_RESOLVED;
+ cb->addr->flags &= ~RSPAMD_SPF_FLAG_PERMFAIL;
+ spf_record_process_addr(rec, addr, elt_data);
+ }
+ break;
+ case SPF_RESOLVE_REDIRECT:
+ if (elt_data->type == RDNS_REQUEST_TXT) {
+ cb->addr->flags |= RSPAMD_SPF_FLAG_RESOLVED;
+ if (reply->entries) {
+ msg_debug_spf("got redirection record for %s: '%s'",
+ req_name->name,
+ reply->entries[0].content.txt.data);
+ }
+
+ if (!spf_process_txt_record(rec, cb->resolved, reply, NULL)) {
+ cb->addr->flags |= RSPAMD_SPF_FLAG_PERMFAIL;
+ }
+ }
+
+ goto end;
+ break;
+ case SPF_RESOLVE_INCLUDE:
+ if (elt_data->type == RDNS_REQUEST_TXT) {
+ struct rdns_reply_entry *selected = NULL;
+
+ cb->addr->flags |= RSPAMD_SPF_FLAG_RESOLVED;
+ spf_process_txt_record(rec, cb->resolved, reply, &selected);
+ if (selected) {
+ msg_debug_spf("got include record for %s: '%s'",
+ req_name->name,
+ selected->content.txt.data);
+ }
+ else {
+ msg_debug_spf("no include record for %s",
+ req_name->name);
+ }
+ }
+ goto end;
+
+ break;
+ case SPF_RESOLVE_EXP:
+ break;
+ case SPF_RESOLVE_EXISTS:
+ if (elt_data->type == RDNS_REQUEST_A ||
+ elt_data->type == RDNS_REQUEST_AAAA) {
+ /*
+ * If specified address resolves, we can accept
+ * connection from every IP
+ */
+ addr->flags |= RSPAMD_SPF_FLAG_RESOLVED;
+ spf_record_addr_set(addr, TRUE);
+ }
+ break;
+ }
+ }
+ }
+ else if (reply->code == RDNS_RC_NXDOMAIN || reply->code == RDNS_RC_NOREC) {
+ switch (cb->cur_action) {
+ case SPF_RESOLVE_MX:
+ if (!(cb->addr->flags & RSPAMD_SPF_FLAG_RESOLVED)) {
+ cb->addr->flags |= RSPAMD_SPF_FLAG_PERMFAIL;
+ msg_info_spf(
+ "spf error for domain %s: cannot find MX"
+ " record for %s: %s",
+ cb->rec->sender_domain,
+ cb->resolved->cur_domain,
+ rdns_strerror(reply->code));
+ spf_record_addr_set(addr, FALSE);
+ }
+ break;
+ case SPF_RESOLVE_A:
+ if (!(cb->addr->flags & RSPAMD_SPF_FLAG_RESOLVED)) {
+ cb->addr->flags |= RSPAMD_SPF_FLAG_PERMFAIL;
+ msg_info_spf(
+ "spf error for domain %s: cannot resolve A"
+ " record for %s: %s",
+ cb->rec->sender_domain,
+ cb->resolved->cur_domain,
+ rdns_strerror(reply->code));
+
+ if (rdns_request_has_type(reply->request, RDNS_REQUEST_A)) {
+ spf_record_addr_set(addr, FALSE);
+ }
+ }
+ break;
+ case SPF_RESOLVE_AAA:
+ if (!(cb->addr->flags & RSPAMD_SPF_FLAG_RESOLVED)) {
+ cb->addr->flags |= RSPAMD_SPF_FLAG_PERMFAIL;
+ msg_info_spf(
+ "spf error for domain %s: cannot resolve AAAA"
+ " record for %s: %s",
+ cb->rec->sender_domain,
+ cb->resolved->cur_domain,
+ rdns_strerror(reply->code));
+ if (rdns_request_has_type(reply->request, RDNS_REQUEST_AAAA)) {
+ spf_record_addr_set(addr, FALSE);
+ }
+ }
+ break;
+ case SPF_RESOLVE_PTR:
+ if (!(cb->addr->flags & RSPAMD_SPF_FLAG_RESOLVED)) {
+ msg_info_spf(
+ "spf error for domain %s: cannot resolve PTR"
+ " record for %s: %s",
+ cb->rec->sender_domain,
+ cb->resolved->cur_domain,
+ rdns_strerror(reply->code));
+ cb->addr->flags |= RSPAMD_SPF_FLAG_PERMFAIL;
+
+ spf_record_addr_set(addr, FALSE);
+ }
+ break;
+ case SPF_RESOLVE_REDIRECT:
+ if (!(cb->addr->flags & RSPAMD_SPF_FLAG_RESOLVED)) {
+ cb->addr->flags |= RSPAMD_SPF_FLAG_PERMFAIL;
+ msg_info_spf(
+ "spf error for domain %s: cannot resolve REDIRECT"
+ " record for %s: %s",
+ cb->rec->sender_domain,
+ cb->resolved->cur_domain,
+ rdns_strerror(reply->code));
+ }
+
+ break;
+ case SPF_RESOLVE_INCLUDE:
+ if (!(cb->addr->flags & RSPAMD_SPF_FLAG_RESOLVED)) {
+ msg_info_spf(
+ "spf error for domain %s: cannot resolve INCLUDE"
+ " record for %s: %s",
+ cb->rec->sender_domain,
+ cb->resolved->cur_domain,
+ rdns_strerror(reply->code));
+
+ cb->addr->flags |= RSPAMD_SPF_FLAG_PERMFAIL;
+ }
+ break;
+ case SPF_RESOLVE_EXP:
+ break;
+ case SPF_RESOLVE_EXISTS:
+ if (!(cb->addr->flags & RSPAMD_SPF_FLAG_RESOLVED)) {
+ msg_debug_spf(
+ "spf macro resolution for domain %s: cannot resolve EXISTS"
+ " macro for %s: %s",
+ cb->rec->sender_domain,
+ cb->resolved->cur_domain,
+ rdns_strerror(reply->code));
+ spf_record_addr_set(addr, FALSE);
+ }
+ break;
+ }
+ }
+ else {
+ cb->addr->flags |= RSPAMD_SPF_FLAG_TEMPFAIL;
+ msg_info_spf(
+ "spf error for domain %s: cannot resolve %s DNS record for"
+ " %s: %s",
+ cb->rec->sender_domain,
+ rspamd_spf_dns_action_to_str(cb->cur_action),
+ cb->ptr_host,
+ rdns_strerror(reply->code));
+ }
+
+end:
+ rspamd_spf_maybe_return(cb->rec);
+}
+
+/*
+ * The syntax defined by the following BNF:
+ * [ ":" domain-spec ] [ dual-cidr-length ]
+ * ip4-cidr-length = "/" 1*DIGIT
+ * ip6-cidr-length = "/" 1*DIGIT
+ * dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ]
+ */
+static const gchar *
+parse_spf_domain_mask(struct spf_record *rec, struct spf_addr *addr,
+ struct spf_resolved_element *resolved,
+ gboolean allow_mask)
+{
+ struct rspamd_task *task = rec->task;
+ enum {
+ parse_spf_elt = 0,
+ parse_semicolon,
+ parse_domain,
+ parse_slash,
+ parse_ipv4_mask,
+ parse_second_slash,
+ parse_ipv6_mask,
+ skip_garbage
+ } state = 0;
+ const gchar *p = addr->spf_string, *host, *c;
+ gchar *hostbuf;
+ gchar t;
+ guint16 cur_mask = 0;
+
+ host = resolved->cur_domain;
+ c = p;
+
+ while (*p) {
+ t = *p;
+
+ switch (state) {
+ case parse_spf_elt:
+ if (t == ':' || t == '=') {
+ state = parse_semicolon;
+ }
+ else if (t == '/') {
+ /* No domain but mask */
+ state = parse_slash;
+ }
+ p++;
+ break;
+ case parse_semicolon:
+ if (t == '/') {
+ /* Empty domain, technically an error */
+ state = parse_slash;
+ }
+ else {
+ c = p;
+ state = parse_domain;
+ }
+ break;
+ case parse_domain:
+ if (t == '/') {
+ hostbuf = rspamd_mempool_alloc(task->task_pool, p - c + 1);
+ rspamd_strlcpy(hostbuf, c, p - c + 1);
+ host = hostbuf;
+ state = parse_slash;
+ }
+ p++;
+ break;
+ case parse_slash:
+ c = p;
+ if (allow_mask) {
+ state = parse_ipv4_mask;
+ }
+ else {
+ state = skip_garbage;
+ }
+ cur_mask = 0;
+ break;
+ case parse_ipv4_mask:
+ if (g_ascii_isdigit(t)) {
+ /* Ignore errors here */
+ cur_mask = cur_mask * 10 + (t - '0');
+ }
+ else if (t == '/') {
+ if (cur_mask <= 32) {
+ addr->m.dual.mask_v4 = cur_mask;
+ }
+ else {
+ msg_notice_spf("bad ipv4 mask for %s: %d",
+ rec->sender_domain, cur_mask);
+ }
+ state = parse_second_slash;
+ }
+ p++;
+ break;
+ case parse_second_slash:
+ c = p;
+ state = parse_ipv6_mask;
+ cur_mask = 0;
+ break;
+ case parse_ipv6_mask:
+ if (g_ascii_isdigit(t)) {
+ /* Ignore errors here */
+ cur_mask = cur_mask * 10 + (t - '0');
+ }
+ p++;
+ break;
+ case skip_garbage:
+ p++;
+ break;
+ }
+ }
+
+ /* Process end states */
+ if (state == parse_ipv4_mask) {
+ if (cur_mask <= 32) {
+ addr->m.dual.mask_v4 = cur_mask;
+ }
+ else {
+ msg_notice_spf("bad ipv4 mask for %s: %d", rec->sender_domain, cur_mask);
+ }
+ }
+ else if (state == parse_ipv6_mask) {
+ if (cur_mask <= 128) {
+ addr->m.dual.mask_v6 = cur_mask;
+ }
+ else {
+ msg_notice_spf("bad ipv6 mask: %d", cur_mask);
+ }
+ }
+ else if (state == parse_domain && p - c > 0) {
+ hostbuf = rspamd_mempool_alloc(task->task_pool, p - c + 1);
+ rspamd_strlcpy(hostbuf, c, p - c + 1);
+ host = hostbuf;
+ }
+
+ if (cur_mask == 0) {
+ addr->m.dual.mask_v4 = 32;
+ addr->m.dual.mask_v6 = 64;
+ }
+
+ return host;
+}
+
+static gboolean
+parse_spf_a(struct spf_record *rec,
+ struct spf_resolved_element *resolved, struct spf_addr *addr)
+{
+ struct spf_dns_cb *cb;
+ const gchar *host = NULL;
+ struct rspamd_task *task = rec->task;
+
+ CHECK_REC(rec);
+
+ host = parse_spf_domain_mask(rec, addr, resolved, TRUE);
+
+ if (host == NULL) {
+ return FALSE;
+ }
+
+ rec->dns_requests++;
+ cb = rspamd_mempool_alloc(task->task_pool, sizeof(struct spf_dns_cb));
+ cb->rec = rec;
+ cb->ptr_host = host;
+ cb->addr = addr;
+ cb->cur_action = SPF_RESOLVE_A;
+ cb->resolved = resolved;
+ msg_debug_spf("resolve a %s", host);
+
+ if (rspamd_dns_resolver_request_task_forced(task,
+ spf_record_dns_callback, (void *) cb, RDNS_REQUEST_A, host)) {
+ rec->requests_inflight++;
+
+ cb = rspamd_mempool_alloc(task->task_pool, sizeof(struct spf_dns_cb));
+ cb->rec = rec;
+ cb->ptr_host = host;
+ cb->addr = addr;
+ cb->cur_action = SPF_RESOLVE_AAA;
+ cb->resolved = resolved;
+
+ if (!spf_lib_ctx->disable_ipv6) {
+ if (rspamd_dns_resolver_request_task_forced(task,
+ spf_record_dns_callback, (void *) cb, RDNS_REQUEST_AAAA, host)) {
+ rec->requests_inflight++;
+ }
+ }
+ else {
+ msg_debug_spf("skip AAAA request for a record resolution");
+ }
+
+ return TRUE;
+ }
+ else {
+ msg_notice_spf("unresolvable A element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ }
+
+ return FALSE;
+}
+
+static gboolean
+parse_spf_ptr(struct spf_record *rec,
+ struct spf_resolved_element *resolved, struct spf_addr *addr)
+{
+ struct spf_dns_cb *cb;
+ const gchar *host;
+ gchar *ptr;
+ struct rspamd_task *task = rec->task;
+
+ CHECK_REC(rec);
+
+ host = parse_spf_domain_mask(rec, addr, resolved, FALSE);
+
+ rec->dns_requests++;
+ cb = rspamd_mempool_alloc(task->task_pool, sizeof(struct spf_dns_cb));
+ cb->rec = rec;
+ cb->addr = addr;
+ cb->cur_action = SPF_RESOLVE_PTR;
+ cb->resolved = resolved;
+ cb->ptr_host = rspamd_mempool_strdup(task->task_pool, host);
+ ptr =
+ rdns_generate_ptr_from_str(rspamd_inet_address_to_string(
+ task->from_addr));
+
+ if (ptr == NULL) {
+ return FALSE;
+ }
+
+ rspamd_mempool_add_destructor(task->task_pool, free, ptr);
+ msg_debug_spf("resolve ptr %s for %s", ptr, host);
+
+ if (rspamd_dns_resolver_request_task_forced(task,
+ spf_record_dns_callback, (void *) cb, RDNS_REQUEST_PTR, ptr)) {
+ rec->requests_inflight++;
+ rec->ttl = 0;
+ msg_debug_spf("disable SPF caching as there is PTR expansion");
+
+ return TRUE;
+ }
+ else {
+ msg_notice_spf("unresolvable PTR element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ }
+
+ return FALSE;
+}
+
+static gboolean
+parse_spf_mx(struct spf_record *rec,
+ struct spf_resolved_element *resolved, struct spf_addr *addr)
+{
+ struct spf_dns_cb *cb;
+ const gchar *host;
+ struct rspamd_task *task = rec->task;
+
+ CHECK_REC(rec);
+
+ host = parse_spf_domain_mask(rec, addr, resolved, TRUE);
+
+ if (host == NULL) {
+ return FALSE;
+ }
+
+ rec->dns_requests++;
+ cb = rspamd_mempool_alloc(task->task_pool, sizeof(struct spf_dns_cb));
+ cb->rec = rec;
+ cb->addr = addr;
+ cb->cur_action = SPF_RESOLVE_MX;
+ cb->ptr_host = host;
+ cb->resolved = resolved;
+
+ msg_debug_spf("resolve mx for %s", host);
+ if (rspamd_dns_resolver_request_task_forced(task,
+ spf_record_dns_callback, (void *) cb, RDNS_REQUEST_MX, host)) {
+ rec->requests_inflight++;
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+parse_spf_all(struct spf_record *rec, struct spf_addr *addr)
+{
+ /* All is 0/0 */
+ memset(&addr->addr4, 0, sizeof(addr->addr4));
+ memset(&addr->addr6, 0, sizeof(addr->addr6));
+ /* Here we set all masks to 0 */
+ addr->m.idx = 0;
+ addr->flags |= RSPAMD_SPF_FLAG_ANY | RSPAMD_SPF_FLAG_RESOLVED;
+ msg_debug_spf("parsed all elt");
+
+ /* Disallow +all */
+ if (addr->mech == SPF_PASS) {
+ addr->flags |= RSPAMD_SPF_FLAG_INVALID;
+ msg_notice_spf("domain %s allows any SPF (+all), ignore SPF record completely",
+ rec->sender_domain);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+parse_spf_ip4(struct spf_record *rec, struct spf_addr *addr)
+{
+ /* ip4:addr[/mask] */
+ const gchar *semicolon, *slash;
+ gsize len;
+ gchar ipbuf[INET_ADDRSTRLEN + 1];
+ guint32 mask;
+ static const guint32 min_valid_mask = 8;
+
+ semicolon = strchr(addr->spf_string, ':');
+
+ if (semicolon == NULL) {
+ semicolon = strchr(addr->spf_string, '=');
+
+ if (semicolon == NULL) {
+ msg_notice_spf("invalid ip4 element for %s: %s, no '=' or ':'", addr->spf_string,
+ rec->sender_domain);
+ return FALSE;
+ }
+ }
+
+ semicolon++;
+ slash = strchr(semicolon, '/');
+
+ if (slash) {
+ len = slash - semicolon;
+ }
+ else {
+ len = strlen(semicolon);
+ }
+
+ rspamd_strlcpy(ipbuf, semicolon, MIN(len + 1, sizeof(ipbuf)));
+
+ if (inet_pton(AF_INET, ipbuf, addr->addr4) != 1) {
+ msg_notice_spf("invalid ip4 element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ return FALSE;
+ }
+
+ if (slash) {
+ gchar *end = NULL;
+
+ mask = strtoul(slash + 1, &end, 10);
+ if (mask > 32) {
+ msg_notice_spf("invalid mask for ip4 element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ return FALSE;
+ }
+
+ if (end != NULL && !g_ascii_isspace(*end) && *end != '\0') {
+ /* Invalid mask definition */
+ msg_notice_spf("invalid mask for ip4 element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ return FALSE;
+ }
+
+ addr->m.dual.mask_v4 = mask;
+
+ if (mask < min_valid_mask) {
+ addr->flags |= RSPAMD_SPF_FLAG_INVALID;
+ msg_notice_spf("too wide SPF record for %s: %s/%d",
+ rec->sender_domain,
+ ipbuf, addr->m.dual.mask_v4);
+ }
+ }
+ else {
+ addr->m.dual.mask_v4 = 32;
+ }
+
+ addr->flags |= RSPAMD_SPF_FLAG_IPV4 | RSPAMD_SPF_FLAG_RESOLVED;
+ msg_debug_spf("parsed ipv4 record %s/%d", ipbuf, addr->m.dual.mask_v4);
+
+ return TRUE;
+}
+
+static gboolean
+parse_spf_ip6(struct spf_record *rec, struct spf_addr *addr)
+{
+ /* ip6:addr[/mask] */
+ const gchar *semicolon, *slash;
+ gsize len;
+ gchar ipbuf[INET6_ADDRSTRLEN + 1];
+ guint32 mask;
+ static const guint32 min_valid_mask = 8;
+
+ semicolon = strchr(addr->spf_string, ':');
+
+ if (semicolon == NULL) {
+ semicolon = strchr(addr->spf_string, '=');
+
+ if (semicolon == NULL) {
+ msg_notice_spf("invalid ip6 element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ return FALSE;
+ }
+ }
+
+ semicolon++;
+ slash = strchr(semicolon, '/');
+
+ if (slash) {
+ len = slash - semicolon;
+ }
+ else {
+ len = strlen(semicolon);
+ }
+
+ rspamd_strlcpy(ipbuf, semicolon, MIN(len + 1, sizeof(ipbuf)));
+
+ if (inet_pton(AF_INET6, ipbuf, addr->addr6) != 1) {
+ msg_notice_spf("invalid ip6 element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ return FALSE;
+ }
+
+ if (slash) {
+ gchar *end = NULL;
+ mask = strtoul(slash + 1, &end, 10);
+ if (mask > 128) {
+ msg_notice_spf("invalid mask for ip6 element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ return FALSE;
+ }
+
+ if (end != NULL && !g_ascii_isspace(*end) && *end != '\0') {
+ /* Invalid mask definition */
+ msg_notice_spf("invalid mask for ip4 element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ return FALSE;
+ }
+
+ addr->m.dual.mask_v6 = mask;
+
+ if (mask < min_valid_mask) {
+ addr->flags |= RSPAMD_SPF_FLAG_INVALID;
+ msg_notice_spf("too wide SPF record for %s: %s/%d",
+ rec->sender_domain,
+ ipbuf, addr->m.dual.mask_v6);
+ }
+ }
+ else {
+ addr->m.dual.mask_v6 = 128;
+ }
+
+ addr->flags |= RSPAMD_SPF_FLAG_IPV6 | RSPAMD_SPF_FLAG_RESOLVED;
+ msg_debug_spf("parsed ipv6 record %s/%d", ipbuf, addr->m.dual.mask_v6);
+
+ return TRUE;
+}
+
+
+static gboolean
+parse_spf_include(struct spf_record *rec, struct spf_addr *addr)
+{
+ struct spf_dns_cb *cb;
+ const gchar *domain;
+ struct rspamd_task *task = rec->task;
+
+ CHECK_REC(rec);
+ domain = strchr(addr->spf_string, ':');
+
+ if (domain == NULL) {
+ /* Common mistake */
+ domain = strchr(addr->spf_string, '=');
+
+ if (domain == NULL) {
+ msg_notice_spf("invalid include element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ return FALSE;
+ }
+ }
+
+ domain++;
+
+ rec->dns_requests++;
+
+ cb = rspamd_mempool_alloc(task->task_pool, sizeof(struct spf_dns_cb));
+ cb->rec = rec;
+ cb->addr = addr;
+ cb->cur_action = SPF_RESOLVE_INCLUDE;
+ addr->m.idx = rec->resolved->len;
+ cb->resolved = rspamd_spf_new_addr_list(rec, domain);
+ cb->ptr_host = domain;
+ /* Set reference */
+ addr->flags |= RSPAMD_SPF_FLAG_REFERENCE;
+ msg_debug_spf("resolve include %s", domain);
+
+ if (rspamd_dns_resolver_request_task_forced(task,
+ spf_record_dns_callback, (void *) cb, RDNS_REQUEST_TXT, domain)) {
+ rec->requests_inflight++;
+
+ return TRUE;
+ }
+ else {
+ msg_notice_spf("unresolvable include element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ }
+
+
+ return FALSE;
+}
+
+static gboolean
+parse_spf_exp(struct spf_record *rec, struct spf_addr *addr)
+{
+ msg_info_spf("exp record is ignored");
+ return TRUE;
+}
+
+static gboolean
+parse_spf_redirect(struct spf_record *rec,
+ struct spf_resolved_element *resolved, struct spf_addr *addr)
+{
+ struct spf_dns_cb *cb;
+ const gchar *domain;
+ struct rspamd_task *task = rec->task;
+
+ CHECK_REC(rec);
+
+ domain = strchr(addr->spf_string, '=');
+
+ if (domain == NULL) {
+ /* Common mistake */
+ domain = strchr(addr->spf_string, ':');
+
+ if (domain == NULL) {
+ msg_notice_spf("invalid redirect element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ return FALSE;
+ }
+ }
+
+ domain++;
+
+ rec->dns_requests++;
+ resolved->redirected = TRUE;
+
+ cb = rspamd_mempool_alloc(task->task_pool, sizeof(struct spf_dns_cb));
+ /* Set reference */
+ addr->flags |= RSPAMD_SPF_FLAG_REFERENCE | RSPAMD_SPF_FLAG_REDIRECT;
+ addr->m.idx = rec->resolved->len;
+
+ cb->rec = rec;
+ cb->addr = addr;
+ cb->cur_action = SPF_RESOLVE_REDIRECT;
+ cb->resolved = rspamd_spf_new_addr_list(rec, domain);
+ cb->ptr_host = domain;
+ msg_debug_spf("resolve redirect %s", domain);
+
+ if (rspamd_dns_resolver_request_task_forced(task,
+ spf_record_dns_callback, (void *) cb, RDNS_REQUEST_TXT, domain)) {
+ rec->requests_inflight++;
+
+ return TRUE;
+ }
+ else {
+ msg_notice_spf("unresolvable redirect element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ }
+
+ return FALSE;
+}
+
+static gboolean
+parse_spf_exists(struct spf_record *rec, struct spf_addr *addr)
+{
+ struct spf_dns_cb *cb;
+ const gchar *host;
+ struct rspamd_task *task = rec->task;
+ struct spf_resolved_element *resolved;
+
+ resolved = g_ptr_array_index(rec->resolved, rec->resolved->len - 1);
+ CHECK_REC(rec);
+
+ host = strchr(addr->spf_string, ':');
+ if (host == NULL) {
+ host = strchr(addr->spf_string, '=');
+
+ if (host == NULL) {
+ msg_notice_spf("invalid exists element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ return FALSE;
+ }
+ }
+
+ host++;
+ rec->dns_requests++;
+
+ cb = rspamd_mempool_alloc(task->task_pool, sizeof(struct spf_dns_cb));
+ cb->rec = rec;
+ cb->addr = addr;
+ cb->cur_action = SPF_RESOLVE_EXISTS;
+ cb->resolved = resolved;
+ cb->ptr_host = host;
+
+ msg_debug_spf("resolve exists %s", host);
+ if (rspamd_dns_resolver_request_task_forced(task,
+ spf_record_dns_callback, (void *) cb, RDNS_REQUEST_A, host)) {
+ rec->requests_inflight++;
+
+ return TRUE;
+ }
+ else {
+ msg_notice_spf("unresolvable exists element for %s: %s", addr->spf_string,
+ rec->sender_domain);
+ }
+
+ return FALSE;
+}
+
+static gsize
+rspamd_spf_split_elt(const gchar *val, gsize len, gint *pos,
+ gsize poslen, gchar delim)
+{
+ const gchar *p, *end;
+ guint cur_pos = 0, cur_st = 0, nsub = 0;
+
+ p = val;
+ end = val + len;
+
+ while (p < end && cur_pos + 2 < poslen) {
+ if (*p == delim) {
+ if (p - val > cur_st) {
+ pos[cur_pos] = cur_st;
+ pos[cur_pos + 1] = p - val;
+ cur_st = p - val + 1;
+ cur_pos += 2;
+ nsub++;
+ }
+
+ p++;
+ }
+ else {
+ p++;
+ }
+ }
+
+ if (cur_pos + 2 < poslen) {
+ if (end - val > cur_st) {
+ pos[cur_pos] = cur_st;
+ pos[cur_pos + 1] = end - val;
+ nsub++;
+ }
+ }
+ else {
+ pos[cur_pos] = p - val;
+ pos[cur_pos + 1] = end - val;
+ nsub++;
+ }
+
+ return nsub;
+}
+
+static gsize
+rspamd_spf_process_substitution(const gchar *macro_value,
+ gsize macro_len, guint ndelim, gchar delim, gboolean reversed,
+ gchar *dest)
+{
+ gchar *d = dest;
+ const gchar canon_delim = '.';
+ guint vlen, i;
+ gint pos[49 * 2], tlen;
+
+ if (!reversed && ndelim == 0 && delim == canon_delim) {
+ /* Trivial case */
+ memcpy(dest, macro_value, macro_len);
+
+ return macro_len;
+ }
+
+ vlen = rspamd_spf_split_elt(macro_value, macro_len,
+ pos, G_N_ELEMENTS(pos), delim);
+
+ if (vlen > 0) {
+ if (reversed) {
+ for (i = vlen - 1;; i--) {
+ tlen = pos[i * 2 + 1] - pos[i * 2];
+
+ if (i != 0) {
+ memcpy(d, &macro_value[pos[i * 2]], tlen);
+ d += tlen;
+ *d++ = canon_delim;
+ }
+ else {
+ memcpy(d, &macro_value[pos[i * 2]], tlen);
+ d += tlen;
+ break;
+ }
+ }
+ }
+ else {
+ for (i = 0; i < vlen; i++) {
+ tlen = pos[i * 2 + 1] - pos[i * 2];
+
+ if (i != vlen - 1) {
+ memcpy(d, &macro_value[pos[i * 2]], tlen);
+ d += tlen;
+ *d++ = canon_delim;
+ }
+ else {
+ memcpy(d, &macro_value[pos[i * 2]], tlen);
+ d += tlen;
+ }
+ }
+ }
+ }
+ else {
+ /* Trivial case */
+ memcpy(dest, macro_value, macro_len);
+
+ return macro_len;
+ }
+
+ return (d - dest);
+}
+
+static const gchar *
+expand_spf_macro(struct spf_record *rec, struct spf_resolved_element *resolved,
+ const gchar *begin)
+{
+ const gchar *p, *macro_value = NULL;
+ gchar *c, *new, *tmp, delim = '.';
+ gsize len = 0, macro_len = 0;
+ gint state = 0, ndelim = 0;
+ gchar ip_buf[64 + 1]; /* cannot use INET6_ADDRSTRLEN as we use ptr lookup */
+ gboolean need_expand = FALSE, reversed;
+ struct rspamd_task *task;
+
+ g_assert(rec != NULL);
+ g_assert(begin != NULL);
+
+ task = rec->task;
+ p = begin;
+ /* Calculate length */
+ while (*p) {
+ switch (state) {
+ case 0:
+ /* Skip any character and wait for % in input */
+ if (*p == '%') {
+ state = 1;
+ }
+ else {
+ len++;
+ }
+
+ p++;
+ break;
+ case 1:
+ /* We got % sign, so we should whether wait for { or for - or for _ or for % */
+ if (*p == '%' || *p == '_') {
+ /* Just a single % sign or space */
+ len++;
+ state = 0;
+ }
+ else if (*p == '-') {
+ /* %20 */
+ len += sizeof("%20") - 1;
+ state = 0;
+ }
+ else if (*p == '{') {
+ state = 2;
+ }
+ else {
+ /* Something unknown */
+ msg_notice_spf(
+ "spf error for domain %s: unknown spf element",
+ rec->sender_domain);
+ return begin;
+ }
+ p++;
+
+ break;
+ case 2:
+ /* Read macro name */
+ switch (g_ascii_tolower(*p)) {
+ case 'i':
+ len += sizeof(ip_buf) - 1;
+ break;
+ case 's':
+ if (rec->sender) {
+ len += strlen(rec->sender);
+ }
+ else {
+ len += sizeof("unknown") - 1;
+ }
+ break;
+ case 'l':
+ if (rec->local_part) {
+ len += strlen(rec->local_part);
+ }
+ else {
+ len += sizeof("unknown") - 1;
+ }
+ break;
+ case 'o':
+ if (rec->sender_domain) {
+ len += strlen(rec->sender_domain);
+ }
+ else {
+ len += sizeof("unknown") - 1;
+ }
+ break;
+ case 'd':
+ if (resolved->cur_domain) {
+ len += strlen(resolved->cur_domain);
+ }
+ else {
+ len += sizeof("unknown") - 1;
+ }
+ break;
+ case 'v':
+ len += sizeof("in-addr") - 1;
+ break;
+ case 'h':
+ if (task->helo) {
+ len += strlen(task->helo);
+ }
+ else {
+ len += sizeof("unknown") - 1;
+ }
+ break;
+ default:
+ msg_notice_spf(
+ "spf error for domain %s: unknown or "
+ "unsupported spf macro %c in %s",
+ rec->sender_domain,
+ *p,
+ begin);
+ return begin;
+ }
+ p++;
+ state = 3;
+ break;
+ case 3:
+ /* Read modifier */
+ if (*p == '}') {
+ state = 0;
+ need_expand = TRUE;
+ }
+ p++;
+ break;
+
+ default:
+ g_assert_not_reached();
+ }
+ }
+
+ if (!need_expand) {
+ /* No expansion needed */
+ return begin;
+ }
+
+ new = rspamd_mempool_alloc(task->task_pool, len + 1);
+
+ /* Reduce TTL to avoid caching of records with macros */
+ if (rec->ttl != 0) {
+ rec->ttl = 0;
+ msg_debug_spf("disable SPF caching as there is macro expansion");
+ }
+
+ c = new;
+ p = begin;
+ state = 0;
+ /* Begin macro expansion */
+
+ while (*p) {
+ switch (state) {
+ case 0:
+ /* Skip any character and wait for % in input */
+ if (*p == '%') {
+ state = 1;
+ }
+ else {
+ *c = *p;
+ c++;
+ }
+
+ p++;
+ break;
+ case 1:
+ /* We got % sign, so we should whether wait for { or for - or for _ or for % */
+ if (*p == '%') {
+ /* Just a single % sign or space */
+ *c++ = '%';
+ state = 0;
+ }
+ else if (*p == '_') {
+ *c++ = ' ';
+ state = 0;
+ }
+ else if (*p == '-') {
+ /* %20 */
+ *c++ = '%';
+ *c++ = '2';
+ *c++ = '0';
+ state = 0;
+ }
+ else if (*p == '{') {
+ state = 2;
+ }
+ else {
+ /* Something unknown */
+ msg_info_spf(
+ "spf error for domain %s: unknown spf element",
+ rec->sender_domain);
+ return begin;
+ }
+ p++;
+ break;
+ case 2:
+ /* Read macro name */
+ switch (g_ascii_tolower(*p)) {
+ case 'i':
+ if (task->from_addr) {
+ if (rspamd_inet_address_get_af(task->from_addr) == AF_INET) {
+ macro_len = rspamd_strlcpy(ip_buf,
+ rspamd_inet_address_to_string(task->from_addr),
+ sizeof(ip_buf));
+ macro_value = ip_buf;
+ }
+ else if (rspamd_inet_address_get_af(task->from_addr) == AF_INET6) {
+ /* See #3625 for details */
+ socklen_t slen;
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)
+ rspamd_inet_address_get_sa(task->from_addr, &slen);
+
+ /* Expand IPv6 address */
+#define IPV6_OCTET(x) bytes[(x)] >> 4, bytes[(x)] & 0xF
+ unsigned char *bytes = (unsigned char *) &sin6->sin6_addr;
+ macro_len = rspamd_snprintf(ip_buf, sizeof(ip_buf),
+ "%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd."
+ "%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd.%xd",
+ IPV6_OCTET(0), IPV6_OCTET(1),
+ IPV6_OCTET(2), IPV6_OCTET(3),
+ IPV6_OCTET(4), IPV6_OCTET(5),
+ IPV6_OCTET(6), IPV6_OCTET(7),
+ IPV6_OCTET(8), IPV6_OCTET(9),
+ IPV6_OCTET(10), IPV6_OCTET(11),
+ IPV6_OCTET(12), IPV6_OCTET(13),
+ IPV6_OCTET(14), IPV6_OCTET(15));
+ macro_value = ip_buf;
+#undef IPV6_OCTET
+ }
+ else {
+ macro_len = rspamd_snprintf(ip_buf, sizeof(ip_buf),
+ "127.0.0.1");
+ macro_value = ip_buf;
+ }
+ }
+ else {
+ macro_len = rspamd_snprintf(ip_buf, sizeof(ip_buf),
+ "127.0.0.1");
+ macro_value = ip_buf;
+ }
+ break;
+ case 's':
+ if (rec->sender) {
+ macro_len = strlen(rec->sender);
+ macro_value = rec->sender;
+ }
+ else {
+ macro_len = sizeof("unknown") - 1;
+ macro_value = "unknown";
+ }
+ break;
+ case 'l':
+ if (rec->local_part) {
+ macro_len = strlen(rec->local_part);
+ macro_value = rec->local_part;
+ }
+ else {
+ macro_len = sizeof("unknown") - 1;
+ macro_value = "unknown";
+ }
+ break;
+ case 'o':
+ if (rec->sender_domain) {
+ macro_len = strlen(rec->sender_domain);
+ macro_value = rec->sender_domain;
+ }
+ else {
+ macro_len = sizeof("unknown") - 1;
+ macro_value = "unknown";
+ }
+ break;
+ case 'd':
+ if (resolved && resolved->cur_domain) {
+ macro_len = strlen(resolved->cur_domain);
+ macro_value = resolved->cur_domain;
+ }
+ else {
+ macro_len = sizeof("unknown") - 1;
+ macro_value = "unknown";
+ }
+ break;
+ case 'v':
+ if (task->from_addr) {
+ if (rspamd_inet_address_get_af(task->from_addr) == AF_INET) {
+ macro_len = sizeof("in-addr") - 1;
+ macro_value = "in-addr";
+ }
+ else {
+ macro_len = sizeof("ip6") - 1;
+ macro_value = "ip6";
+ }
+ }
+ else {
+ macro_len = sizeof("in-addr") - 1;
+ macro_value = "in-addr";
+ }
+ break;
+ case 'h':
+ if (task->helo) {
+ tmp = strchr(task->helo, '@');
+ if (tmp) {
+ macro_len = strlen(tmp + 1);
+ macro_value = tmp + 1;
+ }
+ else {
+ macro_len = strlen(task->helo);
+ macro_value = task->helo;
+ }
+ }
+ else {
+ macro_len = sizeof("unknown") - 1;
+ macro_value = "unknown";
+ }
+ break;
+ default:
+ msg_info_spf(
+ "spf error for domain %s: unknown or "
+ "unsupported spf macro %c in %s",
+ rec->sender_domain,
+ *p,
+ begin);
+ return begin;
+ }
+
+ p++;
+ state = 3;
+ ndelim = 0;
+ delim = '.';
+ reversed = FALSE;
+ break;
+
+ case 3:
+ /* Read modifier */
+ if (*p == '}') {
+ state = 0;
+ len = rspamd_spf_process_substitution(macro_value,
+ macro_len, ndelim, delim, reversed, c);
+ c += len;
+ }
+ else if (*p == 'r' && len != 0) {
+ reversed = TRUE;
+ }
+ else if (g_ascii_isdigit(*p)) {
+ ndelim = strtoul(p, &tmp, 10);
+
+ if (tmp == NULL || tmp == p) {
+ p++;
+ }
+ else {
+ p = tmp;
+
+ continue;
+ }
+ }
+ else if (*p == '+' || *p == '-' ||
+ *p == '.' || *p == ',' || *p == '/' || *p == '_' ||
+ *p == '=') {
+ delim = *p;
+ }
+ else {
+ msg_info_spf("spf error for domain %s: unknown or "
+ "unsupported spf macro %c in %s",
+ rec->sender_domain,
+ *p,
+ begin);
+ return begin;
+ }
+ p++;
+ break;
+ }
+ }
+ /* Null terminate */
+ *c = '\0';
+
+ return new;
+}
+
+/* Read current element and try to parse record */
+static gboolean
+spf_process_element(struct spf_record *rec,
+ struct spf_resolved_element *resolved,
+ const gchar *elt,
+ const gchar **elts)
+{
+ struct spf_addr *addr = NULL;
+ gboolean res = FALSE;
+ const gchar *begin;
+ gchar t;
+
+ g_assert(elt != NULL);
+ g_assert(rec != NULL);
+
+ if (*elt == '\0' || resolved->redirected) {
+ return TRUE;
+ }
+
+ begin = expand_spf_macro(rec, resolved, elt);
+ addr = rspamd_spf_new_addr(rec, resolved, begin);
+ g_assert(addr != NULL);
+ t = g_ascii_tolower(addr->spf_string[0]);
+ begin = addr->spf_string;
+
+ /* Now check what we have */
+ switch (t) {
+ case 'a':
+ /* all or a */
+ if (g_ascii_strncasecmp(begin, SPF_ALL,
+ sizeof(SPF_ALL) - 1) == 0) {
+ res = parse_spf_all(rec, addr);
+ }
+ else if (g_ascii_strncasecmp(begin, SPF_A,
+ sizeof(SPF_A) - 1) == 0) {
+ res = parse_spf_a(rec, resolved, addr);
+ }
+ else {
+ msg_notice_spf("spf error for domain %s: bad spf command %s",
+ rec->sender_domain, begin);
+ }
+ break;
+ case 'i':
+ /* include or ip4 */
+ if (g_ascii_strncasecmp(begin, SPF_IP4, sizeof(SPF_IP4) - 1) == 0) {
+ res = parse_spf_ip4(rec, addr);
+ }
+ else if (g_ascii_strncasecmp(begin, SPF_INCLUDE, sizeof(SPF_INCLUDE) - 1) == 0) {
+ res = parse_spf_include(rec, addr);
+ }
+ else if (g_ascii_strncasecmp(begin, SPF_IP6, sizeof(SPF_IP6) - 1) == 0) {
+ res = parse_spf_ip6(rec, addr);
+ }
+ else if (g_ascii_strncasecmp(begin, SPF_IP4_ALT, sizeof(SPF_IP4_ALT) - 1) == 0) {
+ res = parse_spf_ip4(rec, addr);
+ }
+ else if (g_ascii_strncasecmp(begin, SPF_IP6_ALT, sizeof(SPF_IP6_ALT) - 1) == 0) {
+ res = parse_spf_ip6(rec, addr);
+ }
+ else {
+ msg_notice_spf("spf error for domain %s: bad spf command %s",
+ rec->sender_domain, begin);
+ }
+ break;
+ case 'm':
+ /* mx */
+ if (g_ascii_strncasecmp(begin, SPF_MX, sizeof(SPF_MX) - 1) == 0) {
+ res = parse_spf_mx(rec, resolved, addr);
+ }
+ else {
+ msg_notice_spf("spf error for domain %s: bad spf command %s",
+ rec->sender_domain, begin);
+ }
+ break;
+ case 'p':
+ /* ptr */
+ if (g_ascii_strncasecmp(begin, SPF_PTR,
+ sizeof(SPF_PTR) - 1) == 0) {
+ res = parse_spf_ptr(rec, resolved, addr);
+ }
+ else {
+ msg_notice_spf("spf error for domain %s: bad spf command %s",
+ rec->sender_domain, begin);
+ }
+ break;
+ case 'e':
+ /* exp or exists */
+ if (g_ascii_strncasecmp(begin, SPF_EXP,
+ sizeof(SPF_EXP) - 1) == 0) {
+ res = parse_spf_exp(rec, addr);
+ }
+ else if (g_ascii_strncasecmp(begin, SPF_EXISTS,
+ sizeof(SPF_EXISTS) - 1) == 0) {
+ res = parse_spf_exists(rec, addr);
+ }
+ else {
+ msg_notice_spf("spf error for domain %s: bad spf command %s",
+ rec->sender_domain, begin);
+ }
+ break;
+ case 'r':
+ /* redirect */
+ if (g_ascii_strncasecmp(begin, SPF_REDIRECT,
+ sizeof(SPF_REDIRECT) - 1) == 0) {
+ /*
+ * According to https://tools.ietf.org/html/rfc7208#section-6.1
+ * There must be no ALL element anywhere in the record,
+ * redirect must be ignored
+ */
+ gboolean ignore_redirect = FALSE;
+
+ for (const gchar **tmp = elts; *tmp != NULL; tmp++) {
+ if (g_ascii_strcasecmp((*tmp) + 1, "all") == 0) {
+ ignore_redirect = TRUE;
+ break;
+ }
+ }
+
+ if (!ignore_redirect) {
+ res = parse_spf_redirect(rec, resolved, addr);
+ }
+ else {
+ msg_notice_spf("ignore SPF redirect (%s) for domain %s as there is also all element",
+ begin, rec->sender_domain);
+
+ /* Pop the current addr as it is ignored */
+ g_ptr_array_remove_index_fast(resolved->elts,
+ resolved->elts->len - 1);
+
+ return TRUE;
+ }
+ }
+ else {
+ msg_notice_spf("spf error for domain %s: bad spf command %s",
+ rec->sender_domain, begin);
+ }
+ break;
+ case 'v':
+ if (g_ascii_strncasecmp(begin, "v=spf",
+ sizeof("v=spf") - 1) == 0) {
+ /* Skip this element till the end of record */
+ while (*begin && !g_ascii_isspace(*begin)) {
+ begin++;
+ }
+ }
+ break;
+ default:
+ msg_notice_spf("spf error for domain %s: bad spf command %s",
+ rec->sender_domain, begin);
+ break;
+ }
+
+ if (res) {
+ addr->flags |= RSPAMD_SPF_FLAG_PARSED;
+ }
+
+ return res;
+}
+
+static void
+parse_spf_scopes(struct spf_record *rec, gchar **begin)
+{
+ for (;;) {
+ if (g_ascii_strncasecmp(*begin, SPF_SCOPE_PRA, sizeof(SPF_SCOPE_PRA) - 1) == 0) {
+ *begin += sizeof(SPF_SCOPE_PRA) - 1;
+ /* XXX: Implement actual PRA check */
+ /* extract_pra_info (rec); */
+ continue;
+ }
+ else if (g_ascii_strncasecmp(*begin, SPF_SCOPE_MFROM,
+ sizeof(SPF_SCOPE_MFROM) - 1) == 0) {
+ /* mfrom is standard spf1 check */
+ *begin += sizeof(SPF_SCOPE_MFROM) - 1;
+ continue;
+ }
+ else if (**begin != ',') {
+ break;
+ }
+ (*begin)++;
+ }
+}
+
+static gboolean
+start_spf_parse(struct spf_record *rec, struct spf_resolved_element *resolved,
+ gchar *begin)
+{
+ gchar **elts, **cur_elt;
+ gsize len;
+
+ /* Skip spaces */
+ while (g_ascii_isspace(*begin)) {
+ begin++;
+ }
+
+ len = strlen(begin);
+
+ if (g_ascii_strncasecmp(begin, SPF_VER1_STR, sizeof(SPF_VER1_STR) - 1) ==
+ 0) {
+ begin += sizeof(SPF_VER1_STR) - 1;
+
+ while (g_ascii_isspace(*begin) && *begin) {
+ begin++;
+ }
+ }
+ else if (g_ascii_strncasecmp(begin, SPF_VER2_STR, sizeof(SPF_VER2_STR) - 1) == 0) {
+ /* Skip one number of record, so no we are here spf2.0/ */
+ begin += sizeof(SPF_VER2_STR);
+ if (*begin != '/') {
+ msg_notice_spf("spf error for domain %s: sender id is invalid",
+ rec->sender_domain);
+ }
+ else {
+ begin++;
+ parse_spf_scopes(rec, &begin);
+ }
+ /* Now common spf record */
+ }
+ else {
+ msg_debug_spf(
+ "spf error for domain %s: bad spf record start: %*s",
+ rec->sender_domain,
+ (gint) len,
+ begin);
+
+ return FALSE;
+ }
+
+ while (g_ascii_isspace(*begin) && *begin) {
+ begin++;
+ }
+
+ elts = g_strsplit_set(begin, " ", 0);
+
+ if (elts) {
+ cur_elt = elts;
+
+ while (*cur_elt) {
+ spf_process_element(rec, resolved, *cur_elt, (const gchar **) elts);
+ cur_elt++;
+ }
+
+ g_strfreev(elts);
+ }
+
+ rspamd_spf_maybe_return(rec);
+
+ return TRUE;
+}
+
+static void
+spf_dns_callback(struct rdns_reply *reply, gpointer arg)
+{
+ struct spf_record *rec = arg;
+ struct spf_resolved_element *resolved = NULL;
+ struct spf_addr *addr;
+
+ rec->requests_inflight--;
+
+ if (reply->flags & RDNS_TRUNCATED) {
+ msg_warn_spf("got a truncated record when trying to resolve TXT record for %s",
+ rec->sender_domain);
+ resolved = rspamd_spf_new_addr_list(rec, rec->sender_domain);
+ addr = g_malloc0(sizeof(*addr));
+ addr->flags |= RSPAMD_SPF_FLAG_TEMPFAIL;
+ g_ptr_array_insert(resolved->elts, 0, addr);
+
+ rspamd_spf_maybe_return(rec);
+
+ return;
+ }
+ else {
+ if (reply->code == RDNS_RC_NOERROR) {
+ resolved = rspamd_spf_new_addr_list(rec, rec->sender_domain);
+ if (rec->resolved->len == 1) {
+ /* Top level resolved element */
+ rec->ttl = reply->entries->ttl;
+ }
+ }
+ else if ((reply->code == RDNS_RC_NOREC || reply->code == RDNS_RC_NXDOMAIN) && rec->dns_requests == 0) {
+ resolved = rspamd_spf_new_addr_list(rec, rec->sender_domain);
+ addr = g_malloc0(sizeof(*addr));
+ addr->flags |= RSPAMD_SPF_FLAG_NA;
+ g_ptr_array_insert(resolved->elts, 0, addr);
+ }
+ else if (reply->code != RDNS_RC_NOREC && reply->code != RDNS_RC_NXDOMAIN && rec->dns_requests == 0) {
+ resolved = rspamd_spf_new_addr_list(rec, rec->sender_domain);
+ addr = g_malloc0(sizeof(*addr));
+ addr->flags |= RSPAMD_SPF_FLAG_TEMPFAIL;
+ g_ptr_array_insert(resolved->elts, 0, addr);
+ }
+ }
+
+ if (resolved) {
+ struct rdns_reply_entry *selected = NULL;
+
+ if (!spf_process_txt_record(rec, resolved, reply, &selected)) {
+ resolved = g_ptr_array_index(rec->resolved, 0);
+
+ if (rec->resolved->len > 1) {
+ addr = g_ptr_array_index(resolved->elts, 0);
+ if ((reply->code == RDNS_RC_NOREC || reply->code == RDNS_RC_NXDOMAIN) && (addr->flags & RSPAMD_SPF_FLAG_REDIRECT)) {
+ addr->flags |= RSPAMD_SPF_FLAG_PERMFAIL;
+ }
+ else {
+ addr->flags |= RSPAMD_SPF_FLAG_TEMPFAIL;
+ }
+ }
+ else {
+ addr = g_malloc0(sizeof(*addr));
+
+ if (reply->code == RDNS_RC_NOREC || reply->code == RDNS_RC_NXDOMAIN || reply->code == RDNS_RC_NOERROR) {
+ addr->flags |= RSPAMD_SPF_FLAG_NA;
+ }
+ else {
+ addr->flags |= RSPAMD_SPF_FLAG_TEMPFAIL;
+ }
+ g_ptr_array_insert(resolved->elts, 0, addr);
+ }
+ }
+ else {
+ rec->top_record = rspamd_mempool_strdup(rec->task->task_pool,
+ selected->content.txt.data);
+ rspamd_mempool_set_variable(rec->task->task_pool,
+ RSPAMD_MEMPOOL_SPF_RECORD,
+ (gpointer) rec->top_record, NULL);
+ }
+ }
+
+ rspamd_spf_maybe_return(rec);
+}
+
+static struct rspamd_spf_cred *
+rspamd_spf_cache_domain(struct rspamd_task *task)
+{
+ struct rspamd_email_address *addr;
+ struct rspamd_spf_cred *cred = NULL;
+
+ addr = rspamd_task_get_sender(task);
+ if (!addr || (addr->flags & RSPAMD_EMAIL_ADDR_EMPTY)) {
+ /* Get domain from helo */
+
+ if (task->helo) {
+ GString *fs = g_string_new("");
+
+ cred = rspamd_mempool_alloc(task->task_pool, sizeof(*cred));
+ cred->domain = task->helo;
+ cred->local_part = "postmaster";
+ rspamd_printf_gstring(fs, "postmaster@%s", cred->domain);
+ cred->sender = fs->str;
+ rspamd_mempool_add_destructor(task->task_pool,
+ rspamd_gstring_free_hard, fs);
+ }
+ }
+ else {
+ rspamd_ftok_t tok;
+
+ cred = rspamd_mempool_alloc(task->task_pool, sizeof(*cred));
+ tok.begin = addr->domain;
+ tok.len = addr->domain_len;
+ cred->domain = rspamd_mempool_ftokdup(task->task_pool, &tok);
+ tok.begin = addr->user;
+ tok.len = addr->user_len;
+ cred->local_part = rspamd_mempool_ftokdup(task->task_pool, &tok);
+ tok.begin = addr->addr;
+ tok.len = addr->addr_len;
+ cred->sender = rspamd_mempool_ftokdup(task->task_pool, &tok);
+ }
+
+ if (cred) {
+ rspamd_mempool_set_variable(task->task_pool, RSPAMD_MEMPOOL_SPF_DOMAIN,
+ cred, NULL);
+ }
+
+ return cred;
+}
+
+struct rspamd_spf_cred *
+rspamd_spf_get_cred(struct rspamd_task *task)
+{
+ struct rspamd_spf_cred *cred;
+
+ cred = rspamd_mempool_get_variable(task->task_pool,
+ RSPAMD_MEMPOOL_SPF_DOMAIN);
+
+ if (!cred) {
+ cred = rspamd_spf_cache_domain(task);
+ }
+
+ return cred;
+}
+
+const gchar *
+rspamd_spf_get_domain(struct rspamd_task *task)
+{
+ gchar *domain = NULL;
+ struct rspamd_spf_cred *cred;
+
+ cred = rspamd_spf_get_cred(task);
+
+ if (cred) {
+ domain = cred->domain;
+ }
+
+ return domain;
+}
+
+gboolean
+rspamd_spf_resolve(struct rspamd_task *task, spf_cb_t callback,
+ gpointer cbdata, struct rspamd_spf_cred *cred)
+{
+ struct spf_record *rec;
+
+ if (!cred || !cred->domain) {
+ return FALSE;
+ }
+
+ /* First lookup in the hash */
+ if (spf_lib_ctx->spf_hash) {
+ struct spf_resolved *cached;
+
+ cached = rspamd_lru_hash_lookup(spf_lib_ctx->spf_hash, cred->domain,
+ task->task_timestamp);
+
+ if (cached) {
+ cached->flags |= RSPAMD_SPF_FLAG_CACHED;
+
+ if (cached->top_record) {
+ rspamd_mempool_set_variable(task->task_pool,
+ RSPAMD_MEMPOOL_SPF_RECORD,
+ rspamd_mempool_strdup(task->task_pool,
+ cached->top_record),
+ NULL);
+ }
+ callback(cached, task, cbdata);
+
+ return TRUE;
+ }
+ }
+
+
+ rec = rspamd_mempool_alloc0(task->task_pool, sizeof(struct spf_record));
+ rec->task = task;
+ rec->callback = callback;
+ rec->cbdata = cbdata;
+
+ rec->resolved = g_ptr_array_sized_new(8);
+
+ /* Add destructor */
+ rspamd_mempool_add_destructor(task->task_pool,
+ (rspamd_mempool_destruct_t) spf_record_destructor,
+ rec);
+
+ /* Extract from data */
+ rec->sender = cred->sender;
+ rec->local_part = cred->local_part;
+ rec->sender_domain = cred->domain;
+
+ if (rspamd_dns_resolver_request_task_forced(task,
+ spf_dns_callback,
+ (void *) rec, RDNS_REQUEST_TXT, rec->sender_domain)) {
+ rec->requests_inflight++;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+struct spf_resolved *
+_spf_record_ref(struct spf_resolved *flat, const gchar *loc)
+{
+ REF_RETAIN(flat);
+ return flat;
+}
+
+void _spf_record_unref(struct spf_resolved *flat, const gchar *loc)
+{
+ REF_RELEASE(flat);
+}
+
+gchar *
+spf_addr_mask_to_string(struct spf_addr *addr)
+{
+ GString *res;
+ gchar *s, ipstr[INET6_ADDRSTRLEN + 1];
+
+ if (addr->flags & RSPAMD_SPF_FLAG_ANY) {
+ res = g_string_new("any");
+ }
+ else if (addr->flags & RSPAMD_SPF_FLAG_IPV4) {
+ (void) inet_ntop(AF_INET, addr->addr4, ipstr, sizeof(ipstr));
+ res = g_string_sized_new(sizeof(ipstr));
+ rspamd_printf_gstring(res, "%s/%d", ipstr, addr->m.dual.mask_v4);
+ }
+ else if (addr->flags & RSPAMD_SPF_FLAG_IPV6) {
+ (void) inet_ntop(AF_INET6, addr->addr6, ipstr, sizeof(ipstr));
+ res = g_string_sized_new(sizeof(ipstr));
+ rspamd_printf_gstring(res, "%s/%d", ipstr, addr->m.dual.mask_v6);
+ }
+ else {
+ res = g_string_new(NULL);
+ rspamd_printf_gstring(res, "unknown, flags = %d", addr->flags);
+ }
+
+ s = res->str;
+ g_string_free(res, FALSE);
+
+
+ return s;
+}
+
+struct spf_addr *
+spf_addr_match_task(struct rspamd_task *task, struct spf_resolved *rec)
+{
+ const guint8 *s, *d;
+ guint af, mask, bmask, addrlen;
+ struct spf_addr *selected = NULL, *addr, *any_addr = NULL;
+ guint i;
+
+ if (task->from_addr == NULL) {
+ return FALSE;
+ }
+
+ for (i = 0; i < rec->elts->len; i++) {
+ addr = &g_array_index(rec->elts, struct spf_addr, i);
+ if (addr->flags & RSPAMD_SPF_FLAG_TEMPFAIL) {
+ continue;
+ }
+
+ af = rspamd_inet_address_get_af(task->from_addr);
+ /* Basic comparing algorithm */
+ if (((addr->flags & RSPAMD_SPF_FLAG_IPV6) && af == AF_INET6) ||
+ ((addr->flags & RSPAMD_SPF_FLAG_IPV4) && af == AF_INET)) {
+ d = rspamd_inet_address_get_hash_key(task->from_addr, &addrlen);
+
+ if (af == AF_INET6) {
+ s = (const guint8 *) addr->addr6;
+ mask = addr->m.dual.mask_v6;
+ }
+ else {
+ s = (const guint8 *) addr->addr4;
+ mask = addr->m.dual.mask_v4;
+ }
+
+ /* Compare the first bytes */
+ bmask = mask / CHAR_BIT;
+ if (mask > addrlen * CHAR_BIT) {
+ msg_info_task("bad mask length: %d", mask);
+ }
+ else if (memcmp(s, d, bmask) == 0) {
+ if (bmask * CHAR_BIT < mask) {
+ /* Compare the remaining bits */
+ s += bmask;
+ d += bmask;
+ mask = (0xffu << (CHAR_BIT - (mask - bmask * 8u))) & 0xffu;
+
+ if ((*s & mask) == (*d & mask)) {
+ selected = addr;
+ break;
+ }
+ }
+ else {
+ selected = addr;
+ break;
+ }
+ }
+ }
+ else {
+ if (addr->flags & RSPAMD_SPF_FLAG_ANY) {
+ any_addr = addr;
+ }
+ }
+ }
+
+ if (selected) {
+ return selected;
+ }
+
+ return any_addr;
+} \ No newline at end of file
diff --git a/src/libserver/spf.h b/src/libserver/spf.h
new file mode 100644
index 0000000..871ed29
--- /dev/null
+++ b/src/libserver/spf.h
@@ -0,0 +1,159 @@
+#ifndef RSPAMD_SPF_H
+#define RSPAMD_SPF_H
+
+#include "config.h"
+#include "ref.h"
+#include "addr.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_task;
+struct spf_resolved;
+
+typedef void (*spf_cb_t)(struct spf_resolved *record,
+ struct rspamd_task *task, gpointer cbdata);
+
+typedef enum spf_mech_e {
+ SPF_FAIL,
+ SPF_SOFT_FAIL,
+ SPF_PASS,
+ SPF_NEUTRAL
+} spf_mech_t;
+
+static inline gchar spf_mech_char(spf_mech_t mech)
+{
+ switch (mech) {
+ case SPF_FAIL:
+ return '-';
+ case SPF_SOFT_FAIL:
+ return '~';
+ case SPF_PASS:
+ return '+';
+ case SPF_NEUTRAL:
+ default:
+ return '?';
+ }
+}
+
+typedef enum spf_action_e {
+ SPF_RESOLVE_MX,
+ SPF_RESOLVE_A,
+ SPF_RESOLVE_PTR,
+ SPF_RESOLVE_AAA,
+ SPF_RESOLVE_REDIRECT,
+ SPF_RESOLVE_INCLUDE,
+ SPF_RESOLVE_EXISTS,
+ SPF_RESOLVE_EXP
+} spf_action_t;
+
+#define RSPAMD_SPF_FLAG_IPV6 (1u << 0u)
+#define RSPAMD_SPF_FLAG_IPV4 (1u << 1u)
+#define RSPAMD_SPF_FLAG_PROCESSED (1u << 2u)
+#define RSPAMD_SPF_FLAG_ANY (1u << 3u)
+#define RSPAMD_SPF_FLAG_PARSED (1u << 4u)
+#define RSPAMD_SPF_FLAG_INVALID (1u << 5u)
+#define RSPAMD_SPF_FLAG_REFERENCE (1u << 6u)
+#define RSPAMD_SPF_FLAG_REDIRECT (1u << 7u)
+#define RSPAMD_SPF_FLAG_TEMPFAIL (1u << 8u)
+#define RSPAMD_SPF_FLAG_NA (1u << 9u)
+#define RSPAMD_SPF_FLAG_PERMFAIL (1u << 10u)
+#define RSPAMD_SPF_FLAG_RESOLVED (1u << 11u)
+#define RSPAMD_SPF_FLAG_CACHED (1u << 12u)
+
+/** Default SPF limits for avoiding abuse **/
+#define SPF_MAX_NESTING 10
+#define SPF_MAX_DNS_REQUESTS 30
+#define SPF_MIN_CACHE_TTL (60 * 5) /* 5 minutes */
+
+struct spf_addr {
+ guchar addr6[sizeof(struct in6_addr)];
+ guchar addr4[sizeof(struct in_addr)];
+ union {
+ struct {
+ guint16 mask_v4;
+ guint16 mask_v6;
+ } dual;
+ guint32 idx;
+ } m;
+ guint flags;
+ spf_mech_t mech;
+ gchar *spf_string;
+ struct spf_addr *prev, *next;
+};
+
+enum rspamd_spf_resolved_flags {
+ RSPAMD_SPF_RESOLVED_NORMAL = 0,
+ RSPAMD_SPF_RESOLVED_TEMP_FAILED = (1u << 0u),
+ RSPAMD_SPF_RESOLVED_PERM_FAILED = (1u << 1u),
+ RSPAMD_SPF_RESOLVED_NA = (1u << 2u),
+};
+
+struct spf_resolved {
+ gchar *domain;
+ gchar *top_record;
+ guint ttl;
+ gint flags;
+ gdouble timestamp;
+ guint64 digest;
+ GArray *elts; /* Flat list of struct spf_addr */
+ ref_entry_t ref; /* Refcounting */
+};
+
+struct rspamd_spf_cred {
+ gchar *local_part;
+ gchar *domain;
+ gchar *sender;
+};
+
+/*
+ * Resolve spf record for specified task and call a callback after resolution fails/succeed
+ */
+gboolean rspamd_spf_resolve(struct rspamd_task *task,
+ spf_cb_t callback,
+ gpointer cbdata,
+ struct rspamd_spf_cred *cred);
+
+/*
+ * Get a domain for spf for specified task
+ */
+const gchar *rspamd_spf_get_domain(struct rspamd_task *task);
+
+struct rspamd_spf_cred *rspamd_spf_get_cred(struct rspamd_task *task);
+/*
+ * Increase refcount
+ */
+struct spf_resolved *_spf_record_ref(struct spf_resolved *rec, const gchar *loc);
+#define spf_record_ref(rec) \
+ _spf_record_ref((rec), G_STRLOC)
+/*
+ * Decrease refcount
+ */
+void _spf_record_unref(struct spf_resolved *rec, const gchar *loc);
+#define spf_record_unref(rec) \
+ _spf_record_unref((rec), G_STRLOC)
+
+/**
+ * Prints address + mask in a freshly allocated string (must be freed)
+ * @param addr
+ * @return
+ */
+gchar *spf_addr_mask_to_string(struct spf_addr *addr);
+
+/**
+ * Returns spf address that matches the specific task (or nil if not matched)
+ * @param task
+ * @param rec
+ * @return
+ */
+struct spf_addr *spf_addr_match_task(struct rspamd_task *task,
+ struct spf_resolved *rec);
+
+void spf_library_config(const ucl_object_t *obj);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/ssl_util.c b/src/libserver/ssl_util.c
new file mode 100644
index 0000000..8ee53b0
--- /dev/null
+++ b/src/libserver/ssl_util.c
@@ -0,0 +1,1133 @@
+/*-
+ * 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 "libutil/util.h"
+#include "libutil/hash.h"
+#include "libserver/logger.h"
+#include "libserver/cfg_file.h"
+#include "ssl_util.h"
+#include "unix-std.h"
+#include "cryptobox.h"
+#include "contrib/libottery/ottery.h"
+
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include <openssl/rand.h>
+#include <openssl/conf.h>
+#include <openssl/evp.h>
+#include <openssl/engine.h>
+#include <openssl/x509v3.h>
+
+enum rspamd_ssl_state {
+ ssl_conn_reset = 0,
+ ssl_conn_init,
+ ssl_conn_connected,
+ ssl_next_read,
+ ssl_next_write,
+ ssl_next_shutdown,
+};
+
+enum rspamd_ssl_shutdown {
+ ssl_shut_default = 0,
+ ssl_shut_unclean,
+};
+
+struct rspamd_ssl_ctx {
+ SSL_CTX *s;
+ rspamd_lru_hash_t *sessions;
+};
+
+struct rspamd_ssl_connection {
+ gint fd;
+ enum rspamd_ssl_state state;
+ enum rspamd_ssl_shutdown shut;
+ gboolean verify_peer;
+ SSL *ssl;
+ struct rspamd_ssl_ctx *ssl_ctx;
+ gchar *hostname;
+ struct rspamd_io_ev *ev;
+ struct rspamd_io_ev *shut_ev;
+ struct ev_loop *event_loop;
+ rspamd_ssl_handler_t handler;
+ rspamd_ssl_error_handler_t err_handler;
+ gpointer handler_data;
+ gchar log_tag[8];
+};
+
+#define msg_debug_ssl(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_ssl_log_id, "ssl", conn->log_tag, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+static void rspamd_ssl_event_handler(gint fd, short what, gpointer ud);
+
+INIT_LOG_MODULE(ssl)
+
+static GQuark
+rspamd_ssl_quark(void)
+{
+ return g_quark_from_static_string("rspamd-ssl");
+}
+
+#if (OPENSSL_VERSION_NUMBER >= 0x10100000L) && !defined(LIBRESSL_VERSION_NUMBER)
+#ifndef X509_get_notBefore
+#define X509_get_notBefore(x) X509_get0_notBefore(x)
+#endif
+#ifndef X509_get_notAfter
+#define X509_get_notAfter(x) X509_get0_notAfter(x)
+#endif
+#ifndef ASN1_STRING_data
+#define ASN1_STRING_data(x) ASN1_STRING_get0_data(x)
+#endif
+#endif
+
+/* $OpenBSD: tls_verify.c,v 1.14 2015/09/29 10:17:04 deraadt Exp $ */
+/*
+ * Copyright (c) 2014 Jeremie Courreges-Anglas <jca@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+static gboolean
+rspamd_tls_match_name(const char *cert_name, const char *name)
+{
+ const char *cert_domain, *domain, *next_dot;
+
+ if (g_ascii_strcasecmp(cert_name, name) == 0) {
+ return TRUE;
+ }
+
+ /* Wildcard match? */
+ if (cert_name[0] == '*') {
+ /*
+ * Valid wildcards:
+ * - "*.domain.tld"
+ * - "*.sub.domain.tld"
+ * - etc.
+ * Reject "*.tld".
+ * No attempt to prevent the use of eg. "*.co.uk".
+ */
+ cert_domain = &cert_name[1];
+ /* Disallow "*" */
+ if (cert_domain[0] == '\0') {
+ return FALSE;
+ }
+
+ /* Disallow "*foo" */
+ if (cert_domain[0] != '.') {
+ return FALSE;
+ }
+ /* Disallow "*.." */
+ if (cert_domain[1] == '.') {
+ return FALSE;
+ }
+ next_dot = strchr(&cert_domain[1], '.');
+ /* Disallow "*.bar" */
+ if (next_dot == NULL) {
+ return FALSE;
+ }
+ /* Disallow "*.bar.." */
+ if (next_dot[1] == '.') {
+ return FALSE;
+ }
+
+ domain = strchr(name, '.');
+
+ /* No wildcard match against a name with no host part. */
+ if (name[0] == '.') {
+ return FALSE;
+ }
+ /* No wildcard match against a name with no domain part. */
+ if (domain == NULL || strlen(domain) == 1) {
+ return FALSE;
+ }
+
+ if (g_ascii_strcasecmp(cert_domain, domain) == 0) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+/* See RFC 5280 section 4.2.1.6 for SubjectAltName details. */
+static gboolean
+rspamd_tls_check_subject_altname(X509 *cert, const char *name)
+{
+ STACK_OF(GENERAL_NAME) *altname_stack = NULL;
+ int addrlen, type;
+ int count, i;
+ union {
+ struct in_addr ip4;
+ struct in6_addr ip6;
+ } addrbuf;
+ gboolean ret = FALSE;
+
+ altname_stack = X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
+
+ if (altname_stack == NULL) {
+ return FALSE;
+ }
+
+ if (inet_pton(AF_INET, name, &addrbuf) == 1) {
+ type = GEN_IPADD;
+ addrlen = 4;
+ }
+ else if (inet_pton(AF_INET6, name, &addrbuf) == 1) {
+ type = GEN_IPADD;
+ addrlen = 16;
+ }
+ else {
+ type = GEN_DNS;
+ addrlen = 0;
+ }
+
+ count = sk_GENERAL_NAME_num(altname_stack);
+
+ for (i = 0; i < count; i++) {
+ GENERAL_NAME *altname;
+
+ altname = sk_GENERAL_NAME_value(altname_stack, i);
+
+ if (altname->type != type) {
+ continue;
+ }
+
+ if (type == GEN_DNS) {
+ const char *data;
+ int format, len;
+
+ format = ASN1_STRING_type(altname->d.dNSName);
+
+ if (format == V_ASN1_IA5STRING) {
+ data = (const char *) ASN1_STRING_data(altname->d.dNSName);
+ len = ASN1_STRING_length(altname->d.dNSName);
+
+ if (len < 0 || len != (gint) strlen(data)) {
+ ret = FALSE;
+ break;
+ }
+
+ /*
+ * Per RFC 5280 section 4.2.1.6:
+ * " " is a legal domain name, but that
+ * dNSName must be rejected.
+ */
+ if (strcmp(data, " ") == 0) {
+ ret = FALSE;
+ break;
+ }
+
+ if (rspamd_tls_match_name(data, name)) {
+ ret = TRUE;
+ break;
+ }
+ }
+ }
+ else if (type == GEN_IPADD) {
+ const char *data;
+ int datalen;
+
+ datalen = ASN1_STRING_length(altname->d.iPAddress);
+ data = (const char *) ASN1_STRING_data(altname->d.iPAddress);
+
+ if (datalen < 0) {
+ ret = FALSE;
+ break;
+ }
+
+ /*
+ * Per RFC 5280 section 4.2.1.6:
+ * IPv4 must use 4 octets and IPv6 must use 16 octets.
+ */
+ if (datalen == addrlen && memcmp(data, &addrbuf, addrlen) == 0) {
+ ret = TRUE;
+ break;
+ }
+ }
+ }
+
+ sk_GENERAL_NAME_pop_free(altname_stack, GENERAL_NAME_free);
+ return ret;
+}
+
+static gboolean
+rspamd_tls_check_common_name(X509 *cert, const char *name)
+{
+ X509_NAME *subject_name;
+ char *common_name = NULL;
+ union {
+ struct in_addr ip4;
+ struct in6_addr ip6;
+ } addrbuf;
+ int common_name_len;
+ gboolean ret = FALSE;
+
+ subject_name = X509_get_subject_name(cert);
+ if (subject_name == NULL) {
+ goto out;
+ }
+
+ common_name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName, NULL, 0);
+
+ if (common_name_len < 0) {
+ goto out;
+ }
+
+ common_name = g_malloc0(common_name_len + 1);
+ X509_NAME_get_text_by_NID(subject_name, NID_commonName, common_name,
+ common_name_len + 1);
+
+ /* NUL bytes in CN? */
+ if (common_name_len != (gint) strlen(common_name)) {
+ goto out;
+ }
+
+ if (inet_pton(AF_INET, name, &addrbuf) == 1 || inet_pton(AF_INET6, name, &addrbuf) == 1) {
+ /*
+ * We don't want to attempt wildcard matching against IP
+ * addresses, so perform a simple comparison here.
+ */
+ if (strcmp(common_name, name) == 0) {
+ ret = TRUE;
+ }
+ else {
+ ret = FALSE;
+ }
+
+ goto out;
+ }
+
+ if (rspamd_tls_match_name(common_name, name)) {
+ ret = TRUE;
+ }
+
+out:
+ g_free(common_name);
+
+ return ret;
+}
+
+static gboolean
+rspamd_tls_check_name(X509 *cert, const char *name)
+{
+ gboolean ret;
+
+ ret = rspamd_tls_check_subject_altname(cert, name);
+ if (ret) {
+ return ret;
+ }
+
+ return rspamd_tls_check_common_name(cert, name);
+}
+
+static gboolean
+rspamd_ssl_peer_verify(struct rspamd_ssl_connection *c)
+{
+ X509 *server_cert;
+ glong ver_err;
+ GError *err = NULL;
+
+ ver_err = SSL_get_verify_result(c->ssl);
+
+ if (ver_err != X509_V_OK) {
+ g_set_error(&err, rspamd_ssl_quark(), 400, "certificate validation "
+ "failed: %s",
+ X509_verify_cert_error_string(ver_err));
+ c->err_handler(c->handler_data, err);
+ g_error_free(err);
+
+ return FALSE;
+ }
+
+ /* Get server's certificate */
+ server_cert = SSL_get_peer_certificate(c->ssl);
+ if (server_cert == NULL) {
+ g_set_error(&err, rspamd_ssl_quark(), 401, "peer certificate is absent");
+ c->err_handler(c->handler_data, err);
+ g_error_free(err);
+
+ return FALSE;
+ }
+
+ if (c->hostname) {
+ if (!rspamd_tls_check_name(server_cert, c->hostname)) {
+ X509_free(server_cert);
+ g_set_error(&err, rspamd_ssl_quark(), 403, "peer certificate fails "
+ "hostname verification for %s",
+ c->hostname);
+ c->err_handler(c->handler_data, err);
+ g_error_free(err);
+
+ return FALSE;
+ }
+ }
+
+ X509_free(server_cert);
+
+ return TRUE;
+}
+
+static void
+rspamd_tls_set_error(gint retcode, const gchar *stage, GError **err)
+{
+ GString *reason;
+ gchar buf[120];
+ gint err_code = 0;
+
+ reason = g_string_sized_new(sizeof(buf));
+
+ if (retcode == SSL_ERROR_SYSCALL) {
+ rspamd_printf_gstring(reason, "syscall fail: %s", strerror(errno));
+ err_code = 500;
+ }
+ else {
+ while ((err_code = ERR_get_error()) != 0) {
+ ERR_error_string(err_code, buf);
+ rspamd_printf_gstring(reason, "ssl error: %s,", buf);
+ }
+
+ err_code = 400;
+
+ if (reason->len > 0 && reason->str[reason->len - 1] == ',') {
+ reason->str[reason->len - 1] = '\0';
+ reason->len--;
+ }
+ }
+
+ g_set_error(err, rspamd_ssl_quark(), err_code,
+ "ssl %s error: %s", stage, reason->str);
+ g_string_free(reason, TRUE);
+}
+
+static void
+rspamd_ssl_connection_dtor(struct rspamd_ssl_connection *conn)
+{
+ msg_debug_ssl("closing SSL connection %p; %d sessions in the cache",
+ conn->ssl, rspamd_lru_hash_size(conn->ssl_ctx->sessions));
+ SSL_free(conn->ssl);
+
+ if (conn->hostname) {
+ g_free(conn->hostname);
+ }
+
+ /*
+ * Try to workaround for the race between timeout and ssl error
+ */
+ if (conn->shut_ev != conn->ev && ev_can_stop(&conn->ev->tm)) {
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ }
+
+ if (conn->shut_ev) {
+ rspamd_ev_watcher_stop(conn->event_loop, conn->shut_ev);
+ g_free(conn->shut_ev);
+ }
+
+ close(conn->fd);
+ g_free(conn);
+}
+
+static void
+rspamd_ssl_shutdown(struct rspamd_ssl_connection *conn)
+{
+ gint ret = 0, nret, retries;
+ static const gint max_retries = 5;
+
+ /*
+ * Fucking openssl...
+ * From the manual, 0 means: "The shutdown is not yet finished.
+ * Call SSL_shutdown() for a second time,
+ * if a bidirectional shutdown shall be performed.
+ * The output of SSL_get_error(3) may be misleading,
+ * as an erroneous SSL_ERROR_SYSCALL may be flagged
+ * even though no error occurred."
+ *
+ * What is `second`, what if `second` also returns 0?
+ * What a retarded behaviour!
+ */
+ for (retries = 0; retries < max_retries; retries++) {
+ ret = SSL_shutdown(conn->ssl);
+
+ if (ret != 0) {
+ break;
+ }
+ }
+
+ if (ret == 1) {
+ /* All done */
+ msg_debug_ssl("ssl shutdown: all done");
+ rspamd_ssl_connection_dtor(conn);
+ }
+ else if (ret < 0) {
+ short what;
+
+ nret = SSL_get_error(conn->ssl, ret);
+ conn->state = ssl_next_shutdown;
+
+ if (nret == SSL_ERROR_WANT_READ) {
+ msg_debug_ssl("ssl shutdown: need read");
+ what = EV_READ;
+ }
+ else if (nret == SSL_ERROR_WANT_WRITE) {
+ msg_debug_ssl("ssl shutdown: need write");
+ what = EV_WRITE;
+ }
+ else {
+ /* Cannot do anything else, fatal error */
+ GError *err = NULL;
+
+ rspamd_tls_set_error(nret, "final shutdown", &err);
+ msg_debug_ssl("ssl shutdown: fatal error: %e; retries=%d; ret=%d",
+ err, retries, ret);
+ g_error_free(err);
+ rspamd_ssl_connection_dtor(conn);
+
+ return;
+ }
+
+ /* As we own fd, we can try to perform shutdown one more time */
+ /* BUGON: but we DO NOT own conn->ev, and it's a big issue */
+ static const ev_tstamp shutdown_time = 5.0;
+
+ if (conn->shut_ev == NULL) {
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ conn->shut_ev = g_malloc0(sizeof(*conn->shut_ev));
+ rspamd_ev_watcher_init(conn->shut_ev, conn->fd, what,
+ rspamd_ssl_event_handler, conn);
+ rspamd_ev_watcher_start(conn->event_loop, conn->shut_ev, shutdown_time);
+ /* XXX: can it be done safely ? */
+ conn->ev = conn->shut_ev;
+ }
+ else {
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->shut_ev, what);
+ }
+
+ conn->state = ssl_next_shutdown;
+ }
+ else if (ret == 0) {
+ /* What can we do here?? */
+ msg_debug_ssl("ssl shutdown: openssl failed to initiate shutdown after "
+ "%d attempts!",
+ max_retries);
+ rspamd_ssl_connection_dtor(conn);
+ }
+}
+
+static void
+rspamd_ssl_event_handler(gint fd, short what, gpointer ud)
+{
+ struct rspamd_ssl_connection *conn = ud;
+ gint ret;
+ GError *err = NULL;
+
+ if (what == EV_TIMER) {
+ if (conn->state == ssl_next_shutdown) {
+ /* No way to restore, just terminate */
+ rspamd_ssl_connection_dtor(conn);
+ }
+ else {
+ conn->shut = ssl_shut_unclean;
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ g_set_error(&err, rspamd_ssl_quark(), 408,
+ "ssl connection timed out");
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ }
+
+ return;
+ }
+
+ msg_debug_ssl("ssl event; what=%d; c->state=%d", (int) what,
+ (int) conn->state);
+
+ switch (conn->state) {
+ case ssl_conn_init:
+ /* Continue connection */
+ ret = SSL_connect(conn->ssl);
+
+ if (ret == 1) {
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ /* Verify certificate */
+ if ((!conn->verify_peer) || rspamd_ssl_peer_verify(conn)) {
+ msg_debug_ssl("ssl connect: connected");
+ conn->state = ssl_conn_connected;
+ conn->handler(fd, EV_WRITE, conn->handler_data);
+ }
+ else {
+ return;
+ }
+ }
+ else {
+ ret = SSL_get_error(conn->ssl, ret);
+
+ if (ret == SSL_ERROR_WANT_READ) {
+ msg_debug_ssl("ssl connect: need read");
+ what = EV_READ;
+ }
+ else if (ret == SSL_ERROR_WANT_WRITE) {
+ msg_debug_ssl("ssl connect: need write");
+ what = EV_WRITE;
+ }
+ else {
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ rspamd_tls_set_error(ret, "connect", &err);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ return;
+ }
+
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->ev, what);
+ }
+ break;
+ case ssl_next_read:
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->ev, EV_READ);
+ conn->state = ssl_conn_connected;
+ conn->handler(fd, EV_READ, conn->handler_data);
+ break;
+ case ssl_next_write:
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->ev, EV_WRITE);
+ conn->state = ssl_conn_connected;
+ conn->handler(fd, EV_WRITE, conn->handler_data);
+ break;
+ case ssl_conn_connected:
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->ev, what);
+ conn->state = ssl_conn_connected;
+ conn->handler(fd, what, conn->handler_data);
+ break;
+ case ssl_next_shutdown:
+ rspamd_ssl_shutdown(conn);
+ break;
+ default:
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ g_set_error(&err, rspamd_ssl_quark(), 500,
+ "ssl bad state error: %d", conn->state);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ break;
+ }
+}
+
+struct rspamd_ssl_connection *
+rspamd_ssl_connection_new(gpointer ssl_ctx, struct ev_loop *ev_base,
+ gboolean verify_peer, const gchar *log_tag)
+{
+ struct rspamd_ssl_connection *conn;
+ struct rspamd_ssl_ctx *ctx = (struct rspamd_ssl_ctx *) ssl_ctx;
+
+ g_assert(ssl_ctx != NULL);
+ conn = g_malloc0(sizeof(*conn));
+ conn->ssl_ctx = ctx;
+ conn->event_loop = ev_base;
+ conn->verify_peer = verify_peer;
+
+ if (log_tag) {
+ rspamd_strlcpy(conn->log_tag, log_tag, sizeof(conn->log_tag));
+ }
+ else {
+ rspamd_random_hex(conn->log_tag, sizeof(log_tag) - 1);
+ conn->log_tag[sizeof(log_tag) - 1] = '\0';
+ }
+
+ return conn;
+}
+
+
+gboolean
+rspamd_ssl_connect_fd(struct rspamd_ssl_connection *conn, gint fd,
+ const gchar *hostname, struct rspamd_io_ev *ev, ev_tstamp timeout,
+ rspamd_ssl_handler_t handler, rspamd_ssl_error_handler_t err_handler,
+ gpointer handler_data)
+{
+ gint ret;
+ SSL_SESSION *session = NULL;
+
+ g_assert(conn != NULL);
+
+ /* Ensure that we start from the empty SSL errors stack */
+ ERR_clear_error();
+ conn->ssl = SSL_new(conn->ssl_ctx->s);
+
+ if (hostname) {
+ session = rspamd_lru_hash_lookup(conn->ssl_ctx->sessions, hostname,
+ ev_now(conn->event_loop));
+ }
+
+ if (session) {
+ SSL_set_session(conn->ssl, session);
+ }
+
+ SSL_set_app_data(conn->ssl, conn);
+ msg_debug_ssl("new ssl connection %p; session reused=%s",
+ conn->ssl, SSL_session_reused(conn->ssl) ? "true" : "false");
+
+ if (conn->state != ssl_conn_reset) {
+ return FALSE;
+ }
+
+ /* We dup fd to allow graceful closing */
+ gint nfd = dup(fd);
+
+ if (nfd == -1) {
+ return FALSE;
+ }
+
+ conn->fd = nfd;
+ conn->ev = ev;
+ conn->handler = handler;
+ conn->err_handler = err_handler;
+ conn->handler_data = handler_data;
+
+ if (SSL_set_fd(conn->ssl, conn->fd) != 1) {
+ close(conn->fd);
+
+ return FALSE;
+ }
+
+ if (hostname) {
+ conn->hostname = g_strdup(hostname);
+#ifdef HAVE_SSL_TLSEXT_HOSTNAME
+ SSL_set_tlsext_host_name(conn->ssl, conn->hostname);
+#endif
+ }
+
+ conn->state = ssl_conn_init;
+
+ ret = SSL_connect(conn->ssl);
+
+ if (ret == 1) {
+ conn->state = ssl_conn_connected;
+
+ msg_debug_ssl("connected, start write event");
+ rspamd_ev_watcher_stop(conn->event_loop, ev);
+ rspamd_ev_watcher_init(ev, nfd, EV_WRITE, rspamd_ssl_event_handler, conn);
+ rspamd_ev_watcher_start(conn->event_loop, ev, timeout);
+ }
+ else {
+ ret = SSL_get_error(conn->ssl, ret);
+
+ if (ret == SSL_ERROR_WANT_READ) {
+ msg_debug_ssl("not connected, want read");
+ }
+ else if (ret == SSL_ERROR_WANT_WRITE) {
+ msg_debug_ssl("not connected, want write");
+ }
+ else {
+ GError *err = NULL;
+
+ conn->shut = ssl_shut_unclean;
+ rspamd_tls_set_error(ret, "initial connect", &err);
+ msg_debug_ssl("not connected, fatal error %e", err);
+ g_error_free(err);
+
+
+ return FALSE;
+ }
+
+ rspamd_ev_watcher_stop(conn->event_loop, ev);
+ rspamd_ev_watcher_init(ev, nfd, EV_WRITE | EV_READ,
+ rspamd_ssl_event_handler, conn);
+ rspamd_ev_watcher_start(conn->event_loop, ev, timeout);
+ }
+
+ return TRUE;
+}
+
+void rspamd_ssl_connection_restore_handlers(struct rspamd_ssl_connection *conn,
+ rspamd_ssl_handler_t handler,
+ rspamd_ssl_error_handler_t err_handler,
+ gpointer handler_data,
+ short ev_what)
+{
+ conn->handler = handler;
+ conn->err_handler = err_handler;
+ conn->handler_data = handler_data;
+
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ rspamd_ev_watcher_init(conn->ev, conn->fd, ev_what, rspamd_ssl_event_handler, conn);
+ rspamd_ev_watcher_start(conn->event_loop, conn->ev, conn->ev->timeout);
+}
+
+gssize
+rspamd_ssl_read(struct rspamd_ssl_connection *conn, gpointer buf,
+ gsize buflen)
+{
+ gint ret;
+ short what;
+ GError *err = NULL;
+
+ g_assert(conn != NULL);
+
+ if (conn->state != ssl_conn_connected && conn->state != ssl_next_read) {
+ errno = EINVAL;
+ g_set_error(&err, rspamd_ssl_quark(), 400,
+ "ssl state error: cannot read data");
+ conn->shut = ssl_shut_unclean;
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+
+ return -1;
+ }
+
+ ret = SSL_read(conn->ssl, buf, buflen);
+ msg_debug_ssl("ssl read: %d", ret);
+
+ if (ret > 0) {
+ conn->state = ssl_conn_connected;
+ return ret;
+ }
+ else if (ret == 0) {
+ ret = SSL_get_error(conn->ssl, ret);
+
+ if (ret == SSL_ERROR_ZERO_RETURN || ret == SSL_ERROR_SYSCALL) {
+ conn->state = ssl_conn_reset;
+ return 0;
+ }
+ else {
+ conn->shut = ssl_shut_unclean;
+ rspamd_tls_set_error(ret, "read", &err);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ errno = EINVAL;
+
+ return -1;
+ }
+ }
+ else {
+ ret = SSL_get_error(conn->ssl, ret);
+ conn->state = ssl_next_read;
+ what = 0;
+
+ if (ret == SSL_ERROR_WANT_READ) {
+ msg_debug_ssl("ssl read: need read");
+ what |= EV_READ;
+ }
+ else if (ret == SSL_ERROR_WANT_WRITE) {
+ msg_debug_ssl("ssl read: need write");
+ what |= EV_WRITE;
+ }
+ else {
+ conn->shut = ssl_shut_unclean;
+ rspamd_tls_set_error(ret, "read", &err);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ errno = EINVAL;
+
+ return -1;
+ }
+
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->ev, what);
+ errno = EAGAIN;
+ }
+
+ return -1;
+}
+
+gssize
+rspamd_ssl_write(struct rspamd_ssl_connection *conn, gconstpointer buf,
+ gsize buflen)
+{
+ gint ret;
+ short what;
+ GError *err = NULL;
+
+ g_assert(conn != NULL);
+
+ if (conn->state != ssl_conn_connected && conn->state != ssl_next_write) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ ret = SSL_write(conn->ssl, buf, buflen);
+ msg_debug_ssl("ssl write: ret=%d, buflen=%z", ret, buflen);
+
+ if (ret > 0) {
+ conn->state = ssl_conn_connected;
+ return ret;
+ }
+ else if (ret == 0) {
+ ret = SSL_get_error(conn->ssl, ret);
+
+ if (ret == SSL_ERROR_ZERO_RETURN) {
+ rspamd_tls_set_error(ret, "write", &err);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ errno = ECONNRESET;
+ conn->state = ssl_conn_reset;
+
+ return -1;
+ }
+ else {
+ conn->shut = ssl_shut_unclean;
+ rspamd_tls_set_error(ret, "write", &err);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ errno = EINVAL;
+
+ return -1;
+ }
+ }
+ else {
+ ret = SSL_get_error(conn->ssl, ret);
+ conn->state = ssl_next_write;
+
+ if (ret == SSL_ERROR_WANT_READ) {
+ msg_debug_ssl("ssl write: need read");
+ what = EV_READ;
+ }
+ else if (ret == SSL_ERROR_WANT_WRITE) {
+ msg_debug_ssl("ssl write: need write");
+ what = EV_WRITE;
+ }
+ else {
+ conn->shut = ssl_shut_unclean;
+ rspamd_tls_set_error(ret, "write", &err);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ errno = EINVAL;
+
+ return -1;
+ }
+
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->ev, what);
+ errno = EAGAIN;
+ }
+
+ return -1;
+}
+
+gssize
+rspamd_ssl_writev(struct rspamd_ssl_connection *conn, struct iovec *iov,
+ gsize iovlen)
+{
+ /*
+ * Static is needed to avoid issue:
+ * https://github.com/openssl/openssl/issues/6865
+ */
+ static guchar ssl_buf[16384];
+ guchar *p;
+ struct iovec *cur;
+ gsize i, remain;
+
+ remain = sizeof(ssl_buf);
+ p = ssl_buf;
+
+ for (i = 0; i < iovlen; i++) {
+ cur = &iov[i];
+
+ if (cur->iov_len > 0) {
+ if (remain >= cur->iov_len) {
+ memcpy(p, cur->iov_base, cur->iov_len);
+ p += cur->iov_len;
+ remain -= cur->iov_len;
+ }
+ else {
+ memcpy(p, cur->iov_base, remain);
+ p += remain;
+ remain = 0;
+ break;
+ }
+ }
+ }
+
+ return rspamd_ssl_write(conn, ssl_buf, p - ssl_buf);
+}
+
+/**
+ * Removes connection data
+ * @param conn
+ */
+void rspamd_ssl_connection_free(struct rspamd_ssl_connection *conn)
+{
+ if (conn) {
+ if (conn->shut == ssl_shut_unclean) {
+ /* Ignore return result and close socket */
+ msg_debug_ssl("unclean shutdown");
+ SSL_set_quiet_shutdown(conn->ssl, 1);
+ (void) SSL_shutdown(conn->ssl);
+ rspamd_ssl_connection_dtor(conn);
+ }
+ else {
+ msg_debug_ssl("normal shutdown");
+ rspamd_ssl_shutdown(conn);
+ }
+ }
+}
+
+static int
+rspamd_ssl_new_client_session(SSL *ssl, SSL_SESSION *sess)
+{
+ struct rspamd_ssl_connection *conn;
+
+ conn = SSL_get_app_data(ssl);
+
+ if (conn->hostname) {
+ rspamd_lru_hash_insert(conn->ssl_ctx->sessions,
+ g_strdup(conn->hostname), SSL_get1_session(ssl),
+ ev_now(conn->event_loop), SSL_CTX_get_timeout(conn->ssl_ctx->s));
+ msg_debug_ssl("saved new session for %s: %p", conn->hostname, conn);
+ }
+
+ return 0;
+}
+
+static struct rspamd_ssl_ctx *
+rspamd_init_ssl_ctx_common(void)
+{
+ struct rspamd_ssl_ctx *ret;
+ SSL_CTX *ssl_ctx;
+ gint ssl_options;
+ static const guint client_cache_size = 1024;
+
+ rspamd_openssl_maybe_init();
+
+ ret = g_malloc0(sizeof(*ret));
+ ssl_options = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3;
+ ssl_ctx = SSL_CTX_new(SSLv23_method());
+
+#ifdef SSL_OP_NO_COMPRESSION
+ ssl_options |= SSL_OP_NO_COMPRESSION;
+#elif OPENSSL_VERSION_NUMBER >= 0x00908000L
+ sk_SSL_COMP_zero(SSL_COMP_get_compression_methods());
+#endif
+
+ SSL_CTX_set_options(ssl_ctx, ssl_options);
+
+#ifdef TLS1_3_VERSION
+ SSL_CTX_set_min_proto_version(ssl_ctx, 0);
+ SSL_CTX_set_max_proto_version(ssl_ctx, TLS1_3_VERSION);
+#endif
+
+#ifdef SSL_SESS_CACHE_CLIENT
+ SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL_STORE);
+#endif
+
+ ret->s = ssl_ctx;
+ ret->sessions = rspamd_lru_hash_new_full(client_cache_size,
+ g_free, (GDestroyNotify) SSL_SESSION_free, rspamd_str_hash,
+ rspamd_str_equal);
+ SSL_CTX_set_app_data(ssl_ctx, ret);
+ SSL_CTX_sess_set_new_cb(ssl_ctx, rspamd_ssl_new_client_session);
+
+ return ret;
+}
+
+gpointer
+rspamd_init_ssl_ctx(void)
+{
+ struct rspamd_ssl_ctx *ssl_ctx = rspamd_init_ssl_ctx_common();
+
+ SSL_CTX_set_verify(ssl_ctx->s, SSL_VERIFY_PEER, NULL);
+ SSL_CTX_set_verify_depth(ssl_ctx->s, 4);
+
+ return ssl_ctx;
+}
+
+gpointer rspamd_init_ssl_ctx_noverify(void)
+{
+ struct rspamd_ssl_ctx *ssl_ctx_noverify = rspamd_init_ssl_ctx_common();
+
+ SSL_CTX_set_verify(ssl_ctx_noverify->s, SSL_VERIFY_NONE, NULL);
+
+ return ssl_ctx_noverify;
+}
+
+void rspamd_openssl_maybe_init(void)
+{
+ static gboolean openssl_initialized = FALSE;
+
+ if (!openssl_initialized) {
+ ERR_load_crypto_strings();
+ SSL_load_error_strings();
+
+ OpenSSL_add_all_algorithms();
+ OpenSSL_add_all_digests();
+ OpenSSL_add_all_ciphers();
+
+#if OPENSSL_VERSION_NUMBER >= 0x1000104fL && OPENSSL_VERSION_NUMBER < 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
+ ENGINE_load_builtin_engines();
+#endif
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
+ SSL_library_init();
+#else
+ OPENSSL_init_ssl(0, NULL);
+#endif
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
+ OPENSSL_config(NULL);
+#endif
+ if (RAND_status() == 0) {
+ guchar seed[128];
+
+ /* Try to use ottery to seed rand */
+ ottery_rand_bytes(seed, sizeof(seed));
+ RAND_seed(seed, sizeof(seed));
+ rspamd_explicit_memzero(seed, sizeof(seed));
+ }
+
+ openssl_initialized = TRUE;
+ }
+}
+
+void rspamd_ssl_ctx_config(struct rspamd_config *cfg, gpointer ssl_ctx)
+{
+ struct rspamd_ssl_ctx *ctx = (struct rspamd_ssl_ctx *) ssl_ctx;
+ static const char default_secure_ciphers[] = "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4";
+
+ if (cfg->ssl_ca_path) {
+ if (SSL_CTX_load_verify_locations(ctx->s, cfg->ssl_ca_path,
+ NULL) != 1) {
+ msg_err_config("cannot load CA certs from %s: %s",
+ cfg->ssl_ca_path,
+ ERR_error_string(ERR_get_error(), NULL));
+ }
+ }
+ else {
+ msg_debug_config("ssl_ca_path is not set, using default CA path");
+ SSL_CTX_set_default_verify_paths(ctx->s);
+ }
+
+ if (cfg->ssl_ciphers) {
+ if (SSL_CTX_set_cipher_list(ctx->s, cfg->ssl_ciphers) != 1) {
+ msg_err_config(
+ "cannot set ciphers set to %s: %s; fallback to %s",
+ cfg->ssl_ciphers,
+ ERR_error_string(ERR_get_error(), NULL),
+ default_secure_ciphers);
+ /* Default settings */
+ SSL_CTX_set_cipher_list(ctx->s, default_secure_ciphers);
+ }
+ }
+}
+
+void rspamd_ssl_ctx_free(gpointer ssl_ctx)
+{
+ struct rspamd_ssl_ctx *ctx = (struct rspamd_ssl_ctx *) ssl_ctx;
+
+ rspamd_lru_hash_destroy(ctx->sessions);
+ SSL_CTX_free(ctx->s);
+ g_free(ssl_ctx);
+} \ No newline at end of file
diff --git a/src/libserver/ssl_util.h b/src/libserver/ssl_util.h
new file mode 100644
index 0000000..cde7d47
--- /dev/null
+++ b/src/libserver/ssl_util.h
@@ -0,0 +1,120 @@
+/*-
+ * 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 SRC_LIBUTIL_SSL_UTIL_H_
+#define SRC_LIBUTIL_SSL_UTIL_H_
+
+#include "config.h"
+#include "libutil/addr.h"
+#include "libutil/libev_helper.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_ssl_connection;
+
+typedef void (*rspamd_ssl_handler_t)(gint fd, short what, gpointer d);
+
+typedef void (*rspamd_ssl_error_handler_t)(gpointer d, GError *err);
+
+/**
+ * Creates a new ssl connection data structure
+ * @param ssl_ctx initialized SSL_CTX structure
+ * @return opaque connection data
+ */
+struct rspamd_ssl_connection *rspamd_ssl_connection_new(gpointer ssl_ctx,
+ struct ev_loop *ev_base,
+ gboolean verify_peer,
+ const gchar *log_tag);
+
+/**
+ * Connects SSL session using the specified (connected) FD
+ * @param conn connection
+ * @param fd fd to use
+ * @param hostname hostname for SNI
+ * @param ev event to use
+ * @param tv timeout for connection
+ * @param handler connected session handler
+ * @param handler_data opaque data
+ * @return TRUE if a session has been connected
+ */
+gboolean rspamd_ssl_connect_fd(struct rspamd_ssl_connection *conn, gint fd,
+ const gchar *hostname, struct rspamd_io_ev *ev, ev_tstamp timeout,
+ rspamd_ssl_handler_t handler, rspamd_ssl_error_handler_t err_handler,
+ gpointer handler_data);
+
+/**
+ * Restores SSL handlers for the existing ssl connection (e.g. after keepalive)
+ * @param conn
+ * @param handler
+ * @param err_handler
+ * @param handler_data
+ */
+void rspamd_ssl_connection_restore_handlers(struct rspamd_ssl_connection *conn,
+ rspamd_ssl_handler_t handler,
+ rspamd_ssl_error_handler_t err_handler,
+ gpointer handler_data,
+ short ev_what);
+
+/**
+ * Perform async read from SSL socket
+ * @param conn
+ * @param buf
+ * @param buflen
+ * @return
+ */
+gssize rspamd_ssl_read(struct rspamd_ssl_connection *conn, gpointer buf,
+ gsize buflen);
+
+/**
+ * Perform async write to ssl buffer
+ * @param conn
+ * @param buf
+ * @param buflen
+ * @param ev
+ * @param tv
+ * @return
+ */
+gssize rspamd_ssl_write(struct rspamd_ssl_connection *conn, gconstpointer buf,
+ gsize buflen);
+
+/**
+ * Emulate writev by copying iovec to a temporary buffer
+ * @param conn
+ * @param buf
+ * @param buflen
+ * @return
+ */
+gssize rspamd_ssl_writev(struct rspamd_ssl_connection *conn, struct iovec *iov,
+ gsize iovlen);
+
+/**
+ * Removes connection data
+ * @param conn
+ */
+void rspamd_ssl_connection_free(struct rspamd_ssl_connection *conn);
+
+gpointer rspamd_init_ssl_ctx(void);
+gpointer rspamd_init_ssl_ctx_noverify(void);
+void rspamd_ssl_ctx_config(struct rspamd_config *cfg, gpointer ssl_ctx);
+void rspamd_ssl_ctx_free(gpointer ssl_ctx);
+void rspamd_openssl_maybe_init(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SRC_LIBUTIL_SSL_UTIL_H_ */
diff --git a/src/libserver/symcache/symcache_c.cxx b/src/libserver/symcache/symcache_c.cxx
new file mode 100644
index 0000000..6a7e41c
--- /dev/null
+++ b/src/libserver/symcache/symcache_c.cxx
@@ -0,0 +1,715 @@
+/*
+ * 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 "symcache_internal.hxx"
+#include "symcache_periodic.hxx"
+#include "symcache_item.hxx"
+#include "symcache_runtime.hxx"
+
+/**
+ * C API for symcache
+ */
+
+#define C_API_SYMCACHE(ptr) (reinterpret_cast<rspamd::symcache::symcache *>(ptr))
+#define C_API_SYMCACHE_RUNTIME(ptr) (reinterpret_cast<rspamd::symcache::symcache_runtime *>(ptr))
+#define C_API_SYMCACHE_ITEM(ptr) (reinterpret_cast<rspamd::symcache::cache_item *>(ptr))
+#define C_API_SYMCACHE_DYN_ITEM(ptr) (reinterpret_cast<rspamd::symcache::cache_dynamic_item *>(ptr))
+
+void rspamd_symcache_destroy(struct rspamd_symcache *cache)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ delete real_cache;
+}
+
+struct rspamd_symcache *
+rspamd_symcache_new(struct rspamd_config *cfg)
+{
+ auto *ncache = new rspamd::symcache::symcache(cfg);
+
+ return (struct rspamd_symcache *) ncache;
+}
+
+gboolean
+rspamd_symcache_init(struct rspamd_symcache *cache)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ return real_cache->init();
+}
+
+void rspamd_symcache_save(struct rspamd_symcache *cache)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ real_cache->save_items();
+}
+
+gint rspamd_symcache_add_symbol(struct rspamd_symcache *cache,
+ const gchar *name,
+ gint priority,
+ symbol_func_t func,
+ gpointer user_data,
+ int type,
+ gint parent)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ /* Legacy stuff */
+ if (name == nullptr) {
+ name = "";
+ }
+
+ if (parent == -1) {
+ return real_cache->add_symbol_with_callback(name, priority, func, user_data, type);
+ }
+ else {
+ return real_cache->add_virtual_symbol(name, parent, type);
+ }
+}
+
+bool rspamd_symcache_add_symbol_augmentation(struct rspamd_symcache *cache,
+ int sym_id,
+ const char *augmentation,
+ const char *value)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+ auto log_tag = [&]() { return real_cache->log_tag(); };
+
+ if (augmentation == nullptr) {
+ msg_err_cache("null augmentation is not allowed for item %d", sym_id);
+ return false;
+ }
+
+
+ auto *item = real_cache->get_item_by_id_mut(sym_id, false);
+
+ if (item == nullptr) {
+ msg_err_cache("item %d is not found", sym_id);
+ return false;
+ }
+
+ /* Handle empty or absent strings equally */
+ if (value == nullptr || value[0] == '\0') {
+ return item->add_augmentation(*real_cache, augmentation, std::nullopt);
+ }
+
+ return item->add_augmentation(*real_cache, augmentation, value);
+}
+
+void rspamd_symcache_set_peak_callback(struct rspamd_symcache *cache, gint cbref)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ real_cache->set_peak_cb(cbref);
+}
+
+gboolean
+rspamd_symcache_add_condition_delayed(struct rspamd_symcache *cache,
+ const gchar *sym, lua_State *L, gint cbref)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ real_cache->add_delayed_condition(sym, cbref);
+
+ return TRUE;
+}
+
+gint rspamd_symcache_find_symbol(struct rspamd_symcache *cache,
+ const gchar *name)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ /* Legacy stuff but used */
+ if (name == nullptr) {
+ return -1;
+ }
+
+ auto sym_maybe = real_cache->get_item_by_name(name, false);
+
+ if (sym_maybe != nullptr) {
+ return sym_maybe->id;
+ }
+
+ return -1;
+}
+
+gboolean
+rspamd_symcache_stat_symbol(struct rspamd_symcache *cache,
+ const gchar *name,
+ gdouble *frequency,
+ gdouble *freq_stddev,
+ gdouble *tm,
+ guint *nhits)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ auto sym_maybe = real_cache->get_item_by_name(name, false);
+
+ if (sym_maybe != nullptr) {
+ *frequency = sym_maybe->st->avg_frequency;
+ *freq_stddev = sqrt(sym_maybe->st->stddev_frequency);
+ *tm = sym_maybe->st->time_counter.mean;
+
+ if (nhits) {
+ *nhits = sym_maybe->st->hits;
+ }
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+
+guint rspamd_symcache_stats_symbols_count(struct rspamd_symcache *cache)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+ return real_cache->get_stats_symbols_count();
+}
+
+guint64
+rspamd_symcache_get_cksum(struct rspamd_symcache *cache)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+ return real_cache->get_cksum();
+}
+
+gboolean
+rspamd_symcache_validate(struct rspamd_symcache *cache,
+ struct rspamd_config *cfg,
+ gboolean strict)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ return real_cache->validate(strict);
+}
+
+ucl_object_t *
+rspamd_symcache_counters(struct rspamd_symcache *cache)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+ return real_cache->counters();
+}
+
+void *
+rspamd_symcache_start_refresh(struct rspamd_symcache *cache,
+ struct ev_loop *ev_base, struct rspamd_worker *w)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+ return new rspamd::symcache::cache_refresh_cbdata{real_cache, ev_base, w};
+}
+
+void rspamd_symcache_inc_frequency(struct rspamd_symcache *cache, struct rspamd_symcache_item *item,
+ const char *sym_name)
+{
+ auto *real_item = C_API_SYMCACHE_ITEM(item);
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ if (real_item) {
+ real_item->inc_frequency(sym_name, *real_cache);
+ }
+}
+
+void rspamd_symcache_add_delayed_dependency(struct rspamd_symcache *cache,
+ const gchar *from, const gchar *to)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+ real_cache->add_delayed_dependency(from, to);
+}
+
+const gchar *
+rspamd_symcache_get_parent(struct rspamd_symcache *cache,
+ const gchar *symbol)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ auto *sym = real_cache->get_item_by_name(symbol, false);
+
+ if (sym && sym->is_virtual()) {
+ auto *parent = sym->get_parent(*real_cache);
+
+ if (parent) {
+ return parent->get_name().c_str();
+ }
+ }
+
+ return nullptr;
+}
+
+const gchar *
+rspamd_symcache_item_name(struct rspamd_symcache_item *item)
+{
+ auto *real_item = C_API_SYMCACHE_ITEM(item);
+
+ if (real_item == nullptr) {
+ return nullptr;
+ }
+
+ return real_item->get_name().c_str();
+}
+
+gint rspamd_symcache_item_flags(struct rspamd_symcache_item *item)
+{
+ auto *real_item = C_API_SYMCACHE_ITEM(item);
+
+ if (real_item == nullptr) {
+ return 0;
+ }
+
+ return real_item->get_flags();
+}
+
+
+const gchar *
+rspamd_symcache_dyn_item_name(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *dyn_item)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ auto *real_dyn_item = C_API_SYMCACHE_DYN_ITEM(dyn_item);
+
+ if (cache_runtime == nullptr || real_dyn_item == nullptr) {
+ return nullptr;
+ }
+
+ auto static_item = cache_runtime->get_item_by_dynamic_item(real_dyn_item);
+
+ return static_item->get_name().c_str();
+}
+
+gint rspamd_symcache_item_flags(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *dyn_item)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ auto *real_dyn_item = C_API_SYMCACHE_DYN_ITEM(dyn_item);
+
+ if (cache_runtime == nullptr || real_dyn_item == nullptr) {
+ return 0;
+ }
+
+ auto static_item = cache_runtime->get_item_by_dynamic_item(real_dyn_item);
+
+ return static_item->get_flags();
+}
+
+guint rspamd_symcache_get_symbol_flags(struct rspamd_symcache *cache,
+ const gchar *symbol)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ auto *sym = real_cache->get_item_by_name(symbol, false);
+
+ if (sym) {
+ return sym->get_flags();
+ }
+
+ return 0;
+}
+
+const struct rspamd_symcache_item_stat *
+rspamd_symcache_item_stat(struct rspamd_symcache_item *item)
+{
+ auto *real_item = C_API_SYMCACHE_ITEM(item);
+ return real_item->st;
+}
+
+void rspamd_symcache_get_symbol_details(struct rspamd_symcache *cache,
+ const gchar *symbol,
+ ucl_object_t *this_sym_ucl)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ auto *sym = real_cache->get_item_by_name(symbol, false);
+
+ if (sym) {
+ ucl_object_insert_key(this_sym_ucl,
+ ucl_object_fromstring(sym->get_type_str()),
+ "type", strlen("type"), false);
+ }
+}
+
+void rspamd_symcache_foreach(struct rspamd_symcache *cache,
+ void (*func)(struct rspamd_symcache_item *item, gpointer /* userdata */),
+ gpointer ud)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ real_cache->symbols_foreach([&](const rspamd::symcache::cache_item *item) {
+ func((struct rspamd_symcache_item *) item, ud);
+ });
+}
+
+void rspamd_symcache_process_settings_elt(struct rspamd_symcache *cache,
+ struct rspamd_config_settings_elt *elt)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ real_cache->process_settings_elt(elt);
+}
+
+bool rspamd_symcache_set_allowed_settings_ids(struct rspamd_symcache *cache,
+ const gchar *symbol,
+ const guint32 *ids,
+ guint nids)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ auto *item = real_cache->get_item_by_name_mut(symbol, false);
+
+ if (item == nullptr) {
+ return false;
+ }
+
+ item->allowed_ids.set_ids(ids, nids);
+ return true;
+}
+
+bool rspamd_symcache_set_forbidden_settings_ids(struct rspamd_symcache *cache,
+ const gchar *symbol,
+ const guint32 *ids,
+ guint nids)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ auto *item = real_cache->get_item_by_name_mut(symbol, false);
+
+ if (item == nullptr) {
+ return false;
+ }
+
+ item->forbidden_ids.set_ids(ids, nids);
+ return true;
+}
+
+const guint32 *
+rspamd_symcache_get_allowed_settings_ids(struct rspamd_symcache *cache,
+ const gchar *symbol,
+ guint *nids)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ const auto *item = real_cache->get_item_by_name(symbol, false);
+ return item->allowed_ids.get_ids(*nids);
+}
+
+const guint32 *
+rspamd_symcache_get_forbidden_settings_ids(struct rspamd_symcache *cache,
+ const gchar *symbol,
+ guint *nids)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ const auto *item = real_cache->get_item_by_name(symbol, false);
+ return item->forbidden_ids.get_ids(*nids);
+}
+
+void rspamd_symcache_disable_all_symbols(struct rspamd_task *task,
+ struct rspamd_symcache *_cache,
+ guint skip_mask)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+
+ cache_runtime->disable_all_symbols(skip_mask);
+}
+
+gboolean
+rspamd_symcache_disable_symbol(struct rspamd_task *task,
+ struct rspamd_symcache *cache,
+ const gchar *symbol)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ if (cache_runtime == nullptr) {
+ return FALSE;
+ }
+
+ return cache_runtime->disable_symbol(task, *real_cache, symbol);
+}
+
+gboolean
+rspamd_symcache_enable_symbol(struct rspamd_task *task,
+ struct rspamd_symcache *cache,
+ const gchar *symbol)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ if (cache_runtime == nullptr) {
+ return FALSE;
+ }
+
+ return cache_runtime->enable_symbol(task, *real_cache, symbol);
+}
+
+void rspamd_symcache_disable_symbol_static(struct rspamd_symcache *cache,
+ const gchar *symbol)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ real_cache->disable_symbol_delayed(symbol);
+}
+
+void rspamd_symcache_enable_symbol_static(struct rspamd_symcache *cache,
+ const gchar *symbol)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ real_cache->enable_symbol_delayed(symbol);
+}
+
+/* A real structure to match C results without extra copying */
+struct rspamd_symcache_real_timeout_result {
+ struct rspamd_symcache_timeout_result c_api_result;
+ std::vector<std::pair<double, const rspamd::symcache::cache_item *>> elts;
+};
+
+struct rspamd_symcache_timeout_result *
+rspamd_symcache_get_max_timeout(struct rspamd_symcache *cache)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+ auto *res = new rspamd_symcache_real_timeout_result;
+
+ res->c_api_result.max_timeout = real_cache->get_max_timeout(res->elts);
+ res->c_api_result.items = reinterpret_cast<struct rspamd_symcache_timeout_item *>(res->elts.data());
+ res->c_api_result.nitems = res->elts.size();
+
+ return &res->c_api_result;
+}
+
+void rspamd_symcache_timeout_result_free(struct rspamd_symcache_timeout_result *res)
+{
+ auto *real_result = reinterpret_cast<rspamd_symcache_real_timeout_result *>(res);
+ delete real_result;
+}
+
+gboolean
+rspamd_symcache_is_checked(struct rspamd_task *task,
+ struct rspamd_symcache *cache,
+ const gchar *symbol)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ if (cache_runtime == nullptr) {
+ return FALSE;
+ }
+
+ return cache_runtime->is_symbol_checked(*real_cache, symbol);
+}
+
+gboolean
+rspamd_symcache_process_settings(struct rspamd_task *task,
+ struct rspamd_symcache *cache)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ if (cache_runtime == nullptr) {
+ return FALSE;
+ }
+
+ return cache_runtime->process_settings(task, *real_cache);
+}
+
+gboolean
+rspamd_symcache_is_item_allowed(struct rspamd_task *task,
+ struct rspamd_symcache_item *item,
+ gboolean exec_only)
+{
+ auto *real_item = C_API_SYMCACHE_ITEM(item);
+
+ if (real_item == nullptr) {
+ return TRUE;
+ }
+
+ return real_item->is_allowed(task, exec_only);
+}
+
+gboolean
+rspamd_symcache_is_symbol_enabled(struct rspamd_task *task,
+ struct rspamd_symcache *cache,
+ const gchar *symbol)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ if (!cache_runtime) {
+ return TRUE;
+ }
+
+ return cache_runtime->is_symbol_enabled(task, *real_cache, symbol);
+}
+
+struct rspamd_symcache_dynamic_item *
+rspamd_symcache_get_cur_item(struct rspamd_task *task)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+
+ if (!cache_runtime) {
+ return nullptr;
+ }
+
+ return (struct rspamd_symcache_dynamic_item *) cache_runtime->get_cur_item();
+}
+
+struct rspamd_symcache_dynamic_item *
+rspamd_symcache_set_cur_item(struct rspamd_task *task, struct rspamd_symcache_dynamic_item *item)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ auto *real_dyn_item = C_API_SYMCACHE_DYN_ITEM(item);
+
+ if (!cache_runtime || !real_dyn_item) {
+ return nullptr;
+ }
+
+ return (struct rspamd_symcache_dynamic_item *) cache_runtime->set_cur_item(real_dyn_item);
+}
+
+void rspamd_symcache_enable_profile(struct rspamd_task *task)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ if (!cache_runtime) {
+ return;
+ }
+
+ cache_runtime->set_profile_mode(true);
+}
+
+guint rspamd_symcache_item_async_inc_full(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *item,
+ const gchar *subsystem,
+ const gchar *loc)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ auto *real_dyn_item = C_API_SYMCACHE_DYN_ITEM(item);
+
+ auto *static_item = cache_runtime->get_item_by_dynamic_item(real_dyn_item);
+ msg_debug_cache_task("increase async events counter for %s(%d) = %d + 1; "
+ "subsystem %s (%s)",
+ static_item->symbol.c_str(), static_item->id,
+ real_dyn_item->async_events, subsystem, loc);
+
+ return ++real_dyn_item->async_events;
+}
+
+guint rspamd_symcache_item_async_dec_full(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *item,
+ const gchar *subsystem,
+ const gchar *loc)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ auto *real_dyn_item = C_API_SYMCACHE_DYN_ITEM(item);
+
+ auto *static_item = cache_runtime->get_item_by_dynamic_item(real_dyn_item);
+ msg_debug_cache_task("decrease async events counter for %s(%d) = %d - 1; "
+ "subsystem %s (%s)",
+ static_item->symbol.c_str(), static_item->id,
+ real_dyn_item->async_events, subsystem, loc);
+
+ if (G_UNLIKELY(real_dyn_item->async_events == 0)) {
+ msg_err_cache_task("INTERNAL ERROR: trying decrease async events counter for %s(%d) that is already zero; "
+ "subsystem %s (%s)",
+ static_item->symbol.c_str(), static_item->id,
+ real_dyn_item->async_events, subsystem, loc);
+ g_abort();
+ g_assert_not_reached();
+ }
+
+ return --real_dyn_item->async_events;
+}
+
+gboolean
+rspamd_symcache_item_async_dec_check_full(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *item,
+ const gchar *subsystem,
+ const gchar *loc)
+{
+ if (rspamd_symcache_item_async_dec_full(task, item, subsystem, loc) == 0) {
+ rspamd_symcache_finalize_item(task, item);
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+struct rspamd_abstract_callback_data *
+rspamd_symcache_get_cbdata(struct rspamd_symcache *cache,
+ const gchar *symbol)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ auto *item = real_cache->get_item_by_name(symbol, true);
+
+ if (item) {
+ return (struct rspamd_abstract_callback_data *) item->get_cbdata();
+ }
+
+ return nullptr;
+}
+
+void rspamd_symcache_composites_foreach(struct rspamd_task *task,
+ struct rspamd_symcache *cache,
+ GHFunc func,
+ gpointer fd)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+
+ real_cache->composites_foreach([&](const auto *item) {
+ auto *dyn_item = cache_runtime->get_dynamic_item(item->id);
+
+ if (dyn_item && !dyn_item->started) {
+ auto *old_item = cache_runtime->set_cur_item(dyn_item);
+ func((void *) item->get_name().c_str(), item->get_cbdata(), fd);
+ dyn_item->finished = true;
+ cache_runtime->set_cur_item(old_item);
+ }
+ });
+
+ cache_runtime->set_cur_item(nullptr);
+}
+
+gboolean
+rspamd_symcache_process_symbols(struct rspamd_task *task,
+ struct rspamd_symcache *cache,
+ guint stage)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ if (task->symcache_runtime == nullptr) {
+ task->symcache_runtime = rspamd::symcache::symcache_runtime::create(task, *real_cache);
+ }
+
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ return cache_runtime->process_symbols(task, *real_cache, stage);
+}
+
+void rspamd_symcache_finalize_item(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *item)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ auto *real_dyn_item = C_API_SYMCACHE_DYN_ITEM(item);
+
+ cache_runtime->finalize_item(task, real_dyn_item);
+}
+
+void rspamd_symcache_runtime_destroy(struct rspamd_task *task)
+{
+ auto *cache_runtime = C_API_SYMCACHE_RUNTIME(task->symcache_runtime);
+ cache_runtime->savepoint_dtor();
+} \ No newline at end of file
diff --git a/src/libserver/symcache/symcache_id_list.hxx b/src/libserver/symcache/symcache_id_list.hxx
new file mode 100644
index 0000000..bef4fa9
--- /dev/null
+++ b/src/libserver/symcache/symcache_id_list.hxx
@@ -0,0 +1,95 @@
+/*-
+ * Copyright 2022 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_SYMCACHE_ID_LIST_HXX
+#define RSPAMD_SYMCACHE_ID_LIST_HXX
+#pragma once
+
+#include <cstdint>
+#include <cstring> // for memset
+#include <algorithm>// for sort/bsearch
+
+#include "config.h"
+#include "libutil/mem_pool.h"
+#include "contrib/ankerl/svector.h"
+
+namespace rspamd::symcache {
+/*
+ * This structure is optimised to store ids list:
+ * - If the first element is -1 then use dynamic part, else use static part
+ * There is no std::variant to save space
+ */
+
+constexpr const auto id_capacity = 4;
+constexpr const auto id_sort_threshold = 32;
+
+struct id_list {
+ ankerl::svector<std::uint32_t, id_capacity> data;
+
+ id_list() = default;
+
+ auto reset()
+ {
+ data.clear();
+ }
+
+ /**
+ * Returns ids from a compressed list, accepting a mutable reference for number of elements
+ * @param nids output of the number of elements
+ * @return
+ */
+ auto get_ids(unsigned &nids) const -> const std::uint32_t *
+ {
+ nids = data.size();
+
+ return data.data();
+ }
+
+ auto add_id(std::uint32_t id) -> void
+ {
+ data.push_back(id);
+
+ /* Check sort threshold */
+ if (data.size() > id_sort_threshold) {
+ std::sort(data.begin(), data.end());
+ }
+ }
+
+ auto set_ids(const std::uint32_t *ids, std::size_t nids) -> void
+ {
+ data.resize(nids);
+
+ for (auto &id: data) {
+ id = *ids++;
+ }
+
+ if (data.size() > id_sort_threshold) {
+ std::sort(data.begin(), data.end());
+ }
+ }
+
+ auto check_id(unsigned int id) const -> bool
+ {
+ if (data.size() > id_sort_threshold) {
+ return std::binary_search(data.begin(), data.end(), id);
+ }
+ return std::find(data.begin(), data.end(), id) != data.end();
+ }
+};
+
+}// namespace rspamd::symcache
+
+#endif//RSPAMD_SYMCACHE_ID_LIST_HXX
diff --git a/src/libserver/symcache/symcache_impl.cxx b/src/libserver/symcache/symcache_impl.cxx
new file mode 100644
index 0000000..93675ac
--- /dev/null
+++ b/src/libserver/symcache/symcache_impl.cxx
@@ -0,0 +1,1316 @@
+/*
+ * 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 "lua/lua_common.h"
+#include "symcache_internal.hxx"
+#include "symcache_item.hxx"
+#include "symcache_runtime.hxx"
+#include "unix-std.h"
+#include "libutil/cxx/file_util.hxx"
+#include "libutil/cxx/util.hxx"
+#include "fmt/core.h"
+#include "contrib/t1ha/t1ha.h"
+
+#ifdef __has_include
+#if __has_include(<version>)
+#include <version>
+#endif
+#endif
+#include <cmath>
+
+namespace rspamd::symcache {
+
+INIT_LOG_MODULE_PUBLIC(symcache)
+
+auto symcache::init() -> bool
+{
+ auto res = true;
+ reload_time = cfg->cache_reload_time;
+
+ if (cfg->cache_filename != nullptr) {
+ msg_debug_cache("loading symcache saved data from %s", cfg->cache_filename);
+ load_items();
+ }
+
+ ankerl::unordered_dense::set<int> disabled_ids;
+ /* Process enabled/disabled symbols */
+ for (const auto &[id, it]: items_by_id) {
+ if (disabled_symbols) {
+ /*
+ * Due to the ability to add patterns, this is now O(N^2), but it is done
+ * once on configuration and the amount of static patterns is usually low
+ * The possible optimization is to store non patterns in a different set to check it
+ * quickly. However, it is unlikely that this would be used to something really heavy.
+ */
+ for (const auto &disable_pat: *disabled_symbols) {
+ if (disable_pat.matches(it->get_name())) {
+ msg_debug_cache("symbol %s matches %*s disable pattern", it->get_name().c_str(),
+ (int) disable_pat.to_string_view().size(), disable_pat.to_string_view().data());
+ auto need_disable = true;
+
+ if (enabled_symbols) {
+ for (const auto &enable_pat: *enabled_symbols) {
+ if (enable_pat.matches(it->get_name())) {
+ msg_debug_cache("symbol %s matches %*s enable pattern; skip disabling", it->get_name().c_str(),
+ (int) enable_pat.to_string_view().size(), enable_pat.to_string_view().data());
+ need_disable = false;
+ break;
+ }
+ }
+ }
+
+ if (need_disable) {
+ disabled_ids.insert(it->id);
+
+ if (it->is_virtual()) {
+ auto real_elt = it->get_parent(*this);
+
+ if (real_elt) {
+ disabled_ids.insert(real_elt->id);
+
+ const auto *children = real_elt->get_children();
+ if (children != nullptr) {
+ for (const auto &cld: *children) {
+ msg_debug_cache("symbol %s is a virtual sibling of the disabled symbol %s",
+ cld->get_name().c_str(), it->get_name().c_str());
+ disabled_ids.insert(cld->id);
+ }
+ }
+ }
+ }
+ else {
+ /* Also disable all virtual children of this element */
+ const auto *children = it->get_children();
+
+ if (children != nullptr) {
+ for (const auto &cld: *children) {
+ msg_debug_cache("symbol %s is a virtual child of the disabled symbol %s",
+ cld->get_name().c_str(), it->get_name().c_str());
+ disabled_ids.insert(cld->id);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /* Deal with the delayed dependencies */
+ msg_debug_cache("resolving delayed dependencies: %d in list", (int) delayed_deps->size());
+ for (const auto &delayed_dep: *delayed_deps) {
+ auto virt_item = get_item_by_name(delayed_dep.from, false);
+ auto real_item = get_item_by_name(delayed_dep.from, true);
+
+ if (virt_item == nullptr || real_item == nullptr) {
+ msg_err_cache("cannot register delayed dependency between %s and %s: "
+ "%s is missing",
+ delayed_dep.from.data(),
+ delayed_dep.to.data(), delayed_dep.from.data());
+ }
+ else {
+
+ if (!disabled_ids.contains(real_item->id)) {
+ msg_debug_cache("delayed between %s(%d:%d) -> %s",
+ delayed_dep.from.data(),
+ real_item->id, virt_item->id,
+ delayed_dep.to.data());
+ add_dependency(real_item->id, delayed_dep.to,
+ virt_item != real_item ? virt_item->id : -1);
+ }
+ else {
+ msg_debug_cache("no delayed between %s(%d:%d) -> %s; %s is disabled",
+ delayed_dep.from.data(),
+ real_item->id, virt_item->id,
+ delayed_dep.to.data(),
+ delayed_dep.from.data());
+ }
+ }
+ }
+
+ /* Remove delayed dependencies, as they are no longer needed at this point */
+ delayed_deps.reset();
+
+ /* Physically remove ids that are disabled statically */
+ for (auto id_to_disable: disabled_ids) {
+ /*
+ * This erasure is inefficient, we can swap the last element with the removed id
+ * But in this way, our ids are still sorted by addition
+ */
+
+ /* Preserve refcount here */
+ auto deleted_element_refcount = items_by_id[id_to_disable];
+ items_by_id.erase(id_to_disable);
+ items_by_symbol.erase(deleted_element_refcount->get_name());
+
+ auto &additional_vec = get_item_specific_vector(*deleted_element_refcount);
+#if defined(__cpp_lib_erase_if)
+ std::erase_if(additional_vec, [id_to_disable](cache_item *elt) {
+ return elt->id == id_to_disable;
+ });
+#else
+ auto it = std::remove_if(additional_vec.begin(),
+ additional_vec.end(), [id_to_disable](cache_item *elt) {
+ return elt->id == id_to_disable;
+ });
+ additional_vec.erase(it, additional_vec.end());
+#endif
+
+ /* Refcount is dropped, so the symbol should be freed, ensure that nothing else owns this symbol */
+ g_assert(deleted_element_refcount.use_count() == 1);
+ }
+
+ /* Remove no longer used stuff */
+ enabled_symbols.reset();
+ disabled_symbols.reset();
+
+ /* Deal with the delayed conditions */
+ msg_debug_cache("resolving delayed conditions: %d in list", (int) delayed_conditions->size());
+ for (const auto &delayed_cond: *delayed_conditions) {
+ auto it = get_item_by_name_mut(delayed_cond.sym, true);
+
+ if (it == nullptr) {
+ msg_err_cache(
+ "cannot register delayed condition for %s",
+ delayed_cond.sym.c_str());
+ luaL_unref(delayed_cond.L, LUA_REGISTRYINDEX, delayed_cond.cbref);
+ }
+ else {
+ if (!it->add_condition(delayed_cond.L, delayed_cond.cbref)) {
+ msg_err_cache(
+ "cannot register delayed condition for %s: virtual parent; qed",
+ delayed_cond.sym.c_str());
+ g_abort();
+ }
+
+ msg_debug_cache("added a condition to the symbol %s", it->symbol.c_str());
+ }
+ }
+ delayed_conditions.reset();
+
+ msg_debug_cache("process dependencies");
+ for (const auto &[_id, it]: items_by_id) {
+ it->process_deps(*this);
+ }
+
+ /* Sorting stuff */
+ constexpr auto postfilters_cmp = [](const auto &it1, const auto &it2) -> bool {
+ return it1->priority < it2->priority;
+ };
+ constexpr auto prefilters_cmp = [](const auto &it1, const auto &it2) -> bool {
+ return it1->priority > it2->priority;
+ };
+
+ msg_debug_cache("sorting stuff");
+ std::stable_sort(std::begin(connfilters), std::end(connfilters), prefilters_cmp);
+ std::stable_sort(std::begin(prefilters), std::end(prefilters), prefilters_cmp);
+ std::stable_sort(std::begin(postfilters), std::end(postfilters), postfilters_cmp);
+ std::stable_sort(std::begin(idempotent), std::end(idempotent), postfilters_cmp);
+
+ resort();
+
+ /* Connect metric symbols with symcache symbols */
+ if (cfg->symbols) {
+ msg_debug_cache("connect metrics");
+ g_hash_table_foreach(cfg->symbols,
+ symcache::metric_connect_cb,
+ (void *) this);
+ }
+
+ return res;
+}
+
+auto symcache::load_items() -> bool
+{
+ auto cached_map = util::raii_mmaped_file::mmap_shared(cfg->cache_filename,
+ O_RDONLY, PROT_READ);
+
+ if (!cached_map.has_value()) {
+ if (cached_map.error().category == util::error_category::CRITICAL) {
+ msg_err_cache("%s", cached_map.error().error_message.data());
+ }
+ else {
+ msg_info_cache("%s", cached_map.error().error_message.data());
+ }
+ return false;
+ }
+
+
+ if (cached_map->get_size() < (gint) sizeof(symcache_header)) {
+ msg_info_cache("cannot use file %s, truncated: %z", cfg->cache_filename,
+ errno, strerror(errno));
+ return false;
+ }
+
+ const auto *hdr = (struct symcache_header *) cached_map->get_map();
+
+ if (memcmp(hdr->magic, symcache_magic,
+ sizeof(symcache_magic)) != 0) {
+ msg_info_cache("cannot use file %s, bad magic", cfg->cache_filename);
+
+ return false;
+ }
+
+ auto *parser = ucl_parser_new(0);
+ const auto *p = (const std::uint8_t *) (hdr + 1);
+
+ if (!ucl_parser_add_chunk(parser, p, cached_map->get_size() - sizeof(*hdr))) {
+ msg_info_cache("cannot use file %s, cannot parse: %s", cfg->cache_filename,
+ ucl_parser_get_error(parser));
+ ucl_parser_free(parser);
+
+ return false;
+ }
+
+ auto *top = ucl_parser_get_object(parser);
+ ucl_parser_free(parser);
+
+ if (top == nullptr || ucl_object_type(top) != UCL_OBJECT) {
+ msg_info_cache("cannot use file %s, bad object", cfg->cache_filename);
+ ucl_object_unref(top);
+
+ return false;
+ }
+
+ auto it = ucl_object_iterate_new(top);
+ const ucl_object_t *cur;
+ while ((cur = ucl_object_iterate_safe(it, true)) != nullptr) {
+ auto item_it = items_by_symbol.find(ucl_object_key(cur));
+
+ if (item_it != items_by_symbol.end()) {
+ auto item = item_it->second;
+ /* Copy saved info */
+ /*
+ * XXX: don't save or load weight, it should be obtained from the
+ * metric
+ */
+#if 0
+ elt = ucl_object_lookup (cur, "weight");
+
+ if (elt) {
+ w = ucl_object_todouble (elt);
+ if (w != 0) {
+ item->weight = w;
+ }
+ }
+#endif
+ const auto *elt = ucl_object_lookup(cur, "time");
+ if (elt) {
+ item->st->avg_time = ucl_object_todouble(elt);
+ }
+
+ elt = ucl_object_lookup(cur, "count");
+ if (elt) {
+ item->st->total_hits = ucl_object_toint(elt);
+ item->last_count = item->st->total_hits;
+ }
+
+ elt = ucl_object_lookup(cur, "frequency");
+ if (elt && ucl_object_type(elt) == UCL_OBJECT) {
+ const ucl_object_t *freq_elt;
+
+ freq_elt = ucl_object_lookup(elt, "avg");
+
+ if (freq_elt) {
+ item->st->avg_frequency = ucl_object_todouble(freq_elt);
+ }
+ freq_elt = ucl_object_lookup(elt, "stddev");
+
+ if (freq_elt) {
+ item->st->stddev_frequency = ucl_object_todouble(freq_elt);
+ }
+ }
+
+ if (item->is_virtual() && !item->is_ghost()) {
+ const auto &parent = item->get_parent(*this);
+
+ if (parent) {
+ if (parent->st->weight < item->st->weight) {
+ parent->st->weight = item->st->weight;
+ }
+ }
+ /*
+ * We maintain avg_time for virtual symbols equal to the
+ * parent item avg_time
+ */
+ item->st->avg_time = parent->st->avg_time;
+ }
+
+ total_weight += fabs(item->st->weight);
+ total_hits += item->st->total_hits;
+ }
+ }
+
+ ucl_object_iterate_free(it);
+ ucl_object_unref(top);
+
+ return true;
+}
+
+template<typename T>
+static constexpr auto round_to_hundreds(T x)
+{
+ return (::floor(x) * 100.0) / 100.0;
+}
+
+bool symcache::save_items() const
+{
+ if (cfg->cache_filename == nullptr) {
+ return false;
+ }
+
+ auto file_sink = util::raii_file_sink::create(cfg->cache_filename,
+ O_WRONLY | O_TRUNC, 00644);
+
+ if (!file_sink.has_value()) {
+ if (errno == EEXIST) {
+ /* Some other process is already writing data, give up silently */
+ return false;
+ }
+
+ msg_err_cache("%s", file_sink.error().error_message.data());
+
+ return false;
+ }
+
+ struct symcache_header hdr;
+ memset(&hdr, 0, sizeof(hdr));
+ memcpy(hdr.magic, symcache_magic, sizeof(symcache_magic));
+
+ if (write(file_sink->get_fd(), &hdr, sizeof(hdr)) == -1) {
+ msg_err_cache("cannot write to file %s, error %d, %s", cfg->cache_filename,
+ errno, strerror(errno));
+
+ return false;
+ }
+
+ auto *top = ucl_object_typed_new(UCL_OBJECT);
+
+ for (const auto &it: items_by_symbol) {
+ auto item = it.second;
+ auto elt = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(elt,
+ ucl_object_fromdouble(round_to_hundreds(item->st->weight)),
+ "weight", 0, false);
+ ucl_object_insert_key(elt,
+ ucl_object_fromdouble(round_to_hundreds(item->st->time_counter.mean)),
+ "time", 0, false);
+ ucl_object_insert_key(elt, ucl_object_fromint(item->st->total_hits),
+ "count", 0, false);
+
+ auto *freq = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(freq,
+ ucl_object_fromdouble(round_to_hundreds(item->st->frequency_counter.mean)),
+ "avg", 0, false);
+ ucl_object_insert_key(freq,
+ ucl_object_fromdouble(round_to_hundreds(item->st->frequency_counter.stddev)),
+ "stddev", 0, false);
+ ucl_object_insert_key(elt, freq, "frequency", 0, false);
+
+ ucl_object_insert_key(top, elt, it.first.data(), 0, true);
+ }
+
+ auto fp = fdopen(file_sink->get_fd(), "a");
+ auto *efunc = ucl_object_emit_file_funcs(fp);
+ auto ret = ucl_object_emit_full(top, UCL_EMIT_JSON_COMPACT, efunc, nullptr);
+ ucl_object_emit_funcs_free(efunc);
+ ucl_object_unref(top);
+ fclose(fp);
+
+ return ret;
+}
+
+auto symcache::metric_connect_cb(void *k, void *v, void *ud) -> void
+{
+ auto *cache = (symcache *) ud;
+ const auto *sym = (const char *) k;
+ auto *s = (struct rspamd_symbol *) v;
+ auto weight = *s->weight_ptr;
+ auto *item = cache->get_item_by_name_mut(sym, false);
+
+ if (item) {
+ item->st->weight = weight;
+ s->cache_item = (void *) item;
+ }
+}
+
+
+auto symcache::get_item_by_id(int id, bool resolve_parent) const -> const cache_item *
+{
+ if (id < 0 || id >= items_by_id.size()) {
+ msg_err_cache("internal error: requested item with id %d, when we have just %d items in the cache",
+ id, (int) items_by_id.size());
+ return nullptr;
+ }
+
+ const auto &maybe_item = rspamd::find_map(items_by_id, id);
+
+ if (!maybe_item.has_value()) {
+ msg_err_cache("internal error: requested item with id %d but it is empty; qed",
+ id);
+ return nullptr;
+ }
+
+ const auto &item = maybe_item.value().get();
+
+ if (resolve_parent && item->is_virtual()) {
+ return item->get_parent(*this);
+ }
+
+ return item.get();
+}
+
+auto symcache::get_item_by_id_mut(int id, bool resolve_parent) const -> cache_item *
+{
+ if (id < 0 || id >= items_by_id.size()) {
+ msg_err_cache("internal error: requested item with id %d, when we have just %d items in the cache",
+ id, (int) items_by_id.size());
+ return nullptr;
+ }
+
+ const auto &maybe_item = rspamd::find_map(items_by_id, id);
+
+ if (!maybe_item.has_value()) {
+ msg_err_cache("internal error: requested item with id %d but it is empty; qed",
+ id);
+ return nullptr;
+ }
+
+ const auto &item = maybe_item.value().get();
+
+ if (resolve_parent && item->is_virtual()) {
+ return const_cast<cache_item *>(item->get_parent(*this));
+ }
+
+ return item.get();
+}
+
+auto symcache::get_item_by_name(std::string_view name, bool resolve_parent) const -> const cache_item *
+{
+ auto it = items_by_symbol.find(name);
+
+ if (it == items_by_symbol.end()) {
+ return nullptr;
+ }
+
+ if (resolve_parent && it->second->is_virtual()) {
+ it->second->resolve_parent(*this);
+ return it->second->get_parent(*this);
+ }
+
+ return it->second;
+}
+
+auto symcache::get_item_by_name_mut(std::string_view name, bool resolve_parent) const -> cache_item *
+{
+ auto it = items_by_symbol.find(name);
+
+ if (it == items_by_symbol.end()) {
+ return nullptr;
+ }
+
+ if (resolve_parent && it->second->is_virtual()) {
+ return (cache_item *) it->second->get_parent(*this);
+ }
+
+ return it->second;
+}
+
+auto symcache::add_dependency(int id_from, std::string_view to, int virtual_id_from) -> void
+{
+ g_assert(id_from >= 0 && id_from < (gint) items_by_id.size());
+ const auto &source = items_by_id[id_from];
+ g_assert(source.get() != nullptr);
+
+ source->deps.emplace_back(nullptr,
+ std::string(to),
+ id_from,
+ -1);
+
+
+ if (virtual_id_from >= 0) {
+ g_assert(virtual_id_from < (gint) items_by_id.size());
+ /* We need that for settings id propagation */
+ const auto &vsource = items_by_id[virtual_id_from];
+ g_assert(vsource.get() != nullptr);
+ vsource->deps.emplace_back(nullptr,
+ std::string(to),
+ -1,
+ virtual_id_from);
+ }
+}
+
+auto symcache::resort() -> void
+{
+ auto log_func = RSPAMD_LOG_FUNC;
+ auto ord = std::make_shared<order_generation>(filters.size() +
+ prefilters.size() +
+ composites.size() +
+ postfilters.size() +
+ idempotent.size() +
+ connfilters.size() +
+ classifiers.size(),
+ cur_order_gen);
+
+ for (auto &it: filters) {
+ if (it) {
+ total_hits += it->st->total_hits;
+ /* Unmask topological order */
+ it->order = 0;
+ ord->d.emplace_back(it->getptr());
+ }
+ }
+
+ enum class tsort_mask {
+ PERM,
+ TEMP
+ };
+
+ constexpr auto tsort_unmask = [](cache_item *it) -> auto {
+ return (it->order & ~((1u << 31) | (1u << 30)));
+ };
+
+ /* Recursive topological sort helper */
+ const auto tsort_visit = [&](cache_item *it, unsigned cur_order, auto &&rec) {
+ constexpr auto tsort_mark = [](cache_item *it, tsort_mask how) {
+ switch (how) {
+ case tsort_mask::PERM:
+ it->order |= (1u << 31);
+ break;
+ case tsort_mask::TEMP:
+ it->order |= (1u << 30);
+ break;
+ }
+ };
+ constexpr auto tsort_is_marked = [](cache_item *it, tsort_mask how) {
+ switch (how) {
+ case tsort_mask::PERM:
+ return (it->order & (1u << 31));
+ case tsort_mask::TEMP:
+ return (it->order & (1u << 30));
+ }
+
+ return 100500u; /* Because fuck compilers, that's why */
+ };
+
+ if (tsort_is_marked(it, tsort_mask::PERM)) {
+ if (cur_order > tsort_unmask(it)) {
+ /* Need to recalculate the whole chain */
+ it->order = cur_order; /* That also removes all masking */
+ }
+ else {
+ /* We are fine, stop DFS */
+ return;
+ }
+ }
+ else if (tsort_is_marked(it, tsort_mask::TEMP)) {
+ msg_err_cache_lambda("cyclic dependencies found when checking '%s'!",
+ it->symbol.c_str());
+ return;
+ }
+
+ tsort_mark(it, tsort_mask::TEMP);
+ msg_debug_cache_lambda("visiting node: %s (%d)", it->symbol.c_str(), cur_order);
+
+ for (const auto &dep: it->deps) {
+ msg_debug_cache_lambda("visiting dep: %s (%d)", dep.item->symbol.c_str(), cur_order + 1);
+ rec(dep.item, cur_order + 1, rec);
+ }
+
+ it->order = cur_order;
+ tsort_mark(it, tsort_mask::PERM);
+ };
+ /*
+ * Topological sort
+ */
+ total_hits = 0;
+ auto used_items = ord->d.size();
+
+ for (const auto &it: ord->d) {
+ if (it->order == 0) {
+ tsort_visit(it.get(), 0, tsort_visit);
+ }
+ }
+
+
+ /* Main sorting comparator */
+ constexpr auto score_functor = [](auto w, auto f, auto t) -> auto {
+ auto time_alpha = 1.0, weight_alpha = 0.1, freq_alpha = 0.01;
+
+ return ((w > 0.0 ? w : weight_alpha) * (f > 0.0 ? f : freq_alpha) /
+ (t > time_alpha ? t : time_alpha));
+ };
+
+ auto cache_order_cmp = [&](const auto &it1, const auto &it2) -> auto {
+ constexpr const auto topology_mult = 1e7,
+ priority_mult = 1e6,
+ augmentations1_mult = 1e5;
+ auto w1 = tsort_unmask(it1.get()) * topology_mult,
+ w2 = tsort_unmask(it2.get()) * topology_mult;
+
+ w1 += it1->priority * priority_mult;
+ w2 += it2->priority * priority_mult;
+ w1 += it1->get_augmentation_weight() * augmentations1_mult;
+ w2 += it2->get_augmentation_weight() * augmentations1_mult;
+
+ auto avg_freq = ((double) total_hits / used_items);
+ auto avg_weight = (total_weight / used_items);
+ auto f1 = (double) it1->st->total_hits / avg_freq;
+ auto f2 = (double) it2->st->total_hits / avg_freq;
+ auto weight1 = std::fabs(it1->st->weight) / avg_weight;
+ auto weight2 = std::fabs(it2->st->weight) / avg_weight;
+ auto t1 = it1->st->avg_time;
+ auto t2 = it2->st->avg_time;
+ w1 += score_functor(weight1, f1, t1);
+ w2 += score_functor(weight2, f2, t2);
+
+ return w1 > w2;
+ };
+
+ std::stable_sort(std::begin(ord->d), std::end(ord->d), cache_order_cmp);
+ /*
+ * Here lives some ugly legacy!
+ * We have several filters classes, connfilters, prefilters, filters... etc
+ *
+ * Our order is meaningful merely for filters, but we have to add other classes
+ * to understand if those symbols are checked or disabled.
+ * We can disable symbols for almost everything but not for virtual symbols.
+ * The rule of thumb is that if a symbol has explicit parent, then it is a
+ * virtual symbol that follows it's special rules
+ */
+
+ /*
+ * We enrich ord with all other symbol types without any sorting,
+ * as it is done in another place
+ */
+ constexpr auto append_items_vec = [](const auto &vec, auto &out) {
+ for (const auto &it: vec) {
+ if (it) {
+ out.emplace_back(it->getptr());
+ }
+ }
+ };
+
+ append_items_vec(connfilters, ord->d);
+ append_items_vec(prefilters, ord->d);
+ append_items_vec(postfilters, ord->d);
+ append_items_vec(idempotent, ord->d);
+ append_items_vec(composites, ord->d);
+ append_items_vec(classifiers, ord->d);
+
+ /* After sorting is done, we can assign all elements in the by_symbol hash */
+ for (const auto [i, it]: rspamd::enumerate(ord->d)) {
+ ord->by_symbol.emplace(it->get_name(), i);
+ ord->by_cache_id[it->id] = i;
+ }
+ /* Finally set the current order */
+ std::swap(ord, items_by_order);
+}
+
+auto symcache::add_symbol_with_callback(std::string_view name,
+ int priority,
+ symbol_func_t func,
+ void *user_data,
+ int flags_and_type) -> int
+{
+ auto real_type_pair_maybe = item_type_from_c(flags_and_type);
+
+ if (!real_type_pair_maybe.has_value()) {
+ msg_err_cache("incompatible flags when adding %s: %s", name.data(),
+ real_type_pair_maybe.error().c_str());
+ return -1;
+ }
+
+ auto real_type_pair = real_type_pair_maybe.value();
+
+ if (real_type_pair.first != symcache_item_type::FILTER) {
+ real_type_pair.second |= SYMBOL_TYPE_NOSTAT;
+ }
+ if (real_type_pair.second & (SYMBOL_TYPE_GHOST | SYMBOL_TYPE_CALLBACK)) {
+ real_type_pair.second |= SYMBOL_TYPE_NOSTAT;
+ }
+
+ if (real_type_pair.first == symcache_item_type::VIRTUAL) {
+ msg_err_cache("trying to add virtual symbol %s as real (no parent)", name.data());
+ return -1;
+ }
+
+ std::string static_string_name;
+
+ if (name.empty()) {
+ static_string_name = fmt::format("AUTO_{}_{}", (void *) func, user_data);
+ msg_warn_cache("trying to add an empty symbol name, convert it to %s",
+ static_string_name.c_str());
+ }
+ else {
+ static_string_name = name;
+ }
+
+ if (real_type_pair.first == symcache_item_type::IDEMPOTENT && priority != 0) {
+ msg_warn_cache("priority has been set for idempotent symbol %s: %d",
+ static_string_name.c_str(), priority);
+ }
+
+ if ((real_type_pair.second & SYMBOL_TYPE_FINE) && priority == 0) {
+ /* Adjust priority for negative weighted symbols */
+ priority = 1;
+ }
+
+ if (items_by_symbol.contains(static_string_name)) {
+ msg_err_cache("duplicate symbol name: %s", static_string_name.data());
+ return -1;
+ }
+
+ auto id = items_by_id.size();
+
+ auto item = cache_item::create_with_function(static_pool, id,
+ std::move(static_string_name),
+ priority, func, user_data,
+ real_type_pair.first, real_type_pair.second);
+
+ items_by_symbol.emplace(item->get_name(), item.get());
+ get_item_specific_vector(*item).push_back(item.get());
+ items_by_id.emplace(id, std::move(item));// Takes ownership
+
+ if (!(real_type_pair.second & SYMBOL_TYPE_NOSTAT)) {
+ cksum = t1ha(name.data(), name.size(), cksum);
+ stats_symbols_count++;
+ }
+
+ return id;
+}
+
+auto symcache::add_virtual_symbol(std::string_view name, int parent_id, int flags_and_type) -> int
+{
+ if (name.empty()) {
+ msg_err_cache("cannot register a virtual symbol with no name; qed");
+ return -1;
+ }
+
+ auto real_type_pair_maybe = item_type_from_c(flags_and_type);
+
+ if (!real_type_pair_maybe.has_value()) {
+ msg_err_cache("incompatible flags when adding %s: %s", name.data(),
+ real_type_pair_maybe.error().c_str());
+ return -1;
+ }
+
+ auto real_type_pair = real_type_pair_maybe.value();
+
+ if (items_by_symbol.contains(name)) {
+ msg_err_cache("duplicate symbol name: %s", name.data());
+ return -1;
+ }
+
+ if (items_by_id.size() < parent_id) {
+ msg_err_cache("parent id %d is out of bounds for virtual symbol %s", parent_id, name.data());
+ return -1;
+ }
+
+ auto id = items_by_id.size();
+
+ auto item = cache_item::create_with_virtual(static_pool,
+ id,
+ std::string{name},
+ parent_id, real_type_pair.first, real_type_pair.second);
+ const auto &parent = items_by_id[parent_id].get();
+ parent->add_child(item.get());
+ items_by_symbol.emplace(item->get_name(), item.get());
+ get_item_specific_vector(*item).push_back(item.get());
+ items_by_id.emplace(id, std::move(item));// Takes ownership
+
+ return id;
+}
+
+auto symcache::set_peak_cb(int cbref) -> void
+{
+ if (peak_cb != -1) {
+ luaL_unref(L, LUA_REGISTRYINDEX, peak_cb);
+ }
+
+ peak_cb = cbref;
+ msg_info_cache("registered peak callback");
+}
+
+auto symcache::add_delayed_condition(std::string_view sym, int cbref) -> void
+{
+ delayed_conditions->emplace_back(sym, cbref, (lua_State *) cfg->lua_state);
+}
+
+auto symcache::validate(bool strict) -> bool
+{
+ total_weight = 1.0;
+
+ for (auto &pair: items_by_symbol) {
+ auto &item = pair.second;
+ auto ghost = item->st->weight == 0 ? true : false;
+ auto skipped = !ghost;
+
+ if (item->is_scoreable() && g_hash_table_lookup(cfg->symbols, item->symbol.c_str()) == nullptr) {
+ if (!std::isnan(cfg->unknown_weight)) {
+ item->st->weight = cfg->unknown_weight;
+ auto *s = rspamd_mempool_alloc0_type(static_pool,
+ struct rspamd_symbol);
+ /* Legit as we actually never modify this data */
+ s->name = (char *) item->symbol.c_str();
+ s->weight_ptr = &item->st->weight;
+ g_hash_table_insert(cfg->symbols, (void *) s->name, (void *) s);
+
+ msg_info_cache("adding unknown symbol %s with weight: %.2f",
+ item->symbol.c_str(), cfg->unknown_weight);
+ ghost = false;
+ skipped = false;
+ }
+ else {
+ skipped = true;
+ }
+ }
+ else {
+ skipped = false;
+ }
+
+ if (!ghost && skipped) {
+ if (!(item->flags & SYMBOL_TYPE_SKIPPED)) {
+ item->flags |= SYMBOL_TYPE_SKIPPED;
+ msg_warn_cache("symbol %s has no score registered, skip its check",
+ item->symbol.c_str());
+ }
+ }
+
+ if (ghost) {
+ msg_debug_cache("symbol %s is registered as ghost symbol, it won't be inserted "
+ "to any metric",
+ item->symbol.c_str());
+ }
+
+ if (item->st->weight < 0 && item->priority == 0) {
+ item->priority++;
+ }
+
+ if (item->is_virtual()) {
+ if (!(item->flags & SYMBOL_TYPE_GHOST)) {
+ auto *parent = const_cast<cache_item *>(item->get_parent(*this));
+
+ if (parent == nullptr) {
+ item->resolve_parent(*this);
+ parent = const_cast<cache_item *>(item->get_parent(*this));
+ }
+
+ if (::fabs(parent->st->weight) < ::fabs(item->st->weight)) {
+ parent->st->weight = item->st->weight;
+ }
+
+ auto p1 = ::abs(item->priority);
+ auto p2 = ::abs(parent->priority);
+
+ if (p1 != p2) {
+ parent->priority = MAX(p1, p2);
+ item->priority = parent->priority;
+ }
+ }
+ }
+
+ total_weight += fabs(item->st->weight);
+ }
+
+ /* Now check each metric item and find corresponding symbol in a cache */
+ auto ret = true;
+ GHashTableIter it;
+ void *k, *v;
+ g_hash_table_iter_init(&it, cfg->symbols);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ auto ignore_symbol = false;
+ auto sym_def = (struct rspamd_symbol *) v;
+
+ if (sym_def && (sym_def->flags &
+ (RSPAMD_SYMBOL_FLAG_IGNORE_METRIC | RSPAMD_SYMBOL_FLAG_DISABLED))) {
+ ignore_symbol = true;
+ }
+
+ if (!ignore_symbol) {
+ if (!items_by_symbol.contains((const char *) k)) {
+ msg_debug_cache(
+ "symbol '%s' has its score defined but there is no "
+ "corresponding rule registered",
+ k);
+ }
+ }
+ else if (sym_def->flags & RSPAMD_SYMBOL_FLAG_DISABLED) {
+ auto item = get_item_by_name_mut((const char *) k, false);
+
+ if (item) {
+ item->enabled = FALSE;
+ }
+ }
+ }
+
+ return ret;
+}
+
+auto symcache::counters() const -> ucl_object_t *
+{
+ auto *top = ucl_object_typed_new(UCL_ARRAY);
+ constexpr const auto round_float = [](const auto x, const int digits) -> auto {
+ const auto power10 = ::pow(10, digits);
+ return (::floor(x * power10) / power10);
+ };
+
+ for (auto &pair: items_by_symbol) {
+ auto &item = pair.second;
+ auto symbol = pair.first;
+
+ auto *obj = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(obj, ucl_object_fromlstring(symbol.data(), symbol.size()),
+ "symbol", 0, false);
+
+ if (item->is_virtual()) {
+ if (!(item->flags & SYMBOL_TYPE_GHOST)) {
+ const auto *parent = item->get_parent(*this);
+ ucl_object_insert_key(obj,
+ ucl_object_fromdouble(round_float(item->st->weight, 3)),
+ "weight", 0, false);
+ ucl_object_insert_key(obj,
+ ucl_object_fromdouble(round_float(parent->st->avg_frequency, 3)),
+ "frequency", 0, false);
+ ucl_object_insert_key(obj,
+ ucl_object_fromint(parent->st->total_hits),
+ "hits", 0, false);
+ ucl_object_insert_key(obj,
+ ucl_object_fromdouble(round_float(parent->st->avg_time, 3)),
+ "time", 0, false);
+ }
+ else {
+ ucl_object_insert_key(obj,
+ ucl_object_fromdouble(round_float(item->st->weight, 3)),
+ "weight", 0, false);
+ ucl_object_insert_key(obj,
+ ucl_object_fromdouble(0.0),
+ "frequency", 0, false);
+ ucl_object_insert_key(obj,
+ ucl_object_fromdouble(0.0),
+ "hits", 0, false);
+ ucl_object_insert_key(obj,
+ ucl_object_fromdouble(0.0),
+ "time", 0, false);
+ }
+ }
+ else {
+ ucl_object_insert_key(obj,
+ ucl_object_fromdouble(round_float(item->st->weight, 3)),
+ "weight", 0, false);
+ ucl_object_insert_key(obj,
+ ucl_object_fromdouble(round_float(item->st->avg_frequency, 3)),
+ "frequency", 0, false);
+ ucl_object_insert_key(obj,
+ ucl_object_fromint(item->st->total_hits),
+ "hits", 0, false);
+ ucl_object_insert_key(obj,
+ ucl_object_fromdouble(round_float(item->st->avg_time, 3)),
+ "time", 0, false);
+ }
+
+ ucl_array_append(top, obj);
+ }
+
+ return top;
+}
+
+auto symcache::periodic_resort(struct ev_loop *ev_loop, double cur_time, double last_resort) -> void
+{
+ for (const auto &item: filters) {
+
+ if (item->update_counters_check_peak(L, ev_loop, cur_time, last_resort)) {
+ auto cur_value = (item->st->total_hits - item->last_count) /
+ (cur_time - last_resort);
+ auto cur_err = (item->st->avg_frequency - cur_value);
+ cur_err *= cur_err;
+ msg_debug_cache("peak found for %s is %.2f, avg: %.2f, "
+ "stddev: %.2f, error: %.2f, peaks: %d",
+ item->symbol.c_str(), cur_value,
+ item->st->avg_frequency,
+ item->st->stddev_frequency,
+ cur_err,
+ item->frequency_peaks);
+
+ if (peak_cb != -1) {
+ struct ev_loop **pbase;
+
+ lua_rawgeti(L, LUA_REGISTRYINDEX, peak_cb);
+ pbase = (struct ev_loop **) lua_newuserdata(L, sizeof(*pbase));
+ *pbase = ev_loop;
+ rspamd_lua_setclass(L, "rspamd{ev_base}", -1);
+ lua_pushlstring(L, item->symbol.c_str(), item->symbol.size());
+ lua_pushnumber(L, item->st->avg_frequency);
+ lua_pushnumber(L, ::sqrt(item->st->stddev_frequency));
+ lua_pushnumber(L, cur_value);
+ lua_pushnumber(L, cur_err);
+
+ if (lua_pcall(L, 6, 0, 0) != 0) {
+ msg_info_cache("call to peak function for %s failed: %s",
+ item->symbol.c_str(), lua_tostring(L, -1));
+ lua_pop(L, 1);
+ }
+ }
+ }
+ }
+}
+
+symcache::~symcache()
+{
+ if (peak_cb != -1) {
+ luaL_unref(L, LUA_REGISTRYINDEX, peak_cb);
+ }
+}
+
+auto symcache::maybe_resort() -> bool
+{
+ if (items_by_order->generation_id != cur_order_gen) {
+ /*
+ * Cache has been modified, need to resort it
+ */
+ msg_info_cache("symbols cache has been modified since last check:"
+ " old id: %ud, new id: %ud",
+ items_by_order->generation_id, cur_order_gen);
+ resort();
+
+ return true;
+ }
+
+ return false;
+}
+
+auto symcache::get_item_specific_vector(const cache_item &it) -> symcache::items_ptr_vec &
+{
+ switch (it.get_type()) {
+ case symcache_item_type::CONNFILTER:
+ return connfilters;
+ case symcache_item_type::FILTER:
+ return filters;
+ case symcache_item_type::IDEMPOTENT:
+ return idempotent;
+ case symcache_item_type::PREFILTER:
+ return prefilters;
+ case symcache_item_type::POSTFILTER:
+ return postfilters;
+ case symcache_item_type::COMPOSITE:
+ return composites;
+ case symcache_item_type::CLASSIFIER:
+ return classifiers;
+ case symcache_item_type::VIRTUAL:
+ return virtual_symbols;
+ }
+
+ RSPAMD_UNREACHABLE;
+}
+
+auto symcache::process_settings_elt(struct rspamd_config_settings_elt *elt) -> void
+{
+
+ auto id = elt->id;
+
+ if (elt->symbols_disabled) {
+ /* Process denied symbols */
+ ucl_object_iter_t iter = nullptr;
+ const ucl_object_t *cur;
+
+ while ((cur = ucl_object_iterate(elt->symbols_disabled, &iter, true)) != NULL) {
+ const auto *sym = ucl_object_key(cur);
+ auto *item = get_item_by_name_mut(sym, false);
+
+ if (item != nullptr) {
+ if (item->is_virtual()) {
+ /*
+ * Virtual symbols are special:
+ * we ignore them in symcache but prevent them from being
+ * inserted.
+ */
+ item->forbidden_ids.add_id(id);
+ msg_debug_cache("deny virtual symbol %s for settings %ud (%s); "
+ "parent can still be executed",
+ sym, id, elt->name);
+ }
+ else {
+ /* Normal symbol, disable it */
+ item->forbidden_ids.add_id(id);
+ msg_debug_cache("deny symbol %s for settings %ud (%s)",
+ sym, id, elt->name);
+ }
+ }
+ else {
+ msg_warn_cache("cannot find a symbol to disable %s "
+ "when processing settings %ud (%s)",
+ sym, id, elt->name);
+ }
+ }
+ }
+
+ if (elt->symbols_enabled) {
+ ucl_object_iter_t iter = nullptr;
+ const ucl_object_t *cur;
+
+ while ((cur = ucl_object_iterate(elt->symbols_enabled, &iter, true)) != nullptr) {
+ /* Here, we resolve parent and explicitly allow it */
+ const auto *sym = ucl_object_key(cur);
+
+ auto *item = get_item_by_name_mut(sym, false);
+
+ if (item != nullptr) {
+ if (item->is_virtual()) {
+ auto *parent = get_item_by_name_mut(sym, true);
+
+ if (parent) {
+ if (elt->symbols_disabled &&
+ ucl_object_lookup(elt->symbols_disabled, parent->symbol.data())) {
+ msg_err_cache("conflict in %s: cannot enable disabled symbol %s, "
+ "wanted to enable symbol %s",
+ elt->name, parent->symbol.data(), sym);
+ continue;
+ }
+
+ parent->exec_only_ids.add_id(id);
+ msg_debug_cache("allow just execution of symbol %s for settings %ud (%s)",
+ parent->symbol.data(), id, elt->name);
+ }
+ }
+
+ item->allowed_ids.add_id(id);
+ msg_debug_cache("allow execution of symbol %s for settings %ud (%s)",
+ sym, id, elt->name);
+ }
+ else {
+ msg_warn_cache("cannot find a symbol to enable %s "
+ "when processing settings %ud (%s)",
+ sym, id, elt->name);
+ }
+ }
+ }
+}
+
+auto symcache::get_max_timeout(std::vector<std::pair<double, const cache_item *>> &elts) const -> double
+{
+ auto accumulated_timeout = 0.0;
+ auto log_func = RSPAMD_LOG_FUNC;
+ ankerl::unordered_dense::set<const cache_item *> seen_items;
+
+ auto get_item_timeout = [](cache_item *it) {
+ return it->get_numeric_augmentation("timeout").value_or(0.0);
+ };
+
+ /* This function returns the timeout for an item and all it's dependencies */
+ auto get_filter_timeout = [&](cache_item *it, auto self) -> double {
+ auto own_timeout = get_item_timeout(it);
+ auto max_child_timeout = 0.0;
+
+ for (const auto &dep: it->deps) {
+ auto cld_timeout = self(dep.item, self);
+
+ if (cld_timeout > max_child_timeout) {
+ max_child_timeout = cld_timeout;
+ }
+ }
+
+ return own_timeout + max_child_timeout;
+ };
+
+ /* For prefilters and postfilters, we just care about priorities */
+ auto pre_postfilter_iter = [&](const items_ptr_vec &vec) -> double {
+ auto saved_priority = -1;
+ auto max_timeout = 0.0, added_timeout = 0.0;
+ const cache_item *max_elt = nullptr;
+ for (const auto &it: vec) {
+ if (it->priority != saved_priority && max_elt != nullptr && max_timeout > 0) {
+ if (!seen_items.contains(max_elt)) {
+ accumulated_timeout += max_timeout;
+ added_timeout += max_timeout;
+
+ msg_debug_cache_lambda("added %.2f to the timeout (%.2f) as the priority has changed (%d -> %d); "
+ "symbol: %s",
+ max_timeout, accumulated_timeout, saved_priority, it->priority,
+ max_elt->symbol.c_str());
+ elts.emplace_back(max_timeout, max_elt);
+ seen_items.insert(max_elt);
+ }
+ max_timeout = 0;
+ saved_priority = it->priority;
+ max_elt = nullptr;
+ }
+
+ auto timeout = get_item_timeout(it);
+
+ if (timeout > max_timeout) {
+ max_timeout = timeout;
+ max_elt = it;
+ }
+ }
+
+ if (max_elt != nullptr && max_timeout > 0) {
+ if (!seen_items.contains(max_elt)) {
+ accumulated_timeout += max_timeout;
+ added_timeout += max_timeout;
+
+ msg_debug_cache_lambda("added %.2f to the timeout (%.2f) end of processing; "
+ "symbol: %s",
+ max_timeout, accumulated_timeout,
+ max_elt->symbol.c_str());
+ elts.emplace_back(max_timeout, max_elt);
+ seen_items.insert(max_elt);
+ }
+ }
+
+ return added_timeout;
+ };
+
+ auto prefilters_timeout = pre_postfilter_iter(this->prefilters);
+
+ /* For normal filters, we check the maximum chain of the dependencies
+ * This function might have O(N^2) complexity if all symbols are in a single
+ * dependencies chain. But it is not the case in practice
+ */
+ double max_filters_timeout = 0;
+ for (const auto &it: this->filters) {
+ auto timeout = get_filter_timeout(it, get_filter_timeout);
+
+ if (timeout > max_filters_timeout) {
+ max_filters_timeout = timeout;
+ if (!seen_items.contains(it)) {
+ elts.emplace_back(timeout, it);
+ seen_items.insert(it);
+ }
+ }
+ }
+
+ accumulated_timeout += max_filters_timeout;
+
+ auto postfilters_timeout = pre_postfilter_iter(this->postfilters);
+ auto idempotent_timeout = pre_postfilter_iter(this->idempotent);
+
+ /* Sort in decreasing order by timeout */
+ std::stable_sort(std::begin(elts), std::end(elts),
+ [](const auto &p1, const auto &p2) {
+ return p1.first > p2.first;
+ });
+
+ msg_debug_cache("overall cache timeout: %.2f, %.2f from prefilters,"
+ " %.2f from postfilters, %.2f from idempotent filters,"
+ " %.2f from normal filters",
+ accumulated_timeout, prefilters_timeout, postfilters_timeout,
+ idempotent_timeout, max_filters_timeout);
+
+ return accumulated_timeout;
+}
+
+}// namespace rspamd::symcache \ No newline at end of file
diff --git a/src/libserver/symcache/symcache_internal.hxx b/src/libserver/symcache/symcache_internal.hxx
new file mode 100644
index 0000000..255a4b1
--- /dev/null
+++ b/src/libserver/symcache/symcache_internal.hxx
@@ -0,0 +1,652 @@
+/*
+ * 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.
+ */
+
+/**
+ * Internal C++ structures and classes for symcache
+ */
+
+#ifndef RSPAMD_SYMCACHE_INTERNAL_HXX
+#define RSPAMD_SYMCACHE_INTERNAL_HXX
+#pragma once
+
+#include <cmath>
+#include <cstdlib>
+#include <cstdint>
+#include <utility>
+#include <vector>
+#include <string>
+#include <string_view>
+#include <memory>
+#include <variant>
+
+#include "rspamd_symcache.h"
+#include "contrib/libev/ev.h"
+#include "contrib/ankerl/unordered_dense.h"
+#include "contrib/expected/expected.hpp"
+#include "cfg_file.h"
+
+#include "symcache_id_list.hxx"
+
+#define msg_err_cache(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "symcache", log_tag(), \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_err_cache_lambda(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "symcache", log_tag(), \
+ log_func, \
+ __VA_ARGS__)
+#define msg_err_cache_task(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ "symcache", task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_cache(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ "symcache", log_tag(), \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_cache(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ "symcache", log_tag(), \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_cache(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ ::rspamd::symcache::rspamd_symcache_log_id, "symcache", log_tag(), \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_cache_lambda(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ ::rspamd::symcache::rspamd_symcache_log_id, "symcache", log_tag(), \
+ log_func, \
+ __VA_ARGS__)
+#define msg_debug_cache_task(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ ::rspamd::symcache::rspamd_symcache_log_id, "symcache", task->task_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_debug_cache_task_lambda(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ ::rspamd::symcache::rspamd_symcache_log_id, "symcache", task->task_pool->tag.uid, \
+ log_func, \
+ __VA_ARGS__)
+
+struct lua_State;
+
+namespace rspamd::symcache {
+
+/* Defined in symcache_impl.cxx */
+extern int rspamd_symcache_log_id;
+
+static const std::uint8_t symcache_magic[8] = {'r', 's', 'c', 2, 0, 0, 0, 0};
+
+struct symcache_header {
+ std::uint8_t magic[8];
+ unsigned int nitems;
+ std::uint8_t checksum[64];
+ std::uint8_t unused[128];
+};
+
+struct cache_item;
+using cache_item_ptr = std::shared_ptr<cache_item>;
+
+/**
+ * This structure is intended to keep the current ordering for all symbols
+ * It is designed to be shared among all tasks and keep references to the real
+ * symbols.
+ * If some symbol has been added or removed to the symbol cache, it will not affect
+ * the current order, and it will only be regenerated for the subsequent tasks.
+ * This allows safe and no copy sharing and keeping track of all symbols in the
+ * cache runtime.
+ */
+struct order_generation {
+ /* All items ordered */
+ std::vector<cache_item_ptr> d;
+ /* Mapping from symbol name to the position in the order array */
+ ankerl::unordered_dense::map<std::string_view, unsigned int> by_symbol;
+ /* Mapping from symbol id to the position in the order array */
+ ankerl::unordered_dense::map<unsigned int, unsigned int> by_cache_id;
+ /* It matches cache->generation_id; if not, a fresh ordering is required */
+ unsigned int generation_id;
+
+ explicit order_generation(std::size_t nelts, unsigned id)
+ : generation_id(id)
+ {
+ d.reserve(nelts);
+ by_symbol.reserve(nelts);
+ by_cache_id.reserve(nelts);
+ }
+
+ auto size() const -> auto
+ {
+ return d.size();
+ }
+};
+
+using order_generation_ptr = std::shared_ptr<order_generation>;
+
+
+struct delayed_cache_dependency {
+ std::string from;
+ std::string to;
+
+ delayed_cache_dependency(std::string_view _from, std::string_view _to)
+ : from(_from), to(_to)
+ {
+ }
+};
+
+struct delayed_cache_condition {
+ std::string sym;
+ int cbref;
+ lua_State *L;
+
+public:
+ delayed_cache_condition(std::string_view sym, int cbref, lua_State *L)
+ : sym(sym), cbref(cbref), L(L)
+ {
+ }
+};
+
+class delayed_symbol_elt {
+private:
+ std::variant<std::string, rspamd_regexp_t *> content;
+
+public:
+ /* Disable copy */
+ delayed_symbol_elt() = delete;
+ delayed_symbol_elt(const delayed_symbol_elt &) = delete;
+ delayed_symbol_elt &operator=(const delayed_symbol_elt &) = delete;
+ /* Enable move */
+ delayed_symbol_elt(delayed_symbol_elt &&other) noexcept = default;
+ delayed_symbol_elt &operator=(delayed_symbol_elt &&other) noexcept = default;
+
+ explicit delayed_symbol_elt(std::string_view elt) noexcept
+ {
+ if (!elt.empty() && elt[0] == '/') {
+ /* Possibly regexp */
+ auto *re = rspamd_regexp_new_len(elt.data(), elt.size(), nullptr, nullptr);
+
+ if (re != nullptr) {
+ std::get<rspamd_regexp_t *>(content) = re;
+ }
+ else {
+ std::get<std::string>(content) = elt;
+ }
+ }
+ else {
+ std::get<std::string>(content) = elt;
+ }
+ }
+
+ ~delayed_symbol_elt()
+ {
+ if (std::holds_alternative<rspamd_regexp_t *>(content)) {
+ rspamd_regexp_unref(std::get<rspamd_regexp_t *>(content));
+ }
+ }
+
+ auto matches(std::string_view what) const -> bool
+ {
+ return std::visit([&](auto &elt) {
+ using T = typeof(elt);
+ if constexpr (std::is_same_v<T, rspamd_regexp_t *>) {
+ if (rspamd_regexp_match(elt, what.data(), what.size(), false)) {
+ return true;
+ }
+ }
+ else if constexpr (std::is_same_v<T, std::string>) {
+ return elt == what;
+ }
+
+ return false;
+ },
+ content);
+ }
+
+ auto to_string_view() const -> std::string_view
+ {
+ return std::visit([&](auto &elt) {
+ using T = typeof(elt);
+ if constexpr (std::is_same_v<T, rspamd_regexp_t *>) {
+ return std::string_view{rspamd_regexp_get_pattern(elt)};
+ }
+ else if constexpr (std::is_same_v<T, std::string>) {
+ return std::string_view{elt};
+ }
+
+ return std::string_view{};
+ },
+ content);
+ }
+};
+
+struct delayed_symbol_elt_equal {
+ using is_transparent = void;
+ auto operator()(const delayed_symbol_elt &a, const delayed_symbol_elt &b) const
+ {
+ return a.to_string_view() == b.to_string_view();
+ }
+ auto operator()(const delayed_symbol_elt &a, const std::string_view &b) const
+ {
+ return a.to_string_view() == b;
+ }
+ auto operator()(const std::string_view &a, const delayed_symbol_elt &b) const
+ {
+ return a == b.to_string_view();
+ }
+};
+
+struct delayed_symbol_elt_hash {
+ using is_transparent = void;
+ auto operator()(const delayed_symbol_elt &a) const
+ {
+ return ankerl::unordered_dense::hash<std::string_view>()(a.to_string_view());
+ }
+ auto operator()(const std::string_view &a) const
+ {
+ return ankerl::unordered_dense::hash<std::string_view>()(a);
+ }
+};
+
+class symcache {
+private:
+ using items_ptr_vec = std::vector<cache_item *>;
+ /* Map indexed by symbol name: all symbols must have unique names, so this map holds ownership */
+ ankerl::unordered_dense::map<std::string_view, cache_item *> items_by_symbol;
+ ankerl::unordered_dense::map<int, cache_item_ptr> items_by_id;
+
+ /* Items sorted into some order */
+ order_generation_ptr items_by_order;
+ unsigned int cur_order_gen;
+
+ /* Specific vectors for execution/iteration */
+ items_ptr_vec connfilters;
+ items_ptr_vec prefilters;
+ items_ptr_vec filters;
+ items_ptr_vec postfilters;
+ items_ptr_vec composites;
+ items_ptr_vec idempotent;
+ items_ptr_vec classifiers;
+ items_ptr_vec virtual_symbols;
+
+ /* These are stored within pointer to clean up after init */
+ std::unique_ptr<std::vector<delayed_cache_dependency>> delayed_deps;
+ std::unique_ptr<std::vector<delayed_cache_condition>> delayed_conditions;
+ /* Delayed statically enabled or disabled symbols */
+ using delayed_symbol_names = ankerl::unordered_dense::set<delayed_symbol_elt,
+ delayed_symbol_elt_hash, delayed_symbol_elt_equal>;
+ std::unique_ptr<delayed_symbol_names> disabled_symbols;
+ std::unique_ptr<delayed_symbol_names> enabled_symbols;
+
+ rspamd_mempool_t *static_pool;
+ std::uint64_t cksum;
+ double total_weight;
+ std::size_t stats_symbols_count;
+
+private:
+ std::uint64_t total_hits;
+
+ struct rspamd_config *cfg;
+ lua_State *L;
+ double reload_time;
+ double last_profile;
+
+private:
+ int peak_cb;
+ int cache_id;
+
+private:
+ /* Internal methods */
+ auto load_items() -> bool;
+ auto resort() -> void;
+ auto get_item_specific_vector(const cache_item &) -> items_ptr_vec &;
+ /* Helper for g_hash_table_foreach */
+ static auto metric_connect_cb(void *k, void *v, void *ud) -> void;
+
+public:
+ explicit symcache(struct rspamd_config *cfg)
+ : cfg(cfg)
+ {
+ /* XXX: do we need a special pool for symcache? I don't think so */
+ static_pool = cfg->cfg_pool;
+ reload_time = cfg->cache_reload_time;
+ total_hits = 1;
+ total_weight = 1.0;
+ cksum = 0xdeadbabe;
+ peak_cb = -1;
+ cache_id = rspamd_random_uint64_fast();
+ L = (lua_State *) cfg->lua_state;
+ delayed_conditions = std::make_unique<std::vector<delayed_cache_condition>>();
+ delayed_deps = std::make_unique<std::vector<delayed_cache_dependency>>();
+ }
+
+ virtual ~symcache();
+
+ /**
+ * Saves items on disk (if possible)
+ * @return
+ */
+ auto save_items() const -> bool;
+
+ /**
+ * Get an item by ID
+ * @param id
+ * @param resolve_parent
+ * @return
+ */
+ auto get_item_by_id(int id, bool resolve_parent) const -> const cache_item *;
+ auto get_item_by_id_mut(int id, bool resolve_parent) const -> cache_item *;
+ /**
+ * Get an item by it's name
+ * @param name
+ * @param resolve_parent
+ * @return
+ */
+ auto get_item_by_name(std::string_view name, bool resolve_parent) const -> const cache_item *;
+ /**
+ * Get an item by it's name, mutable pointer
+ * @param name
+ * @param resolve_parent
+ * @return
+ */
+ auto get_item_by_name_mut(std::string_view name, bool resolve_parent) const -> cache_item *;
+
+ /**
+ * Add a direct dependency
+ * @param id_from
+ * @param to
+ * @param virtual_id_from
+ * @return
+ */
+ auto add_dependency(int id_from, std::string_view to, int virtual_id_from) -> void;
+
+ /**
+ * Add a delayed dependency between symbols that will be resolved on the init stage
+ * @param from
+ * @param to
+ */
+ auto add_delayed_dependency(std::string_view from, std::string_view to) -> void
+ {
+ if (!delayed_deps) {
+ delayed_deps = std::make_unique<std::vector<delayed_cache_dependency>>();
+ }
+
+ delayed_deps->emplace_back(from, to);
+ }
+
+ /**
+ * Adds a symbol to the list of the disabled symbols
+ * @param sym
+ * @return
+ */
+ auto disable_symbol_delayed(std::string_view sym) -> bool
+ {
+ if (!disabled_symbols) {
+ disabled_symbols = std::make_unique<delayed_symbol_names>();
+ }
+
+ if (!disabled_symbols->contains(sym)) {
+ disabled_symbols->emplace(sym);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Adds a symbol to the list of the enabled symbols
+ * @param sym
+ * @return
+ */
+ auto enable_symbol_delayed(std::string_view sym) -> bool
+ {
+ if (!enabled_symbols) {
+ enabled_symbols = std::make_unique<delayed_symbol_names>();
+ }
+
+ if (!enabled_symbols->contains(sym)) {
+ enabled_symbols->emplace(sym);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Initialises the symbols cache, must be called after all symbols are added
+ * and the config file is loaded
+ */
+ auto init() -> bool;
+
+ /**
+ * Log helper that returns cfg checksum
+ * @return
+ */
+ auto log_tag() const -> const char *
+ {
+ return cfg->checksum;
+ }
+
+ /**
+ * Helper to return a memory pool associated with the cache
+ * @return
+ */
+ auto get_pool() const
+ {
+ return static_pool;
+ }
+
+ /**
+ * A method to add a generic symbol with a callback to couple with C API
+ * @param name name of the symbol, unlike C API it must be "" for callback only (compat) symbols, in this case an automatic name is generated
+ * @param priority
+ * @param func
+ * @param user_data
+ * @param flags_and_type mix of flags and type in a messy C enum
+ * @return id of a new symbol or -1 in case of failure
+ */
+ auto add_symbol_with_callback(std::string_view name,
+ int priority,
+ symbol_func_t func,
+ void *user_data,
+ int flags_and_type) -> int;
+ /**
+ * A method to add a generic virtual symbol with no function associated
+ * @param name must have some value, or a fatal error will strike you
+ * @param parent_id if this param is -1 then this symbol is associated with nothing
+ * @param flags_and_type mix of flags and type in a messy C enum
+ * @return id of a new symbol or -1 in case of failure
+ */
+ auto add_virtual_symbol(std::string_view name, int parent_id,
+ int flags_and_type) -> int;
+
+ /**
+ * Sets a lua callback to be called on peaks in execution time
+ * @param cbref
+ */
+ auto set_peak_cb(int cbref) -> void;
+
+ /**
+ * Add a delayed condition for a symbol that might not be registered yet
+ * @param sym
+ * @param cbref
+ */
+ auto add_delayed_condition(std::string_view sym, int cbref) -> void;
+
+ /**
+ * Returns number of symbols that needs to be checked in statistical algorithm
+ * @return
+ */
+ auto get_stats_symbols_count() const
+ {
+ return stats_symbols_count;
+ }
+
+ /**
+ * Returns a checksum for the cache
+ * @return
+ */
+ auto get_cksum() const
+ {
+ return cksum;
+ }
+
+ /**
+ * Validate symbols in the cache
+ * @param strict
+ * @return
+ */
+ auto validate(bool strict) -> bool;
+
+ /**
+ * Returns counters for the cache
+ * @return
+ */
+ auto counters() const -> ucl_object_t *;
+
+ /**
+ * Adjusts stats of the cache for the periodic counter
+ */
+ auto periodic_resort(struct ev_loop *ev_loop, double cur_time, double last_resort) -> void;
+
+ /**
+ * A simple helper to get the reload time
+ * @return
+ */
+ auto get_reload_time() const
+ {
+ return reload_time;
+ };
+
+ /**
+ * Iterate over all symbols using a specific functor
+ * @tparam Functor
+ * @param f
+ */
+ template<typename Functor>
+ auto symbols_foreach(Functor f) -> void
+ {
+ for (const auto &sym_it: items_by_symbol) {
+ f(sym_it.second);
+ }
+ }
+
+ /**
+ * Iterate over all composites using a specific functor
+ * @tparam Functor
+ * @param f
+ */
+ template<typename Functor>
+ auto composites_foreach(Functor f) -> void
+ {
+ for (const auto &sym_it: composites) {
+ f(sym_it);
+ }
+ }
+
+ /**
+ * Iterate over all composites using a specific functor
+ * @tparam Functor
+ * @param f
+ */
+ template<typename Functor>
+ auto connfilters_foreach(Functor f) -> bool
+ {
+ return std::all_of(std::begin(connfilters), std::end(connfilters),
+ [&](const auto &sym_it) {
+ return f(sym_it);
+ });
+ }
+ template<typename Functor>
+ auto prefilters_foreach(Functor f) -> bool
+ {
+ return std::all_of(std::begin(prefilters), std::end(prefilters),
+ [&](const auto &sym_it) {
+ return f(sym_it);
+ });
+ }
+ template<typename Functor>
+ auto postfilters_foreach(Functor f) -> bool
+ {
+ return std::all_of(std::begin(postfilters), std::end(postfilters),
+ [&](const auto &sym_it) {
+ return f(sym_it);
+ });
+ }
+ template<typename Functor>
+ auto idempotent_foreach(Functor f) -> bool
+ {
+ return std::all_of(std::begin(idempotent), std::end(idempotent),
+ [&](const auto &sym_it) {
+ return f(sym_it);
+ });
+ }
+ template<typename Functor>
+ auto filters_foreach(Functor f) -> bool
+ {
+ return std::all_of(std::begin(filters), std::end(filters),
+ [&](const auto &sym_it) {
+ return f(sym_it);
+ });
+ }
+
+ /**
+ * Resort cache if anything has been changed since last time
+ * @return
+ */
+ auto maybe_resort() -> bool;
+
+ /**
+ * Returns current set of items ordered for sharing ownership
+ * @return
+ */
+ auto get_cache_order() const -> auto
+ {
+ return items_by_order;
+ }
+
+ /**
+ * Get last profile timestamp
+ * @return
+ */
+ auto get_last_profile() const -> auto
+ {
+ return last_profile;
+ }
+
+ /**
+ * Sets last profile timestamp
+ * @param last_profile
+ * @return
+ */
+ auto set_last_profile(double last_profile)
+ {
+ symcache::last_profile = last_profile;
+ }
+
+ /**
+ * Process settings elt identified by id
+ * @param elt
+ */
+ auto process_settings_elt(struct rspamd_config_settings_elt *elt) -> void;
+
+ /**
+ * Returns maximum timeout that is requested by all rules
+ * @return
+ */
+ auto get_max_timeout(std::vector<std::pair<double, const cache_item *>> &elts) const -> double;
+};
+
+
+}// namespace rspamd::symcache
+
+#endif//RSPAMD_SYMCACHE_INTERNAL_HXX
diff --git a/src/libserver/symcache/symcache_item.cxx b/src/libserver/symcache/symcache_item.cxx
new file mode 100644
index 0000000..ac901f5
--- /dev/null
+++ b/src/libserver/symcache/symcache_item.cxx
@@ -0,0 +1,652 @@
+/*
+ * 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 "lua/lua_common.h"
+#include "symcache_internal.hxx"
+#include "symcache_item.hxx"
+#include "fmt/core.h"
+#include "libserver/task.h"
+#include "libutil/cxx/util.hxx"
+#include <numeric>
+#include <functional>
+
+namespace rspamd::symcache {
+
+enum class augmentation_value_type {
+ NO_VALUE,
+ STRING_VALUE,
+ NUMBER_VALUE,
+};
+
+struct augmentation_info {
+ int weight = 0;
+ int implied_flags = 0;
+ augmentation_value_type value_type = augmentation_value_type::NO_VALUE;
+};
+
+/* A list of internal augmentations that are known to Rspamd with their weight */
+static const auto known_augmentations =
+ ankerl::unordered_dense::map<std::string, augmentation_info, rspamd::smart_str_hash, rspamd::smart_str_equal>{
+ {"passthrough", {.weight = 10, .implied_flags = SYMBOL_TYPE_IGNORE_PASSTHROUGH}},
+ {"single_network", {.weight = 1, .implied_flags = 0}},
+ {"no_network", {.weight = 0, .implied_flags = 0}},
+ {"many_network", {.weight = 1, .implied_flags = 0}},
+ {"important", {.weight = 5, .implied_flags = SYMBOL_TYPE_FINE}},
+ {"timeout", {
+ .weight = 0,
+ .implied_flags = 0,
+ .value_type = augmentation_value_type::NUMBER_VALUE,
+ }}};
+
+auto cache_item::get_parent(const symcache &cache) const -> const cache_item *
+{
+ if (is_virtual()) {
+ const auto &virtual_sp = std::get<virtual_item>(specific);
+
+ return virtual_sp.get_parent(cache);
+ }
+
+ return nullptr;
+}
+
+auto cache_item::get_parent_mut(const symcache &cache) -> cache_item *
+{
+ if (is_virtual()) {
+ auto &virtual_sp = std::get<virtual_item>(specific);
+
+ return virtual_sp.get_parent_mut(cache);
+ }
+
+ return nullptr;
+}
+
+auto cache_item::process_deps(const symcache &cache) -> void
+{
+ /* Allow logging macros to work */
+ auto log_tag = [&]() { return cache.log_tag(); };
+
+ for (auto &dep: deps) {
+ msg_debug_cache("process real dependency %s on %s", symbol.c_str(), dep.sym.c_str());
+ auto *dit = cache.get_item_by_name_mut(dep.sym, true);
+
+ if (dep.vid >= 0) {
+ /* Case of the virtual symbol that depends on another (maybe virtual) symbol */
+ const auto *vdit = cache.get_item_by_name(dep.sym, false);
+
+ if (!vdit) {
+ if (dit) {
+ msg_err_cache("cannot add dependency from %s on %s: no dependency symbol registered",
+ dep.sym.c_str(), dit->symbol.c_str());
+ }
+ }
+ else {
+ msg_debug_cache("process virtual dependency %s(%d) on %s(%d)", symbol.c_str(),
+ dep.vid, vdit->symbol.c_str(), vdit->id);
+
+ unsigned nids = 0;
+
+ /* Propagate ids */
+ msg_debug_cache("check id propagation for dependency %s from %s",
+ symbol.c_str(), dit->symbol.c_str());
+
+ const auto *ids = dit->allowed_ids.get_ids(nids);
+
+ if (nids > 0) {
+ msg_debug_cache("propagate allowed ids from %s to %s",
+ dit->symbol.c_str(), symbol.c_str());
+
+ allowed_ids.set_ids(ids, nids);
+ }
+
+ ids = dit->forbidden_ids.get_ids(nids);
+
+ if (nids > 0) {
+ msg_debug_cache("propagate forbidden ids from %s to %s",
+ dit->symbol.c_str(), symbol.c_str());
+
+ forbidden_ids.set_ids(ids, nids);
+ }
+ }
+ }
+
+ if (dit != nullptr) {
+ if (!dit->is_filter()) {
+ /*
+ * Check sanity:
+ * - filters -> prefilter dependency is OK and always satisfied
+ * - postfilter -> (filter, prefilter) dep is ok
+ * - idempotent -> (any) dep is OK
+ *
+ * Otherwise, emit error
+ * However, even if everything is fine this dep is useless ¯\_(ツ)_/¯
+ */
+ auto ok_dep = false;
+
+ if (dit->get_type() == type) {
+ ok_dep = true;
+ }
+ else if (type < dit->get_type()) {
+ ok_dep = true;
+ }
+
+ if (!ok_dep) {
+ msg_err_cache("cannot add dependency from %s on %s: invalid symbol types",
+ dep.sym.c_str(), symbol.c_str());
+
+ continue;
+ }
+ }
+ else {
+ if (dit->id == id) {
+ msg_err_cache("cannot add dependency on self: %s -> %s "
+ "(resolved to %s)",
+ symbol.c_str(), dep.sym.c_str(), dit->symbol.c_str());
+ }
+ else {
+ /* Create a reverse dep */
+ if (is_virtual()) {
+ auto *parent = get_parent_mut(cache);
+
+ if (parent) {
+ dit->rdeps.emplace_back(parent, parent->symbol, parent->id, -1);
+ dep.item = dit;
+ dep.id = dit->id;
+
+ msg_debug_cache("added reverse dependency from %d on %d", parent->id,
+ dit->id);
+ }
+ }
+ else {
+ dep.item = dit;
+ dep.id = dit->id;
+ dit->rdeps.emplace_back(this, symbol, id, -1);
+ msg_debug_cache("added reverse dependency from %d on %d", id,
+ dit->id);
+ }
+ }
+ }
+ }
+ else if (dep.id >= 0) {
+ msg_err_cache("cannot find dependency on symbol %s for symbol %s",
+ dep.sym.c_str(), symbol.c_str());
+
+ continue;
+ }
+ }
+
+ // Remove empty deps
+ deps.erase(std::remove_if(std::begin(deps), std::end(deps),
+ [](const auto &dep) { return !dep.item; }),
+ std::end(deps));
+}
+
+auto cache_item::resolve_parent(const symcache &cache) -> bool
+{
+ auto log_tag = [&]() { return cache.log_tag(); };
+
+ if (is_virtual()) {
+ auto &virt = std::get<virtual_item>(specific);
+
+ if (virt.get_parent(cache)) {
+ msg_debug_cache("trying to resolve parent twice for %s", symbol.c_str());
+
+ return false;
+ }
+
+ return virt.resolve_parent(cache);
+ }
+ else {
+ msg_warn_cache("trying to resolve a parent for non-virtual symbol %s", symbol.c_str());
+ }
+
+ return false;
+}
+
+auto cache_item::update_counters_check_peak(lua_State *L,
+ struct ev_loop *ev_loop,
+ double cur_time,
+ double last_resort) -> bool
+{
+ auto ret = false;
+ static const double decay_rate = 0.25;
+
+ st->total_hits += st->hits;
+ g_atomic_int_set(&st->hits, 0);
+
+ if (last_count > 0) {
+ auto cur_value = (st->total_hits - last_count) /
+ (cur_time - last_resort);
+ rspamd_set_counter_ema(&st->frequency_counter,
+ cur_value, decay_rate);
+ st->avg_frequency = st->frequency_counter.mean;
+ st->stddev_frequency = st->frequency_counter.stddev;
+
+ auto cur_err = (st->avg_frequency - cur_value);
+ cur_err *= cur_err;
+
+ if (st->frequency_counter.number > 10 &&
+ cur_err > ::sqrt(st->stddev_frequency) * 3) {
+ frequency_peaks++;
+ ret = true;
+ }
+ }
+
+ last_count = st->total_hits;
+
+ if (cd->number > 0) {
+ if (!is_virtual()) {
+ st->avg_time = cd->mean;
+ rspamd_set_counter_ema(&st->time_counter,
+ st->avg_time, decay_rate);
+ st->avg_time = st->time_counter.mean;
+ memset(cd, 0, sizeof(*cd));
+ }
+ }
+
+ return ret;
+}
+
+auto cache_item::inc_frequency(const char *sym_name, symcache &cache) -> void
+{
+ if (sym_name && symbol != sym_name) {
+ if (is_filter()) {
+ const auto *children = get_children();
+ if (children) {
+ /* Likely a callback symbol with some virtual symbol that needs to be adjusted */
+ for (const auto &cld: *children) {
+ if (cld->get_name() == sym_name) {
+ cld->inc_frequency(sym_name, cache);
+ }
+ }
+ }
+ }
+ else {
+ /* Name not equal to symbol name, so we need to find the proper name */
+ auto *another_item = cache.get_item_by_name_mut(sym_name, false);
+ if (another_item != nullptr) {
+ another_item->inc_frequency(sym_name, cache);
+ }
+ }
+ }
+ else {
+ /* Symbol and sym name are the same */
+ g_atomic_int_inc(&st->hits);
+ }
+}
+
+auto cache_item::get_type_str() const -> const char *
+{
+ switch (type) {
+ case symcache_item_type::CONNFILTER:
+ return "connfilter";
+ case symcache_item_type::FILTER:
+ return "filter";
+ case symcache_item_type::IDEMPOTENT:
+ return "idempotent";
+ case symcache_item_type::PREFILTER:
+ return "prefilter";
+ case symcache_item_type::POSTFILTER:
+ return "postfilter";
+ case symcache_item_type::COMPOSITE:
+ return "composite";
+ case symcache_item_type::CLASSIFIER:
+ return "classifier";
+ case symcache_item_type::VIRTUAL:
+ return "virtual";
+ }
+
+ RSPAMD_UNREACHABLE;
+}
+
+auto cache_item::is_allowed(struct rspamd_task *task, bool exec_only) const -> bool
+{
+ const auto *what = "execution";
+
+ if (!exec_only) {
+ what = "symbol insertion";
+ }
+
+ /* Static checks */
+ if (!enabled ||
+ (RSPAMD_TASK_IS_EMPTY(task) && !(flags & SYMBOL_TYPE_EMPTY)) ||
+ (flags & SYMBOL_TYPE_MIME_ONLY && !RSPAMD_TASK_IS_MIME(task))) {
+
+ if (!enabled) {
+ msg_debug_cache_task("skipping %s of %s as it is permanently disabled",
+ what, symbol.c_str());
+
+ return false;
+ }
+ else {
+ /*
+ * If we check merely execution (not insertion), then we disallow
+ * mime symbols for non mime tasks and vice versa
+ */
+ if (exec_only) {
+ msg_debug_cache_task("skipping check of %s as it cannot be "
+ "executed for this task type",
+ symbol.c_str());
+
+ return FALSE;
+ }
+ }
+ }
+
+ /* Settings checks */
+ if (task->settings_elt != nullptr) {
+ if (forbidden_ids.check_id(task->settings_elt->id)) {
+ msg_debug_cache_task("deny %s of %s as it is forbidden for "
+ "settings id %ud",
+ what,
+ symbol.c_str(),
+ task->settings_elt->id);
+
+ return false;
+ }
+
+ if (!(flags & SYMBOL_TYPE_EXPLICIT_DISABLE)) {
+ if (!allowed_ids.check_id(task->settings_elt->id)) {
+
+ if (task->settings_elt->policy == RSPAMD_SETTINGS_POLICY_IMPLICIT_ALLOW) {
+ msg_debug_cache_task("allow execution of %s settings id %ud "
+ "allows implicit execution of the symbols;",
+ symbol.c_str(),
+ id);
+
+ return true;
+ }
+
+ if (exec_only) {
+ /*
+ * Special case if any of our virtual children are enabled
+ */
+ if (exec_only_ids.check_id(task->settings_elt->id)) {
+ return true;
+ }
+ }
+
+ msg_debug_cache_task("deny %s of %s as it is not listed "
+ "as allowed for settings id %ud",
+ what,
+ symbol.c_str(),
+ task->settings_elt->id);
+ return false;
+ }
+ }
+ else {
+ msg_debug_cache_task("allow %s of %s for "
+ "settings id %ud as it can be only disabled explicitly",
+ what,
+ symbol.c_str(),
+ task->settings_elt->id);
+ }
+ }
+ else if (flags & SYMBOL_TYPE_EXPLICIT_ENABLE) {
+ msg_debug_cache_task("deny %s of %s as it must be explicitly enabled",
+ what,
+ symbol.c_str());
+ return false;
+ }
+
+ /* Allow all symbols with no settings id */
+ return true;
+}
+
+auto cache_item::add_augmentation(const symcache &cache, std::string_view augmentation,
+ std::optional<std::string_view> value) -> bool
+{
+ auto log_tag = [&]() { return cache.log_tag(); };
+
+ if (augmentations.contains(augmentation)) {
+ msg_warn_cache("duplicate augmentation: %s", augmentation.data());
+
+ return false;
+ }
+
+ auto maybe_known = rspamd::find_map(known_augmentations, augmentation);
+
+ if (maybe_known.has_value()) {
+ auto &known_info = maybe_known.value().get();
+
+ if (known_info.implied_flags) {
+ if ((known_info.implied_flags & flags) == 0) {
+ msg_info_cache("added implied flags (%bd) for symbol %s as it has %s augmentation",
+ known_info.implied_flags, symbol.data(), augmentation.data());
+ flags |= known_info.implied_flags;
+ }
+ }
+
+ if (known_info.value_type == augmentation_value_type::NO_VALUE) {
+ if (value.has_value()) {
+ msg_err_cache("value specified for augmentation %s, that has no value",
+ augmentation.data());
+
+ return false;
+ }
+ return augmentations.try_emplace(augmentation, known_info.weight).second;
+ }
+ else {
+ if (!value.has_value()) {
+ msg_err_cache("value is not specified for augmentation %s, that requires explicit value",
+ augmentation.data());
+
+ return false;
+ }
+
+ if (known_info.value_type == augmentation_value_type::STRING_VALUE) {
+ return augmentations.try_emplace(augmentation, std::string{value.value()},
+ known_info.weight)
+ .second;
+ }
+ else if (known_info.value_type == augmentation_value_type::NUMBER_VALUE) {
+ /* I wish it was supported properly */
+ //auto conv_res = std::from_chars(value->data(), value->size(), num);
+ char numbuf[128], *endptr = nullptr;
+ rspamd_strlcpy(numbuf, value->data(), MIN(value->size(), sizeof(numbuf)));
+ auto num = g_ascii_strtod(numbuf, &endptr);
+
+ if (fabs(num) >= G_MAXFLOAT || std::isnan(num)) {
+ msg_err_cache("value for augmentation %s is not numeric: %*s",
+ augmentation.data(),
+ (int) value->size(), value->data());
+ return false;
+ }
+
+ return augmentations.try_emplace(augmentation, num,
+ known_info.weight)
+ .second;
+ }
+ }
+ }
+ else {
+ msg_debug_cache("added unknown augmentation %s for symbol %s",
+ "unknown", augmentation.data(), symbol.data());
+ return augmentations.try_emplace(augmentation, 0).second;
+ }
+
+ // Should not be reached
+ return false;
+}
+
+auto cache_item::get_augmentation_weight() const -> int
+{
+ return std::accumulate(std::begin(augmentations), std::end(augmentations),
+ 0, [](int acc, const auto &map_pair) {
+ return acc + map_pair.second.weight;
+ });
+}
+
+auto cache_item::get_numeric_augmentation(std::string_view name) const -> std::optional<double>
+{
+ const auto augmentation_value_maybe = rspamd::find_map(this->augmentations, name);
+
+ if (augmentation_value_maybe.has_value()) {
+ const auto &augmentation = augmentation_value_maybe.value().get();
+
+ if (std::holds_alternative<double>(augmentation.value)) {
+ return std::get<double>(augmentation.value);
+ }
+ }
+
+ return std::nullopt;
+}
+
+
+auto virtual_item::get_parent(const symcache &cache) const -> const cache_item *
+{
+ if (parent) {
+ return parent;
+ }
+
+ return cache.get_item_by_id(parent_id, false);
+}
+
+auto virtual_item::get_parent_mut(const symcache &cache) -> cache_item *
+{
+ if (parent) {
+ return parent;
+ }
+
+ return const_cast<cache_item *>(cache.get_item_by_id(parent_id, false));
+}
+
+auto virtual_item::resolve_parent(const symcache &cache) -> bool
+{
+ if (parent) {
+ return false;
+ }
+
+ auto item_ptr = cache.get_item_by_id(parent_id, true);
+
+ if (item_ptr) {
+ parent = const_cast<cache_item *>(item_ptr);
+
+ return true;
+ }
+
+ return false;
+}
+
+auto item_type_from_c(int type) -> tl::expected<std::pair<symcache_item_type, int>, std::string>
+{
+ constexpr const auto trivial_types = SYMBOL_TYPE_CONNFILTER | SYMBOL_TYPE_PREFILTER | SYMBOL_TYPE_POSTFILTER | SYMBOL_TYPE_IDEMPOTENT | SYMBOL_TYPE_COMPOSITE | SYMBOL_TYPE_CLASSIFIER | SYMBOL_TYPE_VIRTUAL;
+
+ constexpr auto all_but_one_ty = [&](int type, int exclude_bit) -> auto {
+ return (type & trivial_types) & (trivial_types & ~exclude_bit);
+ };
+
+ if (type & trivial_types) {
+ auto check_trivial = [&](auto flag,
+ symcache_item_type ty) -> tl::expected<std::pair<symcache_item_type, int>, std::string> {
+ if (all_but_one_ty(type, flag)) {
+ return tl::make_unexpected(fmt::format("invalid flags for a symbol: {}", (int) type));
+ }
+
+ return std::make_pair(ty, type & ~flag);
+ };
+ if (type & SYMBOL_TYPE_CONNFILTER) {
+ return check_trivial(SYMBOL_TYPE_CONNFILTER, symcache_item_type::CONNFILTER);
+ }
+ else if (type & SYMBOL_TYPE_PREFILTER) {
+ return check_trivial(SYMBOL_TYPE_PREFILTER, symcache_item_type::PREFILTER);
+ }
+ else if (type & SYMBOL_TYPE_POSTFILTER) {
+ return check_trivial(SYMBOL_TYPE_POSTFILTER, symcache_item_type::POSTFILTER);
+ }
+ else if (type & SYMBOL_TYPE_IDEMPOTENT) {
+ return check_trivial(SYMBOL_TYPE_IDEMPOTENT, symcache_item_type::IDEMPOTENT);
+ }
+ else if (type & SYMBOL_TYPE_COMPOSITE) {
+ return check_trivial(SYMBOL_TYPE_COMPOSITE, symcache_item_type::COMPOSITE);
+ }
+ else if (type & SYMBOL_TYPE_CLASSIFIER) {
+ return check_trivial(SYMBOL_TYPE_CLASSIFIER, symcache_item_type::CLASSIFIER);
+ }
+ else if (type & SYMBOL_TYPE_VIRTUAL) {
+ return check_trivial(SYMBOL_TYPE_VIRTUAL, symcache_item_type::VIRTUAL);
+ }
+
+ return tl::make_unexpected(fmt::format("internal error: impossible flags combination: {}", (int) type));
+ }
+
+ /* Maybe check other flags combination here? */
+ return std::make_pair(symcache_item_type::FILTER, type);
+}
+
+bool operator<(symcache_item_type lhs, symcache_item_type rhs)
+{
+ auto ret = false;
+ switch (lhs) {
+ case symcache_item_type::CONNFILTER:
+ break;
+ case symcache_item_type::PREFILTER:
+ if (rhs == symcache_item_type::CONNFILTER) {
+ ret = true;
+ }
+ break;
+ case symcache_item_type::FILTER:
+ if (rhs == symcache_item_type::CONNFILTER || rhs == symcache_item_type::PREFILTER) {
+ ret = true;
+ }
+ break;
+ case symcache_item_type::POSTFILTER:
+ if (rhs != symcache_item_type::IDEMPOTENT) {
+ ret = true;
+ }
+ break;
+ case symcache_item_type::IDEMPOTENT:
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+item_condition::~item_condition()
+{
+ if (cb != -1 && L != nullptr) {
+ luaL_unref(L, LUA_REGISTRYINDEX, cb);
+ }
+}
+
+auto item_condition::check(std::string_view sym_name, struct rspamd_task *task) const -> bool
+{
+ if (cb != -1 && L != nullptr) {
+ auto ret = false;
+
+ lua_pushcfunction(L, &rspamd_lua_traceback);
+ auto err_idx = lua_gettop(L);
+
+ lua_rawgeti(L, LUA_REGISTRYINDEX, cb);
+ rspamd_lua_task_push(L, task);
+
+ if (lua_pcall(L, 1, 1, err_idx) != 0) {
+ msg_info_task("call to condition for %s failed: %s",
+ sym_name.data(), lua_tostring(L, -1));
+ }
+ else {
+ ret = lua_toboolean(L, -1);
+ }
+
+ lua_settop(L, err_idx - 1);
+
+ return ret;
+ }
+
+ return true;
+}
+
+}// namespace rspamd::symcache
diff --git a/src/libserver/symcache/symcache_item.hxx b/src/libserver/symcache/symcache_item.hxx
new file mode 100644
index 0000000..a60213a
--- /dev/null
+++ b/src/libserver/symcache/symcache_item.hxx
@@ -0,0 +1,561 @@
+/*
+ * 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.
+ */
+
+#ifndef RSPAMD_SYMCACHE_ITEM_HXX
+#define RSPAMD_SYMCACHE_ITEM_HXX
+
+#pragma once
+
+#include <utility>
+#include <vector>
+#include <string>
+#include <string_view>
+#include <memory>
+#include <variant>
+#include <algorithm>
+#include <optional>
+
+#include "rspamd_symcache.h"
+#include "symcache_id_list.hxx"
+#include "contrib/expected/expected.hpp"
+#include "contrib/libev/ev.h"
+#include "symcache_runtime.hxx"
+#include "libutil/cxx/hash_util.hxx"
+
+namespace rspamd::symcache {
+
+class symcache;
+struct cache_item;
+using cache_item_ptr = std::shared_ptr<cache_item>;
+
+enum class symcache_item_type {
+ CONNFILTER, /* Executed on connection stage */
+ PREFILTER, /* Executed before all filters */
+ FILTER, /* Normal symbol with a callback */
+ POSTFILTER, /* Executed after all filters */
+ IDEMPOTENT, /* Executed after postfilters, cannot change results */
+ CLASSIFIER, /* A virtual classifier symbol */
+ COMPOSITE, /* A virtual composite symbol */
+ VIRTUAL, /* A virtual symbol... */
+};
+
+/*
+ * Compare item types: earlier stages symbols are > than later stages symbols
+ * Order for virtual stuff is not defined.
+ */
+bool operator<(symcache_item_type lhs, symcache_item_type rhs);
+
+constexpr static auto item_type_to_str(symcache_item_type t) -> const char *
+{
+ switch (t) {
+ case symcache_item_type::CONNFILTER:
+ return "connfilter";
+ case symcache_item_type::PREFILTER:
+ return "prefilter";
+ case symcache_item_type::FILTER:
+ return "filter";
+ case symcache_item_type::POSTFILTER:
+ return "postfilter";
+ case symcache_item_type::IDEMPOTENT:
+ return "idempotent";
+ case symcache_item_type::CLASSIFIER:
+ return "classifier";
+ case symcache_item_type::COMPOSITE:
+ return "composite";
+ case symcache_item_type::VIRTUAL:
+ return "virtual";
+ }
+}
+
+/**
+ * This is a public helper to convert a legacy C type to a more static type
+ * @param type input type as a C enum
+ * @return pair of type safe symcache_item_type + the remaining flags or an error
+ */
+auto item_type_from_c(int type) -> tl::expected<std::pair<symcache_item_type, int>, std::string>;
+
+struct item_condition {
+private:
+ lua_State *L = nullptr;
+ int cb = -1;
+
+public:
+ explicit item_condition(lua_State *L_, int cb_) noexcept
+ : L(L_), cb(cb_)
+ {
+ }
+ item_condition(item_condition &&other) noexcept
+ {
+ *this = std::move(other);
+ }
+ /* Make it move only */
+ item_condition(const item_condition &) = delete;
+ item_condition &operator=(item_condition &&other) noexcept
+ {
+ std::swap(other.L, L);
+ std::swap(other.cb, cb);
+ return *this;
+ }
+ ~item_condition();
+
+ auto check(std::string_view sym_name, struct rspamd_task *task) const -> bool;
+};
+
+class normal_item {
+private:
+ symbol_func_t func = nullptr;
+ void *user_data = nullptr;
+ std::vector<cache_item *> virtual_children;
+ std::vector<item_condition> conditions;
+
+public:
+ explicit normal_item(symbol_func_t _func, void *_user_data)
+ : func(_func), user_data(_user_data)
+ {
+ }
+
+ auto add_condition(lua_State *L, int cbref) -> void
+ {
+ conditions.emplace_back(L, cbref);
+ }
+
+ auto call(struct rspamd_task *task, struct rspamd_symcache_dynamic_item *item) const -> void
+ {
+ func(task, item, user_data);
+ }
+
+ auto check_conditions(std::string_view sym_name, struct rspamd_task *task) const -> bool
+ {
+ return std::all_of(std::begin(conditions), std::end(conditions),
+ [&](const auto &cond) { return cond.check(sym_name, task); });
+ }
+
+ auto get_cbdata() const -> auto
+ {
+ return user_data;
+ }
+
+ auto add_child(cache_item *ptr) -> void
+ {
+ virtual_children.push_back(ptr);
+ }
+
+ auto get_childen() const -> const std::vector<cache_item *> &
+ {
+ return virtual_children;
+ }
+};
+
+class virtual_item {
+private:
+ int parent_id = -1;
+ cache_item *parent = nullptr;
+
+public:
+ explicit virtual_item(int _parent_id)
+ : parent_id(_parent_id)
+ {
+ }
+
+ auto get_parent(const symcache &cache) const -> const cache_item *;
+ auto get_parent_mut(const symcache &cache) -> cache_item *;
+
+ auto resolve_parent(const symcache &cache) -> bool;
+};
+
+struct cache_dependency {
+ cache_item *item; /* Real dependency */
+ std::string sym; /* Symbolic dep name */
+ int id; /* Real from */
+ int vid; /* Virtual from */
+public:
+ /* Default piecewise constructor */
+ explicit cache_dependency(cache_item *_item, std::string _sym, int _id, int _vid)
+ : item(_item), sym(std::move(_sym)), id(_id), vid(_vid)
+ {
+ }
+};
+
+/*
+ * Used to store augmentation values
+ */
+struct item_augmentation {
+ std::variant<std::monostate, std::string, double> value;
+ int weight;
+
+ explicit item_augmentation(int weight)
+ : value(std::monostate{}), weight(weight)
+ {
+ }
+ explicit item_augmentation(std::string str_value, int weight)
+ : value(str_value), weight(weight)
+ {
+ }
+ explicit item_augmentation(double double_value, int weight)
+ : value(double_value), weight(weight)
+ {
+ }
+};
+
+struct cache_item : std::enable_shared_from_this<cache_item> {
+ /* The following fields will live in shared memory */
+ struct rspamd_symcache_item_stat *st = nullptr;
+ struct rspamd_counter_data *cd = nullptr;
+
+ /* Unique id - counter */
+ int id;
+ std::uint64_t last_count = 0;
+ std::string symbol;
+ symcache_item_type type;
+ int flags;
+
+ /* Condition of execution */
+ bool enabled = true;
+
+ /* Priority */
+ int priority = 0;
+ /* Topological order */
+ unsigned int order = 0;
+ int frequency_peaks = 0;
+
+ /* Specific data for virtual and callback symbols */
+ std::variant<normal_item, virtual_item> specific;
+
+ /* Settings ids */
+ id_list allowed_ids;
+ /* Allows execution but not symbols insertion */
+ id_list exec_only_ids;
+ id_list forbidden_ids;
+
+ /* Set of augmentations */
+ ankerl::unordered_dense::map<std::string, item_augmentation,
+ rspamd::smart_str_hash, rspamd::smart_str_equal>
+ augmentations;
+
+ /* Dependencies */
+ std::vector<cache_dependency> deps;
+ /* Reverse dependencies */
+ std::vector<cache_dependency> rdeps;
+
+public:
+ /**
+ * Create a normal item with a callback
+ * @param name
+ * @param priority
+ * @param func
+ * @param user_data
+ * @param type
+ * @param flags
+ * @return
+ */
+ template<typename T>
+ static auto create_with_function(rspamd_mempool_t *pool,
+ int id,
+ T &&name,
+ int priority,
+ symbol_func_t func,
+ void *user_data,
+ symcache_item_type type,
+ int flags) -> cache_item_ptr
+ {
+ return std::shared_ptr<cache_item>(new cache_item(pool,
+ id, std::forward<T>(name), priority,
+ func, user_data,
+ type, flags));
+ }
+
+ /**
+ * Create a virtual item
+ * @param name
+ * @param priority
+ * @param parent
+ * @param type
+ * @param flags
+ * @return
+ */
+ template<typename T>
+ static auto create_with_virtual(rspamd_mempool_t *pool,
+ int id,
+ T &&name,
+ int parent,
+ symcache_item_type type,
+ int flags) -> cache_item_ptr
+ {
+ return std::shared_ptr<cache_item>(new cache_item(pool, id, std::forward<T>(name),
+ parent, type, flags));
+ }
+
+ /**
+ * Share ownership on the item
+ * @return
+ */
+ auto getptr() -> cache_item_ptr
+ {
+ return shared_from_this();
+ }
+
+ /**
+ * Process and resolve dependencies for the item
+ * @param cache
+ */
+ auto process_deps(const symcache &cache) -> void;
+
+ auto is_virtual() const -> bool
+ {
+ return std::holds_alternative<virtual_item>(specific);
+ }
+
+ auto is_filter() const -> bool
+ {
+ return std::holds_alternative<normal_item>(specific) &&
+ (type == symcache_item_type::FILTER);
+ }
+
+ /**
+ * Returns true if a symbol should have some score defined
+ * @return
+ */
+ auto is_scoreable() const -> bool
+ {
+ return !(flags & SYMBOL_TYPE_CALLBACK) &&
+ ((type == symcache_item_type::FILTER) ||
+ is_virtual() ||
+ (type == symcache_item_type::COMPOSITE) ||
+ (type == symcache_item_type::CLASSIFIER));
+ }
+
+ auto is_ghost() const -> bool
+ {
+ return flags & SYMBOL_TYPE_GHOST;
+ }
+
+ auto get_parent(const symcache &cache) const -> const cache_item *;
+ auto get_parent_mut(const symcache &cache) -> cache_item *;
+
+ auto resolve_parent(const symcache &cache) -> bool;
+
+ auto get_type() const -> auto
+ {
+ return type;
+ }
+
+ auto get_type_str() const -> const char *;
+
+ auto get_name() const -> const std::string &
+ {
+ return symbol;
+ }
+
+ auto get_flags() const -> auto
+ {
+ return flags;
+ };
+
+ auto add_condition(lua_State *L, int cbref) -> bool
+ {
+ if (!is_virtual()) {
+ auto &normal = std::get<normal_item>(specific);
+ normal.add_condition(L, cbref);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ auto update_counters_check_peak(lua_State *L,
+ struct ev_loop *ev_loop,
+ double cur_time,
+ double last_resort) -> bool;
+
+ /**
+ * Increase frequency for a symbol
+ */
+ auto inc_frequency(const char *sym_name, symcache &cache) -> void;
+
+ /**
+ * Check if an item is allowed to be executed not checking item conditions
+ * @param task
+ * @param exec_only
+ * @return
+ */
+ auto is_allowed(struct rspamd_task *task, bool exec_only) const -> bool;
+
+ /**
+ * Returns callback data
+ * @return
+ */
+ auto get_cbdata() const -> void *
+ {
+ if (std::holds_alternative<normal_item>(specific)) {
+ const auto &filter_data = std::get<normal_item>(specific);
+
+ return filter_data.get_cbdata();
+ }
+
+ return nullptr;
+ }
+
+ /**
+ * Check all conditions for an item
+ * @param task
+ * @return
+ */
+ auto check_conditions(struct rspamd_task *task) const -> auto
+ {
+ if (std::holds_alternative<normal_item>(specific)) {
+ const auto &filter_data = std::get<normal_item>(specific);
+
+ return filter_data.check_conditions(symbol, task);
+ }
+
+ return false;
+ }
+
+ auto call(struct rspamd_task *task, cache_dynamic_item *dyn_item) const -> void
+ {
+ if (std::holds_alternative<normal_item>(specific)) {
+ const auto &filter_data = std::get<normal_item>(specific);
+
+ filter_data.call(task, (struct rspamd_symcache_dynamic_item *) dyn_item);
+ }
+ }
+
+ /**
+ * Add an augmentation to the item, returns `true` if augmentation is known and unique, false otherwise
+ * @param augmentation
+ * @return
+ */
+ auto add_augmentation(const symcache &cache, std::string_view augmentation,
+ std::optional<std::string_view> value) -> bool;
+
+ /**
+ * Return sum weight of all known augmentations
+ * @return
+ */
+ auto get_augmentation_weight() const -> int;
+
+ /**
+ * Returns numeric augmentation value
+ * @param name
+ * @return
+ */
+ auto get_numeric_augmentation(std::string_view name) const -> std::optional<double>;
+
+ /**
+ * Returns string augmentation value
+ * @param name
+ * @return
+ */
+ auto get_string_augmentation(std::string_view name) const -> std::optional<std::string_view>;
+
+ /**
+ * Add a virtual symbol as a child of some normal symbol
+ * @param ptr
+ */
+ auto add_child(cache_item *ptr) -> void
+ {
+ if (std::holds_alternative<normal_item>(specific)) {
+ auto &filter_data = std::get<normal_item>(specific);
+
+ filter_data.add_child(ptr);
+ }
+ else {
+ g_assert("add child is called for a virtual symbol!");
+ }
+ }
+
+ /**
+ * Returns virtual children for a normal item
+ * @param ptr
+ * @return
+ */
+ auto get_children() const -> const std::vector<cache_item *> *
+ {
+ if (std::holds_alternative<normal_item>(specific)) {
+ const auto &filter_data = std::get<normal_item>(specific);
+
+ return &filter_data.get_childen();
+ }
+
+ return nullptr;
+ }
+
+private:
+ /**
+ * Constructor for a normal symbols with callback
+ * @param name
+ * @param _priority
+ * @param func
+ * @param user_data
+ * @param _type
+ * @param _flags
+ */
+ cache_item(rspamd_mempool_t *pool,
+ int _id,
+ std::string &&name,
+ int _priority,
+ symbol_func_t func,
+ void *user_data,
+ symcache_item_type _type,
+ int _flags)
+ : id(_id),
+ symbol(std::move(name)),
+ type(_type),
+ flags(_flags),
+ priority(_priority),
+ specific(normal_item{func, user_data})
+ {
+ /* These structures are kept trivial, so they need to be explicitly reset */
+ forbidden_ids.reset();
+ allowed_ids.reset();
+ exec_only_ids.reset();
+ st = rspamd_mempool_alloc0_shared_type(pool, std::remove_pointer_t<decltype(st)>);
+ cd = rspamd_mempool_alloc0_shared_type(pool, std::remove_pointer_t<decltype(cd)>);
+ }
+
+ /**
+ * Constructor for a virtual symbol
+ * @param name
+ * @param _priority
+ * @param parent
+ * @param _type
+ * @param _flags
+ */
+ cache_item(rspamd_mempool_t *pool,
+ int _id,
+ std::string &&name,
+ int parent,
+ symcache_item_type _type,
+ int _flags)
+ : id(_id),
+ symbol(std::move(name)),
+ type(_type),
+ flags(_flags),
+ specific(virtual_item{parent})
+ {
+ /* These structures are kept trivial, so they need to be explicitly reset */
+ forbidden_ids.reset();
+ allowed_ids.reset();
+ exec_only_ids.reset();
+ st = rspamd_mempool_alloc0_shared_type(pool, std::remove_pointer_t<decltype(st)>);
+ cd = rspamd_mempool_alloc0_shared_type(pool, std::remove_pointer_t<decltype(cd)>);
+ }
+};
+
+}// namespace rspamd::symcache
+
+#endif//RSPAMD_SYMCACHE_ITEM_HXX
diff --git a/src/libserver/symcache/symcache_periodic.hxx b/src/libserver/symcache/symcache_periodic.hxx
new file mode 100644
index 0000000..535956b
--- /dev/null
+++ b/src/libserver/symcache/symcache_periodic.hxx
@@ -0,0 +1,89 @@
+/*-
+ * Copyright 2022 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_SYMCACHE_PERIODIC_HXX
+#define RSPAMD_SYMCACHE_PERIODIC_HXX
+
+#pragma once
+
+#include "config.h"
+#include "contrib/libev/ev.h"
+#include "symcache_internal.hxx"
+#include "worker_util.h"
+
+namespace rspamd::symcache {
+struct cache_refresh_cbdata {
+private:
+ symcache *cache;
+ struct ev_loop *event_loop;
+ struct rspamd_worker *w;
+ double reload_time;
+ double last_resort;
+ ev_timer resort_ev;
+
+public:
+ explicit cache_refresh_cbdata(symcache *_cache,
+ struct ev_loop *_ev_base,
+ struct rspamd_worker *_w)
+ : cache(_cache), event_loop(_ev_base), w(_w)
+ {
+ auto log_tag = [&]() { return cache->log_tag(); };
+ last_resort = rspamd_get_ticks(TRUE);
+ reload_time = cache->get_reload_time();
+ auto tm = rspamd_time_jitter(reload_time, 0);
+ msg_debug_cache("next reload in %.2f seconds", tm);
+ ev_timer_init(&resort_ev, cache_refresh_cbdata::resort_cb,
+ tm, tm);
+ resort_ev.data = (void *) this;
+ ev_timer_start(event_loop, &resort_ev);
+ rspamd_mempool_add_destructor(cache->get_pool(),
+ cache_refresh_cbdata::refresh_dtor, (void *) this);
+ }
+
+ static void refresh_dtor(void *d)
+ {
+ auto *cbdata = (struct cache_refresh_cbdata *) d;
+ delete cbdata;
+ }
+
+ static void resort_cb(EV_P_ ev_timer *w, int _revents)
+ {
+ auto *cbdata = (struct cache_refresh_cbdata *) w->data;
+
+ auto log_tag = [&]() { return cbdata->cache->log_tag(); };
+
+ if (rspamd_worker_is_primary_controller(cbdata->w)) {
+ /* Plan new event */
+ auto tm = rspamd_time_jitter(cbdata->reload_time, 0);
+ msg_debug_cache("resort symbols cache, next reload in %.2f seconds", tm);
+ cbdata->resort_ev.repeat = tm;
+ ev_timer_again(EV_A_ w);
+ auto cur_time = rspamd_get_ticks(FALSE);
+ cbdata->cache->periodic_resort(cbdata->event_loop, cur_time, cbdata->last_resort);
+ cbdata->last_resort = cur_time;
+ }
+ }
+
+private:
+ ~cache_refresh_cbdata()
+ {
+ ev_timer_stop(event_loop, &resort_ev);
+ }
+};
+}// namespace rspamd::symcache
+
+#endif//RSPAMD_SYMCACHE_PERIODIC_HXX
diff --git a/src/libserver/symcache/symcache_runtime.cxx b/src/libserver/symcache/symcache_runtime.cxx
new file mode 100644
index 0000000..d9622d8
--- /dev/null
+++ b/src/libserver/symcache/symcache_runtime.cxx
@@ -0,0 +1,823 @@
+/*
+ * 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 "symcache_internal.hxx"
+#include "symcache_item.hxx"
+#include "symcache_runtime.hxx"
+#include "libutil/cxx/util.hxx"
+#include "libserver/task.h"
+#include "libmime/scan_result.h"
+#include "utlist.h"
+#include "libserver/worker_util.h"
+#include <limits>
+#include <cmath>
+
+namespace rspamd::symcache {
+
+/* At least once per minute */
+constexpr static const auto PROFILE_MAX_TIME = 60.0;
+/* For messages larger than 2Mb enable profiling */
+constexpr static const auto PROFILE_MESSAGE_SIZE_THRESHOLD = 1024ul * 1024 * 2;
+/* Enable profile at least once per this amount of messages processed */
+constexpr static const auto PROFILE_PROBABILITY = 0.01;
+
+auto symcache_runtime::create(struct rspamd_task *task, symcache &cache) -> symcache_runtime *
+{
+ cache.maybe_resort();
+
+ auto &&cur_order = cache.get_cache_order();
+ auto *checkpoint = (symcache_runtime *) rspamd_mempool_alloc0(task->task_pool,
+ sizeof(symcache_runtime) +
+ sizeof(struct cache_dynamic_item) * cur_order->size());
+
+ checkpoint->order = cache.get_cache_order();
+
+ /* Calculate profile probability */
+ ev_now_update_if_cheap(task->event_loop);
+ ev_tstamp now = ev_now(task->event_loop);
+ checkpoint->profile_start = now;
+ checkpoint->lim = rspamd_task_get_required_score(task, task->result);
+
+ if ((cache.get_last_profile() == 0.0 || now > cache.get_last_profile() + PROFILE_MAX_TIME) ||
+ (task->msg.len >= PROFILE_MESSAGE_SIZE_THRESHOLD) ||
+ (rspamd_random_double_fast() >= (1 - PROFILE_PROBABILITY))) {
+ msg_debug_cache_task("enable profiling of symbols for task");
+ checkpoint->profile = true;
+ cache.set_last_profile(now);
+ }
+
+ task->symcache_runtime = (void *) checkpoint;
+
+ return checkpoint;
+}
+
+auto symcache_runtime::process_settings(struct rspamd_task *task, const symcache &cache) -> bool
+{
+ if (!task->settings) {
+ msg_err_task("`process_settings` is called with no settings");
+ return false;
+ }
+
+ const auto *wl = ucl_object_lookup(task->settings, "whitelist");
+
+ if (wl != nullptr) {
+ msg_info_task("task is whitelisted");
+ task->flags |= RSPAMD_TASK_FLAG_SKIP;
+ return true;
+ }
+
+ auto already_disabled = false;
+
+ auto process_group = [&](const ucl_object_t *gr_obj, auto functor) -> void {
+ ucl_object_iter_t it = nullptr;
+ const ucl_object_t *cur;
+
+ if (gr_obj) {
+ while ((cur = ucl_iterate_object(gr_obj, &it, true)) != nullptr) {
+ if (ucl_object_type(cur) == UCL_STRING) {
+ auto *gr = (struct rspamd_symbols_group *)
+ g_hash_table_lookup(task->cfg->groups,
+ ucl_object_tostring(cur));
+
+ if (gr) {
+ GHashTableIter gr_it;
+ void *k, *v;
+ g_hash_table_iter_init(&gr_it, gr->symbols);
+
+ while (g_hash_table_iter_next(&gr_it, &k, &v)) {
+ functor((const char *) k);
+ }
+ }
+ }
+ }
+ }
+ };
+
+ ucl_object_iter_t it = nullptr;
+ const ucl_object_t *cur;
+
+ const auto *enabled = ucl_object_lookup(task->settings, "symbols_enabled");
+
+ if (enabled) {
+ msg_debug_cache_task("disable all symbols as `symbols_enabled` is found");
+ /* Disable all symbols but selected */
+ disable_all_symbols(SYMBOL_TYPE_EXPLICIT_DISABLE);
+ already_disabled = true;
+ it = nullptr;
+
+ while ((cur = ucl_iterate_object(enabled, &it, true)) != nullptr) {
+ enable_symbol(task, cache, ucl_object_tostring(cur));
+ }
+ }
+
+ /* Enable groups of symbols */
+ enabled = ucl_object_lookup(task->settings, "groups_enabled");
+ if (enabled && !already_disabled) {
+ disable_all_symbols(SYMBOL_TYPE_EXPLICIT_DISABLE);
+ }
+ process_group(enabled, [&](const char *sym) {
+ enable_symbol(task, cache, sym);
+ });
+
+ const auto *disabled = ucl_object_lookup(task->settings, "symbols_disabled");
+
+ if (disabled) {
+ it = nullptr;
+
+ while ((cur = ucl_iterate_object(disabled, &it, true)) != nullptr) {
+ disable_symbol(task, cache, ucl_object_tostring(cur));
+ }
+ }
+
+ /* Disable groups of symbols */
+ disabled = ucl_object_lookup(task->settings, "groups_disabled");
+ process_group(disabled, [&](const char *sym) {
+ disable_symbol(task, cache, sym);
+ });
+
+ /* Update required limit */
+ lim = rspamd_task_get_required_score(task, task->result);
+
+ return false;
+}
+
+auto symcache_runtime::disable_all_symbols(int skip_mask) -> void
+{
+ for (auto [i, item]: rspamd::enumerate(order->d)) {
+ auto *dyn_item = &dynamic_items[i];
+
+ if (!(item->get_flags() & skip_mask)) {
+ dyn_item->finished = true;
+ dyn_item->started = true;
+ }
+ }
+}
+
+auto symcache_runtime::disable_symbol(struct rspamd_task *task, const symcache &cache, std::string_view name) -> bool
+{
+ const auto *item = cache.get_item_by_name(name, true);
+
+ if (item != nullptr) {
+
+ auto *dyn_item = get_dynamic_item(item->id);
+
+ if (dyn_item) {
+ dyn_item->finished = true;
+ dyn_item->started = true;
+ msg_debug_cache_task("disable execution of %s", name.data());
+
+ return true;
+ }
+ else {
+ msg_debug_cache_task("cannot disable %s: id not found %d", name.data(), item->id);
+ }
+ }
+ else {
+ msg_debug_cache_task("cannot disable %s: symbol not found", name.data());
+ }
+
+ return false;
+}
+
+auto symcache_runtime::enable_symbol(struct rspamd_task *task, const symcache &cache, std::string_view name) -> bool
+{
+ const auto *item = cache.get_item_by_name(name, true);
+
+ if (item != nullptr) {
+
+ auto *dyn_item = get_dynamic_item(item->id);
+
+ if (dyn_item) {
+ dyn_item->finished = false;
+ dyn_item->started = false;
+ msg_debug_cache_task("enable execution of %s", name.data());
+
+ return true;
+ }
+ else {
+ msg_debug_cache_task("cannot enable %s: id not found %d", name.data(), item->id);
+ }
+ }
+ else {
+ msg_debug_cache_task("cannot enable %s: symbol not found", name.data());
+ }
+
+ return false;
+}
+
+auto symcache_runtime::is_symbol_checked(const symcache &cache, std::string_view name) -> bool
+{
+ const auto *item = cache.get_item_by_name(name, true);
+
+ if (item != nullptr) {
+
+ auto *dyn_item = get_dynamic_item(item->id);
+
+ if (dyn_item) {
+ return dyn_item->started;
+ }
+ }
+
+ return false;
+}
+
+auto symcache_runtime::is_symbol_enabled(struct rspamd_task *task, const symcache &cache, std::string_view name) -> bool
+{
+
+ const auto *item = cache.get_item_by_name(name, true);
+ if (item) {
+
+ if (!item->is_allowed(task, true)) {
+ return false;
+ }
+ else {
+ auto *dyn_item = get_dynamic_item(item->id);
+
+ if (dyn_item) {
+ if (dyn_item->started) {
+ /* Already started */
+ return false;
+ }
+
+ if (!item->is_virtual()) {
+ return std::get<normal_item>(item->specific).check_conditions(item->symbol, task);
+ }
+ }
+ else {
+ /* Unknown item */
+ msg_debug_cache_task("cannot enable %s: symbol not found", name.data());
+ }
+ }
+ }
+
+ return true;
+}
+
+auto symcache_runtime::get_dynamic_item(int id) const -> cache_dynamic_item *
+{
+
+ /* Not found in the cache, do a hash lookup */
+ auto our_id_maybe = rspamd::find_map(order->by_cache_id, id);
+
+ if (our_id_maybe) {
+ return &dynamic_items[our_id_maybe.value()];
+ }
+
+ return nullptr;
+}
+
+auto symcache_runtime::process_symbols(struct rspamd_task *task, symcache &cache, unsigned int stage) -> bool
+{
+ msg_debug_cache_task("symbols processing stage at pass: %d", stage);
+
+ if (RSPAMD_TASK_IS_SKIPPED(task)) {
+ return true;
+ }
+
+ switch (stage) {
+ case RSPAMD_TASK_STAGE_CONNFILTERS:
+ case RSPAMD_TASK_STAGE_PRE_FILTERS:
+ case RSPAMD_TASK_STAGE_POST_FILTERS:
+ case RSPAMD_TASK_STAGE_IDEMPOTENT:
+ return process_pre_postfilters(task, cache,
+ rspamd_session_events_pending(task->s), stage);
+ break;
+
+ case RSPAMD_TASK_STAGE_FILTERS:
+ return process_filters(task, cache, rspamd_session_events_pending(task->s));
+ break;
+
+ default:
+ g_assert_not_reached();
+ }
+}
+
+auto symcache_runtime::process_pre_postfilters(struct rspamd_task *task,
+ symcache &cache,
+ int start_events,
+ unsigned int stage) -> bool
+{
+ auto saved_priority = std::numeric_limits<int>::min();
+ auto all_done = true;
+ auto log_func = RSPAMD_LOG_FUNC;
+ auto compare_functor = +[](int a, int b) { return a < b; };
+
+ auto proc_func = [&](cache_item *item) {
+ /*
+ * We can safely ignore all pre/postfilters except idempotent ones and
+ * those that are marked as ignore passthrough result
+ */
+ if (stage != RSPAMD_TASK_STAGE_IDEMPOTENT &&
+ !(item->flags & SYMBOL_TYPE_IGNORE_PASSTHROUGH)) {
+ if (check_metric_limit(task)) {
+ msg_debug_cache_task_lambda("task has already the result being set, ignore further checks");
+
+ return true;
+ }
+ }
+
+ auto dyn_item = get_dynamic_item(item->id);
+
+ if (!dyn_item->started && !dyn_item->finished) {
+ if (has_slow) {
+ /* Delay */
+ has_slow = false;
+
+ return false;
+ }
+
+ if (saved_priority == std::numeric_limits<int>::min()) {
+ saved_priority = item->priority;
+ }
+ else {
+ if (compare_functor(item->priority, saved_priority) &&
+ rspamd_session_events_pending(task->s) > start_events) {
+ /*
+ * Delay further checks as we have higher
+ * priority filters to be processed
+ */
+ return false;
+ }
+ }
+
+ return process_symbol(task, cache, item, dyn_item);
+ }
+
+ /* Continue processing */
+ return true;
+ };
+
+ switch (stage) {
+ case RSPAMD_TASK_STAGE_CONNFILTERS:
+ all_done = cache.connfilters_foreach(proc_func);
+ break;
+ case RSPAMD_TASK_STAGE_PRE_FILTERS:
+ all_done = cache.prefilters_foreach(proc_func);
+ break;
+ case RSPAMD_TASK_STAGE_POST_FILTERS:
+ compare_functor = +[](int a, int b) { return a > b; };
+ all_done = cache.postfilters_foreach(proc_func);
+ break;
+ case RSPAMD_TASK_STAGE_IDEMPOTENT:
+ compare_functor = +[](int a, int b) { return a > b; };
+ all_done = cache.idempotent_foreach(proc_func);
+ break;
+ default:
+ g_error("invalid invocation");
+ break;
+ }
+
+ return all_done;
+}
+
+auto symcache_runtime::process_filters(struct rspamd_task *task, symcache &cache, int start_events) -> bool
+{
+ auto all_done = true;
+ auto log_func = RSPAMD_LOG_FUNC;
+ auto has_passtrough = false;
+
+ for (const auto [idx, item]: rspamd::enumerate(order->d)) {
+ /* Exclude all non filters */
+ if (item->type != symcache_item_type::FILTER) {
+ /*
+ * We use breaking the loop as we append non-filters to the end of the list
+ * so, it is safe to stop processing immediately
+ */
+ break;
+ }
+
+ if (!(item->flags & (SYMBOL_TYPE_FINE | SYMBOL_TYPE_IGNORE_PASSTHROUGH))) {
+ if (has_passtrough || check_metric_limit(task)) {
+ msg_debug_cache_task_lambda("task has already the result being set, ignore further checks");
+ has_passtrough = true;
+ /* Skip this item */
+ continue;
+ }
+ }
+
+ auto dyn_item = &dynamic_items[idx];
+
+ if (!dyn_item->started) {
+ all_done = false;
+
+ if (!check_item_deps(task, cache, item.get(),
+ dyn_item, false)) {
+ msg_debug_cache_task("blocked execution of %d(%s) unless deps are "
+ "resolved",
+ item->id, item->symbol.c_str());
+
+ continue;
+ }
+
+ process_symbol(task, cache, item.get(), dyn_item);
+
+ if (has_slow) {
+ /* Delay */
+ has_slow = false;
+
+ return false;
+ }
+ }
+ }
+
+ return all_done;
+}
+
+auto symcache_runtime::process_symbol(struct rspamd_task *task, symcache &cache, cache_item *item,
+ cache_dynamic_item *dyn_item) -> bool
+{
+ if (item->type == symcache_item_type::CLASSIFIER || item->type == symcache_item_type::COMPOSITE) {
+ /* Classifiers are special :( */
+ return true;
+ }
+
+ if (rspamd_session_blocked(task->s)) {
+ /*
+ * We cannot add new events as session is either destroyed or
+ * being cleaned up.
+ */
+ return true;
+ }
+
+ g_assert(!item->is_virtual());
+ if (dyn_item->started) {
+ /*
+ * This can actually happen when deps span over different layers
+ */
+ return dyn_item->finished;
+ }
+
+ /* Check has been started */
+ dyn_item->started = true;
+ auto check = true;
+
+ if (!item->is_allowed(task, true) || !item->check_conditions(task)) {
+ check = false;
+ }
+
+ if (check) {
+ msg_debug_cache_task("execute %s, %d; symbol type = %s", item->symbol.data(),
+ item->id, item_type_to_str(item->type));
+
+ if (profile) {
+ ev_now_update_if_cheap(task->event_loop);
+ dyn_item->start_msec = (ev_now(task->event_loop) -
+ profile_start) *
+ 1e3;
+ }
+ dyn_item->async_events = 0;
+ cur_item = dyn_item;
+ items_inflight++;
+ /* Callback now must finalize itself */
+ item->call(task, dyn_item);
+ cur_item = nullptr;
+
+ if (items_inflight == 0) {
+ return true;
+ }
+
+ if (dyn_item->async_events == 0 && !dyn_item->finished) {
+ msg_err_cache_task("critical error: item %s has no async events pending, "
+ "but it is not finalised",
+ item->symbol.data());
+ g_assert_not_reached();
+ }
+
+ return false;
+ }
+ else {
+ dyn_item->finished = true;
+ }
+
+ return true;
+}
+
+auto symcache_runtime::check_metric_limit(struct rspamd_task *task) -> bool
+{
+ if (task->flags & RSPAMD_TASK_FLAG_PASS_ALL) {
+ return false;
+ }
+
+ /* Check score limit */
+ if (!std::isnan(lim)) {
+ if (task->result->score > lim) {
+ return true;
+ }
+ }
+
+ if (task->result->passthrough_result != nullptr) {
+ /* We also need to check passthrough results */
+ auto *pr = task->result->passthrough_result;
+ DL_FOREACH(task->result->passthrough_result, pr)
+ {
+ struct rspamd_action_config *act_config =
+ rspamd_find_action_config_for_action(task->result, pr->action);
+
+ /* Skip least results */
+ if (pr->flags & RSPAMD_PASSTHROUGH_LEAST) {
+ continue;
+ }
+
+ /* Skip disabled actions */
+ if (act_config && (act_config->flags & RSPAMD_ACTION_RESULT_DISABLED)) {
+ continue;
+ }
+
+ /* Immediately stop on non least passthrough action */
+ return true;
+ }
+ }
+
+ return false;
+}
+
+auto symcache_runtime::check_item_deps(struct rspamd_task *task, symcache &cache, cache_item *item,
+ cache_dynamic_item *dyn_item, bool check_only) -> bool
+{
+ constexpr const auto max_recursion = 20;
+ auto log_func = RSPAMD_LOG_FUNC;
+
+ auto inner_functor = [&](int recursion, cache_item *item, cache_dynamic_item *dyn_item, auto rec_functor) -> bool {
+ if (recursion > max_recursion) {
+ msg_err_task_lambda("cyclic dependencies: maximum check level %ud exceed when "
+ "checking dependencies for %s",
+ max_recursion, item->symbol.c_str());
+
+ return true;
+ }
+
+ auto ret = true;
+
+ for (const auto &dep: item->deps) {
+ if (!dep.item) {
+ /* Assume invalid deps as done */
+ msg_debug_cache_task_lambda("symbol %d(%s) has invalid dependencies on %d(%s)",
+ item->id, item->symbol.c_str(), dep.id, dep.sym.c_str());
+ continue;
+ }
+
+ auto *dep_dyn_item = get_dynamic_item(dep.item->id);
+
+ if (!dep_dyn_item->finished) {
+ if (!dep_dyn_item->started) {
+ /* Not started */
+ if (!check_only) {
+ if (!rec_functor(recursion + 1,
+ dep.item,
+ dep_dyn_item,
+ rec_functor)) {
+
+ ret = false;
+ msg_debug_cache_task_lambda("delayed dependency %d(%s) for "
+ "symbol %d(%s)",
+ dep.id, dep.sym.c_str(), item->id, item->symbol.c_str());
+ }
+ else if (!process_symbol(task, cache, dep.item, dep_dyn_item)) {
+ /* Now started, but has events pending */
+ ret = false;
+ msg_debug_cache_task_lambda("started check of %d(%s) symbol "
+ "as dep for "
+ "%d(%s)",
+ dep.id, dep.sym.c_str(), item->id, item->symbol.c_str());
+ }
+ else {
+ msg_debug_cache_task_lambda("dependency %d(%s) for symbol %d(%s) is "
+ "already processed",
+ dep.id, dep.sym.c_str(), item->id, item->symbol.c_str());
+ }
+ }
+ else {
+ msg_debug_cache_task_lambda("dependency %d(%s) for symbol %d(%s) "
+ "cannot be started now",
+ dep.id, dep.sym.c_str(), item->id, item->symbol.c_str());
+ ret = false;
+ }
+ }
+ else {
+ /* Started but not finished */
+ msg_debug_cache_task_lambda("dependency %d(%s) for symbol %d(%s) is "
+ "still executing",
+ dep.id, dep.sym.c_str(), item->id, item->symbol.c_str());
+ ret = false;
+ }
+ }
+ else {
+ msg_debug_cache_task_lambda("dependency %d(%s) for symbol %d(%s) is already "
+ "checked",
+ dep.id, dep.sym.c_str(), item->id, item->symbol.c_str());
+ }
+ }
+
+ return ret;
+ };
+
+ return inner_functor(0, item, dyn_item, inner_functor);
+}
+
+
+struct rspamd_symcache_delayed_cbdata {
+ cache_item *item;
+ struct rspamd_task *task;
+ symcache_runtime *runtime;
+ struct rspamd_async_event *event;
+ struct ev_timer tm;
+};
+
+static void
+rspamd_symcache_delayed_item_fin(gpointer ud)
+{
+ auto *cbd = (struct rspamd_symcache_delayed_cbdata *) ud;
+
+ cbd->event = nullptr;
+ cbd->runtime->unset_slow();
+ ev_timer_stop(cbd->task->event_loop, &cbd->tm);
+}
+
+static void
+rspamd_symcache_delayed_item_cb(EV_P_ ev_timer *w, int what)
+{
+ auto *cbd = (struct rspamd_symcache_delayed_cbdata *) w->data;
+
+ if (cbd->event) {
+ cbd->event = nullptr;
+
+ /* Timer will be stopped here */
+ rspamd_session_remove_event(cbd->task->s,
+ rspamd_symcache_delayed_item_fin, cbd);
+
+ cbd->runtime->process_item_rdeps(cbd->task, cbd->item);
+ }
+}
+
+static void
+rspamd_delayed_timer_dtor(gpointer d)
+{
+ auto *cbd = (struct rspamd_symcache_delayed_cbdata *) d;
+
+ if (cbd->event) {
+ /* Event has not been executed, this will also stop a timer */
+ rspamd_session_remove_event(cbd->task->s,
+ rspamd_symcache_delayed_item_fin, cbd);
+ cbd->event = nullptr;
+ }
+}
+
+auto symcache_runtime::finalize_item(struct rspamd_task *task, cache_dynamic_item *dyn_item) -> void
+{
+ /* Limit to consider a rule as slow (in milliseconds) */
+ constexpr const gdouble slow_diff_limit = 300;
+ auto *item = get_item_by_dynamic_item(dyn_item);
+ /* Sanity checks */
+ g_assert(items_inflight > 0);
+ g_assert(item != nullptr);
+
+ if (dyn_item->async_events > 0) {
+ /*
+ * XXX: Race condition
+ *
+ * It is possible that some async event is still in flight, but we
+ * already know its result, however, it is the responsibility of that
+ * event to decrease async events count and call this function
+ * one more time
+ */
+ msg_debug_cache_task("postpone finalisation of %s(%d) as there are %d "
+ "async events pending",
+ item->symbol.c_str(), item->id, dyn_item->async_events);
+
+ return;
+ }
+
+ msg_debug_cache_task("process finalize for item %s(%d)", item->symbol.c_str(), item->id);
+ dyn_item->finished = true;
+ items_inflight--;
+ cur_item = nullptr;
+
+ auto enable_slow_timer = [&]() -> bool {
+ auto *cbd = rspamd_mempool_alloc0_type(task->task_pool, rspamd_symcache_delayed_cbdata);
+ /* Add timer to allow something else to be executed */
+ ev_timer *tm = &cbd->tm;
+
+ cbd->event = rspamd_session_add_event(task->s,
+ rspamd_symcache_delayed_item_fin, cbd,
+ "symcache");
+ cbd->runtime = this;
+
+ /*
+ * If no event could be added, then we are already in the destruction
+ * phase. So the main issue is to deal with has slow here
+ */
+ if (cbd->event) {
+ ev_timer_init(tm, rspamd_symcache_delayed_item_cb, 0.1, 0.0);
+ ev_set_priority(tm, EV_MINPRI);
+ rspamd_mempool_add_destructor(task->task_pool,
+ rspamd_delayed_timer_dtor, cbd);
+
+ cbd->task = task;
+ cbd->item = item;
+ tm->data = cbd;
+ ev_timer_start(task->event_loop, tm);
+ }
+ else {
+ /* Just reset as no timer is added */
+ has_slow = FALSE;
+ return false;
+ }
+
+ return true;
+ };
+
+ if (profile) {
+ ev_now_update_if_cheap(task->event_loop);
+ auto diff = ((ev_now(task->event_loop) - profile_start) * 1e3 -
+ dyn_item->start_msec);
+
+ if (diff > slow_diff_limit) {
+
+ if (!has_slow) {
+ has_slow = true;
+
+ msg_info_task("slow rule: %s(%d): %.2f ms; enable slow timer delay",
+ item->symbol.c_str(), item->id,
+ diff);
+
+ if (enable_slow_timer()) {
+ /* Allow network execution */
+ return;
+ }
+ }
+ else {
+ msg_info_task("slow rule: %s(%d): %.2f ms",
+ item->symbol.c_str(), item->id,
+ diff);
+ }
+ }
+
+ if (G_UNLIKELY(RSPAMD_TASK_IS_PROFILING(task))) {
+ rspamd_task_profile_set(task, item->symbol.c_str(), diff);
+ }
+
+ if (rspamd_worker_is_scanner(task->worker)) {
+ rspamd_set_counter(item->cd, diff);
+ }
+ }
+
+ process_item_rdeps(task, item);
+}
+
+auto symcache_runtime::process_item_rdeps(struct rspamd_task *task, cache_item *item) -> void
+{
+ auto *cache_ptr = reinterpret_cast<symcache *>(task->cfg->cache);
+
+ // Avoid race condition with the runtime destruction and the delay timer
+ if (!order) {
+ return;
+ }
+
+ for (const auto &rdep: item->rdeps) {
+ if (rdep.item) {
+ auto *dyn_item = get_dynamic_item(rdep.item->id);
+ if (!dyn_item->started) {
+ msg_debug_cache_task("check item %d(%s) rdep of %s ",
+ rdep.item->id, rdep.item->symbol.c_str(), item->symbol.c_str());
+
+ if (!check_item_deps(task, *cache_ptr, rdep.item, dyn_item, false)) {
+ msg_debug_cache_task("blocked execution of %d(%s) rdep of %s "
+ "unless deps are resolved",
+ rdep.item->id, rdep.item->symbol.c_str(), item->symbol.c_str());
+ }
+ else {
+ process_symbol(task, *cache_ptr, rdep.item,
+ dyn_item);
+ }
+ }
+ }
+ }
+}
+
+auto symcache_runtime::get_item_by_dynamic_item(cache_dynamic_item *dyn_item) const -> cache_item *
+{
+ auto idx = dyn_item - dynamic_items;
+
+ if (idx >= 0 && idx < order->size()) {
+ return order->d[idx].get();
+ }
+
+ msg_err("internal error: invalid index to get: %d", (int) idx);
+
+ return nullptr;
+}
+
+}// namespace rspamd::symcache
diff --git a/src/libserver/symcache/symcache_runtime.hxx b/src/libserver/symcache/symcache_runtime.hxx
new file mode 100644
index 0000000..aa8f66c
--- /dev/null
+++ b/src/libserver/symcache/symcache_runtime.hxx
@@ -0,0 +1,209 @@
+/*
+ * 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.
+ */
+
+
+/**
+ * Symcache runtime is produced for each task and it consists of symbols
+ * being executed, being dynamically disabled/enabled and it also captures
+ * the current order of the symbols (produced by resort periodic)
+ */
+
+#ifndef RSPAMD_SYMCACHE_RUNTIME_HXX
+#define RSPAMD_SYMCACHE_RUNTIME_HXX
+#pragma once
+
+#include "symcache_internal.hxx"
+
+struct rspamd_scan_result;
+
+namespace rspamd::symcache {
+/**
+ * These items are saved within task structure and are used to track
+ * symbols execution.
+ * Each symcache item occupies a single dynamic item, that currently has 8 bytes
+ * length
+ */
+struct cache_dynamic_item {
+ std::uint16_t start_msec; /* Relative to task time */
+ bool started;
+ bool finished;
+ std::uint32_t async_events;
+};
+
+static_assert(sizeof(cache_dynamic_item) == sizeof(std::uint64_t));
+static_assert(std::is_trivial_v<cache_dynamic_item>);
+
+class symcache_runtime {
+ unsigned items_inflight;
+ bool profile;
+ bool has_slow;
+
+ double profile_start;
+ double lim;
+
+ struct cache_dynamic_item *cur_item;
+ order_generation_ptr order;
+ /* Dynamically expanded as needed */
+ mutable struct cache_dynamic_item dynamic_items[];
+ /* We allocate this structure merely in memory pool, so destructor is absent */
+ ~symcache_runtime() = delete;
+
+ auto process_symbol(struct rspamd_task *task, symcache &cache, cache_item *item,
+ cache_dynamic_item *dyn_item) -> bool;
+ /* Specific stages of the processing */
+ auto process_pre_postfilters(struct rspamd_task *task, symcache &cache, int start_events, unsigned int stage) -> bool;
+ auto process_filters(struct rspamd_task *task, symcache &cache, int start_events) -> bool;
+ auto check_metric_limit(struct rspamd_task *task) -> bool;
+ auto check_item_deps(struct rspamd_task *task, symcache &cache, cache_item *item,
+ cache_dynamic_item *dyn_item, bool check_only) -> bool;
+
+public:
+ /* Dropper for a shared ownership */
+ auto savepoint_dtor() -> void
+ {
+
+ /* Drop shared ownership */
+ order.reset();
+ }
+ /**
+ * Creates a cache runtime using task mempool
+ * @param task
+ * @param cache
+ * @return
+ */
+ static auto create(struct rspamd_task *task, symcache &cache) -> symcache_runtime *;
+ /**
+ * Process task settings
+ * @param task
+ * @return
+ */
+ auto process_settings(struct rspamd_task *task, const symcache &cache) -> bool;
+
+ /**
+ * Disable all symbols but not touching ones that are in the specific mask
+ * @param skip_mask
+ */
+ auto disable_all_symbols(int skip_mask) -> void;
+
+ /**
+ * Disable a symbol (or it's parent)
+ * @param name
+ * @return
+ */
+ auto disable_symbol(struct rspamd_task *task, const symcache &cache, std::string_view name) -> bool;
+
+ /**
+ * Enable a symbol (or it's parent)
+ * @param name
+ * @return
+ */
+ auto enable_symbol(struct rspamd_task *task, const symcache &cache, std::string_view name) -> bool;
+
+ /**
+ * Checks if an item has been checked/disabled
+ * @param cache
+ * @param name
+ * @return
+ */
+ auto is_symbol_checked(const symcache &cache, std::string_view name) -> bool;
+
+ /**
+ * Checks if a symbol is enabled for execution, checking all pending conditions
+ * @param task
+ * @param cache
+ * @param name
+ * @return
+ */
+ auto is_symbol_enabled(struct rspamd_task *task, const symcache &cache, std::string_view name) -> bool;
+
+ /**
+ * Get the current processed item
+ * @return
+ */
+ auto get_cur_item() const -> auto
+ {
+ return cur_item;
+ }
+
+ /**
+ * Set the current processed item
+ * @param item
+ * @return
+ */
+ auto set_cur_item(cache_dynamic_item *item) -> auto
+ {
+ std::swap(item, cur_item);
+ return item;
+ }
+
+ /**
+ * Set profile mode for the runtime
+ * @param enable
+ * @return
+ */
+ auto set_profile_mode(bool enable) -> auto
+ {
+ std::swap(profile, enable);
+ return enable;
+ }
+
+ /**
+ * Returns the dynamic item by static item id
+ * @param id
+ * @return
+ */
+ auto get_dynamic_item(int id) const -> cache_dynamic_item *;
+
+ /**
+ * Returns static cache item by dynamic cache item
+ * @return
+ */
+ auto get_item_by_dynamic_item(cache_dynamic_item *) const -> cache_item *;
+
+ /**
+ * Process symbols in the cache
+ * @param task
+ * @param cache
+ * @param stage
+ * @return
+ */
+ auto process_symbols(struct rspamd_task *task, symcache &cache, unsigned int stage) -> bool;
+
+ /**
+ * Finalize execution of some item in the cache
+ * @param task
+ * @param item
+ */
+ auto finalize_item(struct rspamd_task *task, cache_dynamic_item *item) -> void;
+
+ /**
+ * Process unblocked reverse dependencies of the specific item
+ * @param task
+ * @param item
+ */
+ auto process_item_rdeps(struct rspamd_task *task, cache_item *item) -> void;
+
+ /* XXX: a helper to allow hiding internal implementation of the slow timer structure */
+ auto unset_slow() -> void
+ {
+ has_slow = false;
+ }
+};
+
+
+}// namespace rspamd::symcache
+
+#endif//RSPAMD_SYMCACHE_RUNTIME_HXX
diff --git a/src/libserver/task.c b/src/libserver/task.c
new file mode 100644
index 0000000..9763d1e
--- /dev/null
+++ b/src/libserver/task.c
@@ -0,0 +1,1975 @@
+/*
+ * 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 "task.h"
+#include "rspamd.h"
+#include "scan_result.h"
+#include "libserver/protocol.h"
+#include "libserver/protocol_internal.h"
+#include "message.h"
+#include "lua/lua_common.h"
+#include "email_addr.h"
+#include "src/libserver/composites/composites.h"
+#include "stat_api.h"
+#include "unix-std.h"
+#include "utlist.h"
+#include "libserver/mempool_vars_internal.h"
+#include "libserver/cfg_file_private.h"
+#include "libmime/lang_detection.h"
+#include "libmime/scan_result_private.h"
+
+#ifdef WITH_JEMALLOC
+#include <jemalloc/jemalloc.h>
+#else
+#if defined(__GLIBC__) && defined(_GNU_SOURCE)
+#include <malloc.h>
+#endif
+#endif
+
+#include <math.h>
+
+#ifdef SYS_ZSTD
+#include "zstd.h"
+#else
+#include "contrib/zstd/zstd.h"
+#endif
+
+__KHASH_IMPL(rspamd_req_headers_hash, static inline,
+ rspamd_ftok_t *, struct rspamd_request_header_chain *, 1,
+ rspamd_ftok_icase_hash, rspamd_ftok_icase_equal)
+
+static GQuark
+rspamd_task_quark(void)
+{
+ return g_quark_from_static_string("task-error");
+}
+
+/*
+ * Create new task
+ */
+struct rspamd_task *
+rspamd_task_new(struct rspamd_worker *worker,
+ struct rspamd_config *cfg,
+ rspamd_mempool_t *pool,
+ struct rspamd_lang_detector *lang_det,
+ struct ev_loop *event_loop,
+ gboolean debug_mem)
+{
+ struct rspamd_task *new_task;
+ rspamd_mempool_t *task_pool;
+ guint flags = 0;
+
+ if (pool == NULL) {
+ task_pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ "task", debug_mem ? RSPAMD_MEMPOOL_DEBUG : 0);
+ flags |= RSPAMD_TASK_FLAG_OWN_POOL;
+ }
+ else {
+ task_pool = pool;
+ }
+
+ new_task = rspamd_mempool_alloc0(task_pool, sizeof(struct rspamd_task));
+ new_task->task_pool = task_pool;
+ new_task->flags = flags;
+ new_task->worker = worker;
+ new_task->lang_det = lang_det;
+
+ if (cfg) {
+ new_task->cfg = cfg;
+ REF_RETAIN(cfg);
+
+ if (cfg->check_all_filters) {
+ new_task->flags |= RSPAMD_TASK_FLAG_PASS_ALL;
+ }
+
+
+ if (cfg->re_cache) {
+ new_task->re_rt = rspamd_re_cache_runtime_new(cfg->re_cache);
+ }
+
+ if (new_task->lang_det == NULL && cfg->lang_det != NULL) {
+ new_task->lang_det = cfg->lang_det;
+ }
+ }
+
+ new_task->event_loop = event_loop;
+ new_task->task_timestamp = ev_time();
+ new_task->time_real_finish = NAN;
+
+ new_task->request_headers = kh_init(rspamd_req_headers_hash);
+ new_task->sock = -1;
+ new_task->flags |= (RSPAMD_TASK_FLAG_MIME);
+ /* Default results chain */
+ rspamd_create_metric_result(new_task, NULL, -1);
+
+ new_task->queue_id = "undef";
+ new_task->messages = ucl_object_typed_new(UCL_OBJECT);
+ kh_static_init(rspamd_task_lua_cache, &new_task->lua_cache);
+
+ return new_task;
+}
+
+
+static void
+rspamd_task_reply(struct rspamd_task *task)
+{
+ const ev_tstamp write_timeout = 5.0;
+
+ if (task->fin_callback) {
+ task->fin_callback(task, task->fin_arg);
+ }
+ else {
+ if (!(task->processed_stages & RSPAMD_TASK_STAGE_REPLIED)) {
+ rspamd_protocol_write_reply(task, write_timeout);
+ }
+ }
+}
+
+/*
+ * Called if all filters are processed
+ * @return TRUE if session should be terminated
+ */
+gboolean
+rspamd_task_fin(void *arg)
+{
+ struct rspamd_task *task = (struct rspamd_task *) arg;
+
+ /* Task is already finished or skipped */
+ if (RSPAMD_TASK_IS_PROCESSED(task)) {
+ rspamd_task_reply(task);
+ return TRUE;
+ }
+
+ if (!rspamd_task_process(task, RSPAMD_TASK_PROCESS_ALL)) {
+ rspamd_task_reply(task);
+ return TRUE;
+ }
+
+ if (RSPAMD_TASK_IS_PROCESSED(task)) {
+ rspamd_task_reply(task);
+ return TRUE;
+ }
+
+ /* One more iteration */
+ return FALSE;
+}
+
+/*
+ * Free all structures of worker_task
+ */
+void rspamd_task_free(struct rspamd_task *task)
+{
+ struct rspamd_email_address *addr;
+ static guint free_iters = 0;
+ guint i;
+
+ if (task) {
+ debug_task("free pointer %p", task);
+
+ if (task->rcpt_envelope) {
+ for (i = 0; i < task->rcpt_envelope->len; i++) {
+ addr = g_ptr_array_index(task->rcpt_envelope, i);
+ rspamd_email_address_free(addr);
+ }
+
+ g_ptr_array_free(task->rcpt_envelope, TRUE);
+ }
+
+ if (task->from_envelope) {
+ rspamd_email_address_free(task->from_envelope);
+ }
+
+ if (task->from_envelope_orig) {
+ rspamd_email_address_free(task->from_envelope_orig);
+ }
+
+ if (task->meta_words) {
+ g_array_free(task->meta_words, TRUE);
+ }
+
+ ucl_object_unref(task->messages);
+
+ if (task->re_rt) {
+ rspamd_re_cache_runtime_destroy(task->re_rt);
+ }
+
+ if (task->http_conn != NULL) {
+ rspamd_http_connection_reset(task->http_conn);
+ rspamd_http_connection_unref(task->http_conn);
+ }
+
+ if (task->settings != NULL) {
+ ucl_object_unref(task->settings);
+ }
+
+ if (task->settings_elt != NULL) {
+ REF_RELEASE(task->settings_elt);
+ }
+
+ if (task->client_addr) {
+ rspamd_inet_address_free(task->client_addr);
+ }
+
+ if (task->from_addr) {
+ rspamd_inet_address_free(task->from_addr);
+ }
+
+ if (task->err) {
+ g_error_free(task->err);
+ }
+
+ ev_timer_stop(task->event_loop, &task->timeout_ev);
+ ev_io_stop(task->event_loop, &task->guard_ev);
+
+ if (task->sock != -1) {
+ close(task->sock);
+ }
+
+ if (task->cfg) {
+
+
+ struct rspamd_lua_cached_entry entry;
+
+ kh_foreach_value(&task->lua_cache, entry, {
+ luaL_unref(task->cfg->lua_state,
+ LUA_REGISTRYINDEX, entry.ref);
+ });
+ kh_static_destroy(rspamd_task_lua_cache, &task->lua_cache);
+
+ if (task->cfg->full_gc_iters && (++free_iters > task->cfg->full_gc_iters)) {
+ /* Perform more expensive cleanup cycle */
+ gsize allocated = 0, active = 0, metadata = 0,
+ resident = 0, mapped = 0, old_lua_mem = 0;
+ gdouble t1, t2;
+
+ old_lua_mem = lua_gc(task->cfg->lua_state, LUA_GCCOUNT, 0);
+ t1 = rspamd_get_ticks(FALSE);
+
+#ifdef WITH_JEMALLOC
+ gsize sz = sizeof(gsize);
+ mallctl("stats.allocated", &allocated, &sz, NULL, 0);
+ mallctl("stats.active", &active, &sz, NULL, 0);
+ mallctl("stats.metadata", &metadata, &sz, NULL, 0);
+ mallctl("stats.resident", &resident, &sz, NULL, 0);
+ mallctl("stats.mapped", &mapped, &sz, NULL, 0);
+#else
+#if defined(__GLIBC__) && defined(_GNU_SOURCE)
+ malloc_trim(0);
+#endif
+#endif
+ lua_gc(task->cfg->lua_state, LUA_GCCOLLECT, 0);
+ t2 = rspamd_get_ticks(FALSE);
+
+ msg_notice_task("perform full gc cycle; memory stats: "
+ "%Hz allocated, %Hz active, %Hz metadata, %Hz resident, %Hz mapped;"
+ " lua memory: %z kb -> %d kb; %f ms for gc iter",
+ allocated, active, metadata, resident, mapped,
+ old_lua_mem, lua_gc(task->cfg->lua_state, LUA_GCCOUNT, 0),
+ (t2 - t1) * 1000.0);
+ free_iters = rspamd_time_jitter(0,
+ (gdouble) task->cfg->full_gc_iters / 2);
+ }
+
+ REF_RELEASE(task->cfg);
+ }
+
+ kh_destroy(rspamd_req_headers_hash, task->request_headers);
+ rspamd_message_unref(task->message);
+
+ if (task->flags & RSPAMD_TASK_FLAG_OWN_POOL) {
+ rspamd_mempool_destructors_enforce(task->task_pool);
+
+ if (task->symcache_runtime) {
+ rspamd_symcache_runtime_destroy(task);
+ }
+
+ rspamd_mempool_delete(task->task_pool);
+ }
+ else if (task->symcache_runtime) {
+ rspamd_symcache_runtime_destroy(task);
+ }
+ }
+}
+
+struct rspamd_task_map {
+ gpointer begin;
+ gulong len;
+ gint fd;
+};
+
+static void
+rspamd_task_unmapper(gpointer ud)
+{
+ struct rspamd_task_map *m = ud;
+
+ munmap(m->begin, m->len);
+ close(m->fd);
+}
+
+gboolean
+rspamd_task_load_message(struct rspamd_task *task,
+ struct rspamd_http_message *msg, const gchar *start, gsize len)
+{
+ guint control_len, r;
+ struct ucl_parser *parser;
+ ucl_object_t *control_obj;
+ gchar filepath[PATH_MAX], *fp;
+ gint fd, flen;
+ gulong offset = 0, shmem_size = 0;
+ rspamd_ftok_t *tok;
+ gpointer map;
+ struct stat st;
+ struct rspamd_task_map *m;
+ const gchar *ft;
+
+#ifdef HAVE_SANE_SHMEM
+ ft = "shm";
+#else
+ ft = "file";
+#endif
+
+ if (msg) {
+ rspamd_protocol_handle_headers(task, msg);
+ }
+
+ tok = rspamd_task_get_request_header(task, "shm");
+
+ if (tok) {
+ /* Shared memory part */
+ r = rspamd_strlcpy(filepath, tok->begin,
+ MIN(sizeof(filepath), tok->len + 1));
+
+ rspamd_url_decode(filepath, filepath, r + 1);
+ flen = strlen(filepath);
+
+ if (filepath[0] == '"' && flen > 2) {
+ /* We need to unquote filepath */
+ fp = &filepath[1];
+ fp[flen - 2] = '\0';
+ }
+ else {
+ fp = &filepath[0];
+ }
+#ifdef HAVE_SANE_SHMEM
+ fd = shm_open(fp, O_RDONLY, 00600);
+#else
+ fd = open(fp, O_RDONLY, 00600);
+#endif
+ if (fd == -1) {
+ g_set_error(&task->err, rspamd_task_quark(), RSPAMD_PROTOCOL_ERROR,
+ "Cannot open %s segment (%s): %s", ft, fp, strerror(errno));
+ return FALSE;
+ }
+
+ if (fstat(fd, &st) == -1) {
+ g_set_error(&task->err, rspamd_task_quark(), RSPAMD_PROTOCOL_ERROR,
+ "Cannot stat %s segment (%s): %s", ft, fp, strerror(errno));
+ close(fd);
+
+ return FALSE;
+ }
+
+ map = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
+
+ if (map == MAP_FAILED) {
+ close(fd);
+ g_set_error(&task->err, rspamd_task_quark(), RSPAMD_PROTOCOL_ERROR,
+ "Cannot mmap %s (%s): %s", ft, fp, strerror(errno));
+ return FALSE;
+ }
+
+ tok = rspamd_task_get_request_header(task, "shm-offset");
+
+ if (tok) {
+ rspamd_strtoul(tok->begin, tok->len, &offset);
+
+ if (offset > (gulong) st.st_size) {
+ msg_err_task("invalid offset %ul (%ul available) for shm "
+ "segment %s",
+ offset, (gulong) st.st_size, fp);
+ munmap(map, st.st_size);
+ close(fd);
+
+ return FALSE;
+ }
+ }
+
+ tok = rspamd_task_get_request_header(task, "shm-length");
+ shmem_size = st.st_size;
+
+
+ if (tok) {
+ rspamd_strtoul(tok->begin, tok->len, &shmem_size);
+
+ if (shmem_size > (gulong) st.st_size) {
+ msg_err_task("invalid length %ul (%ul available) for %s "
+ "segment %s",
+ shmem_size, (gulong) st.st_size, ft, fp);
+ munmap(map, st.st_size);
+ close(fd);
+
+ return FALSE;
+ }
+ }
+
+ task->msg.begin = ((guchar *) map) + offset;
+ task->msg.len = shmem_size;
+ m = rspamd_mempool_alloc(task->task_pool, sizeof(*m));
+ m->begin = map;
+ m->len = st.st_size;
+ m->fd = fd;
+
+ msg_info_task("loaded message from shared memory %s (%ul size, %ul offset), fd=%d",
+ fp, shmem_size, offset, fd);
+
+ rspamd_mempool_add_destructor(task->task_pool, rspamd_task_unmapper, m);
+
+ return TRUE;
+ }
+
+ tok = rspamd_task_get_request_header(task, "file");
+
+ if (tok == NULL) {
+ tok = rspamd_task_get_request_header(task, "path");
+ }
+
+ if (tok) {
+ debug_task("want to scan file %T", tok);
+
+ r = rspamd_strlcpy(filepath, tok->begin,
+ MIN(sizeof(filepath), tok->len + 1));
+
+ rspamd_url_decode(filepath, filepath, r + 1);
+ flen = strlen(filepath);
+
+ if (filepath[0] == '"' && flen > 2) {
+ /* We need to unquote filepath */
+ fp = &filepath[1];
+ fp[flen - 2] = '\0';
+ }
+ else {
+ fp = &filepath[0];
+ }
+
+ if (stat(fp, &st) == -1) {
+ g_set_error(&task->err, rspamd_task_quark(), RSPAMD_PROTOCOL_ERROR,
+ "Invalid file (%s): %s", fp, strerror(errno));
+ return FALSE;
+ }
+
+ if (G_UNLIKELY(st.st_size == 0)) {
+ /* Empty file */
+ task->flags |= RSPAMD_TASK_FLAG_EMPTY;
+ task->msg.begin = rspamd_mempool_strdup(task->task_pool, "");
+ task->msg.len = 0;
+ }
+ else {
+ fd = open(fp, O_RDONLY);
+
+ if (fd == -1) {
+ g_set_error(&task->err, rspamd_task_quark(),
+ RSPAMD_PROTOCOL_ERROR,
+ "Cannot open file (%s): %s", fp, strerror(errno));
+ return FALSE;
+ }
+
+ map = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
+
+
+ if (map == MAP_FAILED) {
+ close(fd);
+ g_set_error(&task->err, rspamd_task_quark(),
+ RSPAMD_PROTOCOL_ERROR,
+ "Cannot mmap file (%s): %s", fp, strerror(errno));
+ return FALSE;
+ }
+
+ task->msg.begin = map;
+ task->msg.len = st.st_size;
+ m = rspamd_mempool_alloc(task->task_pool, sizeof(*m));
+ m->begin = map;
+ m->len = st.st_size;
+ m->fd = fd;
+
+ rspamd_mempool_add_destructor(task->task_pool, rspamd_task_unmapper, m);
+ }
+
+ task->msg.fpath = rspamd_mempool_strdup(task->task_pool, fp);
+ task->flags |= RSPAMD_TASK_FLAG_FILE;
+
+ msg_info_task("loaded message from file %s", fp);
+
+ return TRUE;
+ }
+
+ /* Plain data */
+ debug_task("got input of length %z", task->msg.len);
+
+ /* Check compression */
+ tok = rspamd_task_get_request_header(task, "compression");
+
+ if (tok) {
+ /* Need to uncompress */
+ rspamd_ftok_t t;
+
+ t.begin = "zstd";
+ t.len = 4;
+
+ if (rspamd_ftok_casecmp(tok, &t) == 0) {
+ ZSTD_DStream *zstream;
+ ZSTD_inBuffer zin;
+ ZSTD_outBuffer zout;
+ guchar *out;
+ gsize outlen, r;
+ gulong dict_id;
+
+ if (!rspamd_libs_reset_decompression(task->cfg->libs_ctx)) {
+ g_set_error(&task->err, rspamd_task_quark(),
+ RSPAMD_PROTOCOL_ERROR,
+ "Cannot decompress, decompressor init failed");
+
+ return FALSE;
+ }
+
+ tok = rspamd_task_get_request_header(task, "dictionary");
+
+ if (tok != NULL) {
+ /* We need to use custom dictionary */
+ if (!rspamd_strtoul(tok->begin, tok->len, &dict_id)) {
+ g_set_error(&task->err, rspamd_task_quark(), RSPAMD_PROTOCOL_ERROR,
+ "Non numeric dictionary");
+
+ return FALSE;
+ }
+
+ if (!task->cfg->libs_ctx->in_dict) {
+ g_set_error(&task->err, rspamd_task_quark(), RSPAMD_PROTOCOL_ERROR,
+ "Unknown dictionary, undefined locally");
+
+ return FALSE;
+ }
+
+ if (task->cfg->libs_ctx->in_dict->id != dict_id) {
+ g_set_error(&task->err, rspamd_task_quark(), RSPAMD_PROTOCOL_ERROR,
+ "Unknown dictionary, invalid dictionary id");
+
+ return FALSE;
+ }
+ }
+
+ zstream = task->cfg->libs_ctx->in_zstream;
+
+ zin.pos = 0;
+ zin.src = start;
+ zin.size = len;
+
+ if ((outlen = ZSTD_getDecompressedSize(start, len)) == 0) {
+ outlen = ZSTD_DStreamOutSize();
+ }
+
+ out = g_malloc(outlen);
+ zout.dst = out;
+ zout.pos = 0;
+ zout.size = outlen;
+
+ while (zin.pos < zin.size) {
+ r = ZSTD_decompressStream(zstream, &zout, &zin);
+
+ if (ZSTD_isError(r)) {
+ g_set_error(&task->err, rspamd_task_quark(),
+ RSPAMD_PROTOCOL_ERROR,
+ "Decompression error: %s", ZSTD_getErrorName(r));
+
+ return FALSE;
+ }
+
+ if (zout.pos == zout.size) {
+ /* We need to extend output buffer */
+ zout.size = zout.size * 2 + 1;
+ zout.dst = g_realloc(zout.dst, zout.size);
+ }
+ }
+
+ rspamd_mempool_add_destructor(task->task_pool, g_free, zout.dst);
+ task->msg.begin = zout.dst;
+ task->msg.len = zout.pos;
+ task->protocol_flags |= RSPAMD_TASK_PROTOCOL_FLAG_COMPRESSED;
+
+ msg_info_task("loaded message from zstd compressed stream; "
+ "compressed: %ul; uncompressed: %ul",
+ (gulong) zin.size, (gulong) zout.pos);
+ }
+ else {
+ g_set_error(&task->err, rspamd_task_quark(), RSPAMD_PROTOCOL_ERROR,
+ "Invalid compression method");
+ return FALSE;
+ }
+ }
+ else {
+ task->msg.begin = start;
+ task->msg.len = len;
+ }
+
+ if (task->msg.len == 0) {
+ task->flags |= RSPAMD_TASK_FLAG_EMPTY;
+ }
+
+ if (task->protocol_flags & RSPAMD_TASK_PROTOCOL_FLAG_HAS_CONTROL) {
+ rspamd_ftok_t *hv = rspamd_task_get_request_header(task, MLEN_HEADER);
+ gulong message_len = 0;
+
+ if (!hv || !rspamd_strtoul(hv->begin, hv->len, &message_len) ||
+ task->msg.len < message_len) {
+ msg_warn_task("message has invalid message length: %ul and total len: %ul",
+ message_len, task->msg.len);
+ g_set_error(&task->err, rspamd_task_quark(), RSPAMD_PROTOCOL_ERROR,
+ "Invalid length");
+ return FALSE;
+ }
+
+ control_len = task->msg.len - message_len;
+
+ if (control_len > 0) {
+ parser = ucl_parser_new(UCL_PARSER_KEY_LOWERCASE);
+
+ if (!ucl_parser_add_chunk(parser, task->msg.begin, control_len)) {
+ msg_warn_task("processing of control chunk failed: %s",
+ ucl_parser_get_error(parser));
+ ucl_parser_free(parser);
+ }
+ else {
+ control_obj = ucl_parser_get_object(parser);
+ ucl_parser_free(parser);
+ rspamd_protocol_handle_control(task, control_obj);
+ ucl_object_unref(control_obj);
+ }
+
+ task->msg.begin += control_len;
+ task->msg.len -= control_len;
+ }
+ }
+
+ return TRUE;
+}
+
+static guint
+rspamd_task_select_processing_stage(struct rspamd_task *task, guint stages)
+{
+ guint st, mask;
+
+ mask = task->processed_stages;
+
+ if (mask == 0) {
+ st = 0;
+ }
+ else {
+ for (st = 1; mask != 1; st++) {
+ mask = mask >> 1u;
+ }
+ }
+
+ st = 1 << st;
+
+ if (stages & st) {
+ return st;
+ }
+ else if (st < RSPAMD_TASK_STAGE_DONE) {
+ /* We assume that the stage that was not requested is done */
+ task->processed_stages |= st;
+ return rspamd_task_select_processing_stage(task, stages);
+ }
+
+ /* We are done */
+ return RSPAMD_TASK_STAGE_DONE;
+}
+
+gboolean
+rspamd_task_process(struct rspamd_task *task, guint stages)
+{
+ guint st;
+ gboolean ret = TRUE, all_done = TRUE;
+ GError *stat_error = NULL;
+
+ /* Avoid nested calls */
+ if (task->flags & RSPAMD_TASK_FLAG_PROCESSING) {
+ return TRUE;
+ }
+
+ if (RSPAMD_TASK_IS_PROCESSED(task)) {
+ return TRUE;
+ }
+
+ task->flags |= RSPAMD_TASK_FLAG_PROCESSING;
+
+ st = rspamd_task_select_processing_stage(task, stages);
+
+ switch (st) {
+ case RSPAMD_TASK_STAGE_CONNFILTERS:
+ all_done = rspamd_symcache_process_symbols(task, task->cfg->cache, st);
+ break;
+
+ case RSPAMD_TASK_STAGE_READ_MESSAGE:
+ if (!rspamd_message_parse(task)) {
+ ret = FALSE;
+ }
+ break;
+
+ case RSPAMD_TASK_STAGE_PROCESS_MESSAGE:
+ if (!(task->flags & RSPAMD_TASK_FLAG_SKIP_PROCESS)) {
+ rspamd_message_process(task);
+ }
+ break;
+
+ case RSPAMD_TASK_STAGE_PRE_FILTERS:
+ case RSPAMD_TASK_STAGE_FILTERS:
+ all_done = rspamd_symcache_process_symbols(task, task->cfg->cache, st);
+ break;
+
+ case RSPAMD_TASK_STAGE_CLASSIFIERS:
+ case RSPAMD_TASK_STAGE_CLASSIFIERS_PRE:
+ case RSPAMD_TASK_STAGE_CLASSIFIERS_POST:
+ if (!RSPAMD_TASK_IS_EMPTY(task)) {
+ if (rspamd_stat_classify(task, task->cfg->lua_state, st, &stat_error) ==
+ RSPAMD_STAT_PROCESS_ERROR) {
+ msg_err_task("classify error: %e", stat_error);
+ g_error_free(stat_error);
+ }
+ }
+ break;
+
+ case RSPAMD_TASK_STAGE_COMPOSITES:
+ rspamd_composites_process_task(task);
+ task->result->nresults_postfilters = task->result->nresults;
+ break;
+
+ case RSPAMD_TASK_STAGE_POST_FILTERS:
+ all_done = rspamd_symcache_process_symbols(task, task->cfg->cache,
+ st);
+
+ if (all_done && (task->flags & RSPAMD_TASK_FLAG_LEARN_AUTO) &&
+ !RSPAMD_TASK_IS_EMPTY(task) &&
+ !(task->flags & (RSPAMD_TASK_FLAG_LEARN_SPAM | RSPAMD_TASK_FLAG_LEARN_HAM))) {
+ rspamd_stat_check_autolearn(task);
+ }
+ break;
+
+ case RSPAMD_TASK_STAGE_LEARN:
+ case RSPAMD_TASK_STAGE_LEARN_PRE:
+ case RSPAMD_TASK_STAGE_LEARN_POST:
+ if (task->flags & (RSPAMD_TASK_FLAG_LEARN_SPAM | RSPAMD_TASK_FLAG_LEARN_HAM)) {
+ if (task->err == NULL) {
+ if (!rspamd_stat_learn(task,
+ task->flags & RSPAMD_TASK_FLAG_LEARN_SPAM,
+ task->cfg->lua_state, task->classifier,
+ st, &stat_error)) {
+
+ if (stat_error == NULL) {
+ g_set_error(&stat_error,
+ g_quark_from_static_string("stat"), 500,
+ "Unknown statistics error, found on stage %s;"
+ " classifier: %s",
+ rspamd_task_stage_name(st), task->classifier);
+ }
+
+ if (stat_error->code >= 400) {
+ msg_err_task("learn error: %e", stat_error);
+ }
+ else {
+ msg_notice_task("skip learning: %e", stat_error);
+ }
+
+ if (!(task->flags & RSPAMD_TASK_FLAG_LEARN_AUTO)) {
+ task->err = stat_error;
+ task->processed_stages |= RSPAMD_TASK_STAGE_DONE;
+ }
+ else {
+ /* Do not skip idempotent in case of learn error */
+ if (stat_error) {
+ g_error_free(stat_error);
+ }
+
+ task->processed_stages |= RSPAMD_TASK_STAGE_LEARN |
+ RSPAMD_TASK_STAGE_LEARN_PRE |
+ RSPAMD_TASK_STAGE_LEARN_POST;
+ }
+ }
+ }
+ }
+ break;
+ case RSPAMD_TASK_STAGE_COMPOSITES_POST:
+ /* Second run of composites processing before idempotent filters (if needed) */
+ if (task->result->nresults_postfilters != task->result->nresults) {
+ rspamd_composites_process_task(task);
+ }
+ else {
+ msg_debug_task("skip second run of composites as the result has not been changed");
+ }
+ break;
+
+ case RSPAMD_TASK_STAGE_IDEMPOTENT:
+ /* Stop task timeout */
+ if (ev_can_stop(&task->timeout_ev)) {
+ ev_timer_stop(task->event_loop, &task->timeout_ev);
+ }
+
+ all_done = rspamd_symcache_process_symbols(task, task->cfg->cache, st);
+ break;
+
+ case RSPAMD_TASK_STAGE_DONE:
+ task->processed_stages |= RSPAMD_TASK_STAGE_DONE;
+ break;
+
+ default:
+ /* TODO: not implemented stage */
+ break;
+ }
+
+ if (RSPAMD_TASK_IS_SKIPPED(task)) {
+ /* Set all bits except idempotent filters */
+ task->processed_stages |= 0x7FFF;
+ }
+
+ task->flags &= ~RSPAMD_TASK_FLAG_PROCESSING;
+
+ if (!ret || RSPAMD_TASK_IS_PROCESSED(task)) {
+ if (!ret) {
+ /* Set processed flags */
+ task->processed_stages |= RSPAMD_TASK_STAGE_DONE;
+ }
+
+ msg_debug_task("task is processed");
+
+ return ret;
+ }
+
+ if (ret) {
+ if (rspamd_session_events_pending(task->s) != 0) {
+ /* We have events pending, so we consider this stage as incomplete */
+ msg_debug_task("need more work on stage %d", st);
+ }
+ else {
+ if (all_done) {
+ /* Mark the current stage as done and go to the next stage */
+ msg_debug_task("completed stage %d", st);
+ task->processed_stages |= st;
+ }
+ else {
+ msg_debug_task("need more processing on stage %d", st);
+ }
+
+ /* Tail recursion */
+ return rspamd_task_process(task, stages);
+ }
+ }
+
+ return ret;
+}
+
+struct rspamd_email_address *
+rspamd_task_get_sender(struct rspamd_task *task)
+{
+ return task->from_envelope;
+}
+
+static const gchar *
+rspamd_task_cache_principal_recipient(struct rspamd_task *task,
+ const gchar *rcpt, gsize len)
+{
+ gchar *rcpt_lc;
+
+ if (rcpt == NULL) {
+ return NULL;
+ }
+
+ rcpt_lc = rspamd_mempool_alloc(task->task_pool, len + 1);
+ rspamd_strlcpy(rcpt_lc, rcpt, len + 1);
+ rspamd_str_lc(rcpt_lc, len);
+
+ rspamd_mempool_set_variable(task->task_pool,
+ RSPAMD_MEMPOOL_PRINCIPAL_RECIPIENT, rcpt_lc, NULL);
+
+ return rcpt_lc;
+}
+
+const gchar *
+rspamd_task_get_principal_recipient(struct rspamd_task *task)
+{
+ const gchar *val;
+ struct rspamd_email_address *addr;
+ guint i;
+
+ val = rspamd_mempool_get_variable(task->task_pool,
+ RSPAMD_MEMPOOL_PRINCIPAL_RECIPIENT);
+
+ if (val) {
+ return val;
+ }
+
+ if (task->deliver_to) {
+ return rspamd_task_cache_principal_recipient(task, task->deliver_to,
+ strlen(task->deliver_to));
+ }
+ if (task->rcpt_envelope != NULL) {
+
+ PTR_ARRAY_FOREACH(task->rcpt_envelope, i, addr)
+ {
+ if (addr->addr && !(addr->flags & RSPAMD_EMAIL_ADDR_ORIGINAL)) {
+ return rspamd_task_cache_principal_recipient(task, addr->addr,
+ addr->addr_len);
+ }
+ }
+ }
+
+ GPtrArray *rcpt_mime = MESSAGE_FIELD_CHECK(task, rcpt_mime);
+ if (rcpt_mime != NULL && rcpt_mime->len > 0) {
+ PTR_ARRAY_FOREACH(rcpt_mime, i, addr)
+ {
+ if (addr->addr && !(addr->flags & RSPAMD_EMAIL_ADDR_ORIGINAL)) {
+ return rspamd_task_cache_principal_recipient(task, addr->addr,
+ addr->addr_len);
+ }
+ }
+ }
+
+ return NULL;
+}
+
+gboolean
+rspamd_learn_task_spam(struct rspamd_task *task,
+ gboolean is_spam,
+ const gchar *classifier,
+ GError **err)
+{
+ if (is_spam) {
+ task->flags |= RSPAMD_TASK_FLAG_LEARN_SPAM;
+ }
+ else {
+ task->flags |= RSPAMD_TASK_FLAG_LEARN_HAM;
+ }
+
+ task->classifier = classifier;
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_task_log_check_condition(struct rspamd_task *task,
+ struct rspamd_log_format *lf)
+{
+ gboolean ret = FALSE;
+
+ switch (lf->type) {
+ case RSPAMD_LOG_MID:
+ if (MESSAGE_FIELD_CHECK(task, message_id) &&
+ strcmp(MESSAGE_FIELD(task, message_id), "undef") != 0) {
+ ret = TRUE;
+ }
+ break;
+ case RSPAMD_LOG_QID:
+ if (task->queue_id && strcmp(task->queue_id, "undef") != 0) {
+ ret = TRUE;
+ }
+ break;
+ case RSPAMD_LOG_USER:
+ if (task->auth_user) {
+ ret = TRUE;
+ }
+ break;
+ case RSPAMD_LOG_IP:
+ if (task->from_addr && rspamd_ip_is_valid(task->from_addr)) {
+ ret = TRUE;
+ }
+ break;
+ case RSPAMD_LOG_SMTP_RCPT:
+ case RSPAMD_LOG_SMTP_RCPTS:
+ if (task->rcpt_envelope && task->rcpt_envelope->len > 0) {
+ ret = TRUE;
+ }
+ break;
+ case RSPAMD_LOG_MIME_RCPT:
+ case RSPAMD_LOG_MIME_RCPTS:
+ if (MESSAGE_FIELD_CHECK(task, rcpt_mime) &&
+ MESSAGE_FIELD(task, rcpt_mime)->len > 0) {
+ ret = TRUE;
+ }
+ break;
+ case RSPAMD_LOG_SMTP_FROM:
+ if (task->from_envelope) {
+ ret = TRUE;
+ }
+ break;
+ case RSPAMD_LOG_MIME_FROM:
+ if (MESSAGE_FIELD_CHECK(task, from_mime) &&
+ MESSAGE_FIELD(task, from_mime)->len > 0) {
+ ret = TRUE;
+ }
+ break;
+ case RSPAMD_LOG_FILENAME:
+ if (task->msg.fpath) {
+ ret = TRUE;
+ }
+ break;
+ case RSPAMD_LOG_FORCED_ACTION:
+ if (task->result->passthrough_result) {
+ ret = TRUE;
+ }
+ break;
+ case RSPAMD_LOG_SETTINGS_ID:
+ if (task->settings_elt) {
+ ret = TRUE;
+ }
+ break;
+ default:
+ ret = TRUE;
+ break;
+ }
+
+ return ret;
+}
+
+/*
+ * Sort by symbol's score -> name
+ */
+static gint
+rspamd_task_compare_log_sym(gconstpointer a, gconstpointer b)
+{
+ const struct rspamd_symbol_result *s1 = *(const struct rspamd_symbol_result **) a,
+ *s2 = *(const struct rspamd_symbol_result **) b;
+ gdouble w1, w2;
+
+
+ w1 = fabs(s1->score);
+ w2 = fabs(s2->score);
+
+ if (w1 == w2 && s1->name && s2->name) {
+ return strcmp(s1->name, s2->name);
+ }
+
+ return (w2 - w1) * 1000.0;
+}
+
+static gint
+rspamd_task_compare_log_group(gconstpointer a, gconstpointer b)
+{
+ const struct rspamd_symbols_group *s1 = *(const struct rspamd_symbols_group **) a,
+ *s2 = *(const struct rspamd_symbols_group **) b;
+
+ return strcmp(s1->name, s2->name);
+}
+
+
+static rspamd_ftok_t
+rspamd_task_log_metric_res(struct rspamd_task *task,
+ struct rspamd_log_format *lf)
+{
+ static gchar scorebuf[32];
+ rspamd_ftok_t res = {.begin = NULL, .len = 0};
+ struct rspamd_scan_result *mres;
+ gboolean first = TRUE;
+ rspamd_fstring_t *symbuf;
+ struct rspamd_symbol_result *sym;
+ GPtrArray *sorted_symbols;
+ struct rspamd_action *act;
+ struct rspamd_symbols_group *gr;
+ guint i, j;
+ khiter_t k;
+ guint max_log_elts = task->cfg->log_task_max_elts;
+
+ mres = task->result;
+ act = rspamd_check_action_metric(task, NULL, NULL);
+
+ if (mres != NULL) {
+ switch (lf->type) {
+ case RSPAMD_LOG_ISSPAM:
+ if (RSPAMD_TASK_IS_SKIPPED(task)) {
+ res.begin = "S";
+ }
+ else if (!(act->flags & RSPAMD_ACTION_HAM)) {
+ res.begin = "T";
+ }
+ else {
+ res.begin = "F";
+ }
+
+ res.len = 1;
+ break;
+ case RSPAMD_LOG_ACTION:
+ res.begin = act->name;
+ res.len = strlen(res.begin);
+ break;
+ case RSPAMD_LOG_SCORES:
+ res.len = rspamd_snprintf(scorebuf, sizeof(scorebuf), "%.2f/%.2f",
+ mres->score, rspamd_task_get_required_score(task, mres));
+ res.begin = scorebuf;
+ break;
+ case RSPAMD_LOG_SYMBOLS:
+ symbuf = rspamd_fstring_sized_new(128);
+ sorted_symbols = g_ptr_array_sized_new(kh_size(mres->symbols));
+
+ kh_foreach_value(mres->symbols, sym, {
+ if (!(sym->flags & RSPAMD_SYMBOL_RESULT_IGNORED)) {
+ g_ptr_array_add(sorted_symbols, (gpointer) sym);
+ }
+ });
+
+ g_ptr_array_sort(sorted_symbols, rspamd_task_compare_log_sym);
+
+ for (i = 0; i < sorted_symbols->len; i++) {
+ sym = g_ptr_array_index(sorted_symbols, i);
+
+ if (first) {
+ rspamd_printf_fstring(&symbuf, "%s", sym->name);
+ }
+ else {
+ rspamd_printf_fstring(&symbuf, ",%s", sym->name);
+ }
+
+ if (lf->flags & RSPAMD_LOG_FMT_FLAG_SYMBOLS_SCORES) {
+ rspamd_printf_fstring(&symbuf, "(%.2f)", sym->score);
+ }
+
+ if (lf->flags & RSPAMD_LOG_FMT_FLAG_SYMBOLS_PARAMS) {
+ rspamd_printf_fstring(&symbuf, "{");
+
+ if (sym->options) {
+ struct rspamd_symbol_option *opt;
+
+ j = 0;
+
+ DL_FOREACH(sym->opts_head, opt)
+ {
+ rspamd_printf_fstring(&symbuf, "%*s;",
+ (gint) opt->optlen, opt->option);
+
+ if (j >= max_log_elts && opt->next) {
+ rspamd_printf_fstring(&symbuf, "...;");
+ break;
+ }
+
+ j++;
+ }
+ }
+
+ rspamd_printf_fstring(&symbuf, "}");
+ }
+
+ first = FALSE;
+ }
+
+ g_ptr_array_free(sorted_symbols, TRUE);
+
+ rspamd_mempool_add_destructor(task->task_pool,
+ (rspamd_mempool_destruct_t) rspamd_fstring_free,
+ symbuf);
+ rspamd_mempool_notify_alloc(task->task_pool, symbuf->len);
+ res.begin = symbuf->str;
+ res.len = symbuf->len;
+ break;
+
+ case RSPAMD_LOG_GROUPS:
+ case RSPAMD_LOG_PUBLIC_GROUPS:
+
+ symbuf = rspamd_fstring_sized_new(128);
+ sorted_symbols = g_ptr_array_sized_new(kh_size(mres->sym_groups));
+
+ kh_foreach_key(mres->sym_groups, gr, {
+ if (!(gr->flags & RSPAMD_SYMBOL_GROUP_PUBLIC)) {
+ if (lf->type == RSPAMD_LOG_PUBLIC_GROUPS) {
+ continue;
+ }
+ }
+
+ g_ptr_array_add(sorted_symbols, gr);
+ });
+
+ g_ptr_array_sort(sorted_symbols, rspamd_task_compare_log_group);
+
+ for (i = 0; i < sorted_symbols->len; i++) {
+ gr = g_ptr_array_index(sorted_symbols, i);
+
+ if (first) {
+ rspamd_printf_fstring(&symbuf, "%s", gr->name);
+ }
+ else {
+ rspamd_printf_fstring(&symbuf, ",%s", gr->name);
+ }
+
+ k = kh_get(rspamd_symbols_group_hash, mres->sym_groups, gr);
+
+ rspamd_printf_fstring(&symbuf, "(%.2f)",
+ kh_value(mres->sym_groups, k));
+
+ first = FALSE;
+ }
+
+ g_ptr_array_free(sorted_symbols, TRUE);
+
+ rspamd_mempool_add_destructor(task->task_pool,
+ (rspamd_mempool_destruct_t) rspamd_fstring_free,
+ symbuf);
+ rspamd_mempool_notify_alloc(task->task_pool, symbuf->len);
+ res.begin = symbuf->str;
+ res.len = symbuf->len;
+ break;
+ default:
+ break;
+ }
+ }
+
+ return res;
+}
+
+static rspamd_fstring_t *
+rspamd_task_log_write_var(struct rspamd_task *task, rspamd_fstring_t *logbuf,
+ const rspamd_ftok_t *var, const rspamd_ftok_t *content)
+{
+ rspamd_fstring_t *res = logbuf;
+ const gchar *p, *c, *end;
+
+ if (content == NULL) {
+ /* Just output variable */
+ res = rspamd_fstring_append(res, var->begin, var->len);
+ }
+ else {
+ /* Replace $ with variable value */
+ p = content->begin;
+ c = p;
+ end = p + content->len;
+
+ while (p < end) {
+ if (*p == '$') {
+ if (p > c) {
+ res = rspamd_fstring_append(res, c, p - c);
+ }
+
+ res = rspamd_fstring_append(res, var->begin, var->len);
+ p++;
+ c = p;
+ }
+ else {
+ p++;
+ }
+ }
+
+ if (p > c) {
+ res = rspamd_fstring_append(res, c, p - c);
+ }
+ }
+
+ return res;
+}
+
+static rspamd_fstring_t *
+rspamd_task_write_ialist(struct rspamd_task *task,
+ GPtrArray *addrs, gint lim,
+ struct rspamd_log_format *lf,
+ rspamd_fstring_t *logbuf)
+{
+ rspamd_fstring_t *res = logbuf, *varbuf;
+ rspamd_ftok_t var = {.begin = NULL, .len = 0};
+ struct rspamd_email_address *addr;
+ gint i, nchars = 0, wr = 0, cur_chars;
+ gboolean has_orig = FALSE;
+ guint max_log_elts = task->cfg->log_task_max_elts;
+
+ if (addrs && lim <= 0) {
+ lim = addrs->len;
+ }
+
+ PTR_ARRAY_FOREACH(addrs, i, addr)
+ {
+ if (addr->flags & RSPAMD_EMAIL_ADDR_ORIGINAL) {
+ has_orig = TRUE;
+ break;
+ }
+ }
+
+ varbuf = rspamd_fstring_new();
+
+ PTR_ARRAY_FOREACH(addrs, i, addr)
+ {
+ if (wr >= lim) {
+ break;
+ }
+
+ if (has_orig) {
+ /* Report merely original addresses */
+ if (!(addr->flags & RSPAMD_EMAIL_ADDR_ORIGINAL)) {
+ continue;
+ }
+ }
+
+ bool last = i == lim - 1;
+
+ cur_chars = addr->addr_len;
+ varbuf = rspamd_fstring_append(varbuf, addr->addr,
+ cur_chars);
+ nchars += cur_chars;
+ wr++;
+
+ if (varbuf->len > 0 && !last) {
+ varbuf = rspamd_fstring_append(varbuf, ",", 1);
+ }
+
+ if (!last && (wr >= max_log_elts || nchars >= max_log_elts * 16)) {
+ varbuf = rspamd_fstring_append(varbuf, "...", 3);
+ break;
+ }
+ }
+
+ if (varbuf->len > 0) {
+ var.begin = varbuf->str;
+ var.len = varbuf->len;
+ res = rspamd_task_log_write_var(task, logbuf,
+ &var, (const rspamd_ftok_t *) lf->data);
+ }
+
+ rspamd_fstring_free(varbuf);
+
+ return res;
+}
+
+static rspamd_fstring_t *
+rspamd_task_write_addr_list(struct rspamd_task *task,
+ GPtrArray *addrs, gint lim,
+ struct rspamd_log_format *lf,
+ rspamd_fstring_t *logbuf)
+{
+ rspamd_fstring_t *res = logbuf, *varbuf;
+ rspamd_ftok_t var = {.begin = NULL, .len = 0};
+ struct rspamd_email_address *addr;
+ guint max_log_elts = task->cfg->log_task_max_elts;
+ guint i;
+
+ if (lim <= 0) {
+ lim = addrs->len;
+ }
+
+ varbuf = rspamd_fstring_new();
+
+ for (i = 0; i < lim; i++) {
+ addr = g_ptr_array_index(addrs, i);
+ bool last = i == lim - 1;
+
+ if (addr->addr) {
+ varbuf = rspamd_fstring_append(varbuf, addr->addr, addr->addr_len);
+ }
+
+ if (varbuf->len > 0 && !last) {
+ varbuf = rspamd_fstring_append(varbuf, ",", 1);
+ }
+
+ if (!last && i >= max_log_elts) {
+ varbuf = rspamd_fstring_append(varbuf, "...", 3);
+ break;
+ }
+ }
+
+ if (varbuf->len > 0) {
+ var.begin = varbuf->str;
+ var.len = varbuf->len;
+ res = rspamd_task_log_write_var(task, logbuf,
+ &var, (const rspamd_ftok_t *) lf->data);
+ }
+
+ rspamd_fstring_free(varbuf);
+
+ return res;
+}
+
+static rspamd_fstring_t *
+rspamd_task_log_variable(struct rspamd_task *task,
+ struct rspamd_log_format *lf, rspamd_fstring_t *logbuf)
+{
+ rspamd_fstring_t *res = logbuf;
+ rspamd_ftok_t var = {.begin = NULL, .len = 0};
+ static gchar numbuf[128];
+ static const gchar undef[] = "undef";
+
+ switch (lf->type) {
+ /* String vars */
+ case RSPAMD_LOG_MID:
+ if (MESSAGE_FIELD_CHECK(task, message_id)) {
+ var.begin = MESSAGE_FIELD(task, message_id);
+ var.len = strlen(var.begin);
+ }
+ else {
+ var.begin = undef;
+ var.len = sizeof(undef) - 1;
+ }
+ break;
+ case RSPAMD_LOG_QID:
+ if (task->queue_id) {
+ var.begin = task->queue_id;
+ var.len = strlen(var.begin);
+ }
+ else {
+ var.begin = undef;
+ var.len = sizeof(undef) - 1;
+ }
+ break;
+ case RSPAMD_LOG_USER:
+ if (task->auth_user) {
+ var.begin = task->auth_user;
+ var.len = strlen(var.begin);
+ }
+ else {
+ var.begin = undef;
+ var.len = sizeof(undef) - 1;
+ }
+ break;
+ case RSPAMD_LOG_IP:
+ if (task->from_addr && rspamd_ip_is_valid(task->from_addr)) {
+ var.begin = rspamd_inet_address_to_string(task->from_addr);
+ var.len = strlen(var.begin);
+ }
+ else {
+ var.begin = undef;
+ var.len = sizeof(undef) - 1;
+ }
+ break;
+ /* Numeric vars */
+ case RSPAMD_LOG_LEN:
+ var.len = rspamd_snprintf(numbuf, sizeof(numbuf), "%uz",
+ task->msg.len);
+ var.begin = numbuf;
+ break;
+ case RSPAMD_LOG_DNS_REQ:
+ var.len = rspamd_snprintf(numbuf, sizeof(numbuf), "%uD",
+ task->dns_requests);
+ var.begin = numbuf;
+ break;
+ case RSPAMD_LOG_TIME_REAL:
+ case RSPAMD_LOG_TIME_VIRTUAL:
+ var.begin = rspamd_log_check_time(task->task_timestamp,
+ task->time_real_finish,
+ task->cfg->clock_res);
+ var.len = strlen(var.begin);
+ break;
+ /* InternetAddress vars */
+ case RSPAMD_LOG_SMTP_FROM:
+ if (task->from_envelope) {
+ var.begin = task->from_envelope->addr;
+ var.len = task->from_envelope->addr_len;
+ }
+ break;
+ case RSPAMD_LOG_MIME_FROM:
+ if (MESSAGE_FIELD_CHECK(task, from_mime)) {
+ return rspamd_task_write_ialist(task,
+ MESSAGE_FIELD(task, from_mime),
+ 1,
+ lf,
+ logbuf);
+ }
+ break;
+ case RSPAMD_LOG_SMTP_RCPT:
+ if (task->rcpt_envelope) {
+ return rspamd_task_write_addr_list(task, task->rcpt_envelope, 1, lf,
+ logbuf);
+ }
+ break;
+ case RSPAMD_LOG_MIME_RCPT:
+ if (MESSAGE_FIELD_CHECK(task, rcpt_mime)) {
+ return rspamd_task_write_ialist(task,
+ MESSAGE_FIELD(task, rcpt_mime),
+ 1,
+ lf,
+ logbuf);
+ }
+ break;
+ case RSPAMD_LOG_SMTP_RCPTS:
+ if (task->rcpt_envelope) {
+ return rspamd_task_write_addr_list(task, task->rcpt_envelope, -1, lf,
+ logbuf);
+ }
+ break;
+ case RSPAMD_LOG_MIME_RCPTS:
+ if (MESSAGE_FIELD_CHECK(task, rcpt_mime)) {
+ return rspamd_task_write_ialist(task,
+ MESSAGE_FIELD(task, rcpt_mime),
+ -1, /* All addresses */
+ lf,
+ logbuf);
+ }
+ break;
+ case RSPAMD_LOG_DIGEST:
+ if (task->message) {
+ var.len = rspamd_snprintf(numbuf, sizeof(numbuf), "%*xs",
+ (gint) sizeof(MESSAGE_FIELD(task, digest)),
+ MESSAGE_FIELD(task, digest));
+ var.begin = numbuf;
+ }
+ else {
+ var.begin = undef;
+ var.len = sizeof(undef) - 1;
+ }
+ break;
+ case RSPAMD_LOG_FILENAME:
+ if (task->msg.fpath) {
+ var.len = strlen(task->msg.fpath);
+ var.begin = task->msg.fpath;
+ }
+ else {
+ var.begin = undef;
+ var.len = sizeof(undef) - 1;
+ }
+ break;
+ case RSPAMD_LOG_FORCED_ACTION:
+ if (task->result->passthrough_result) {
+ struct rspamd_passthrough_result *pr = task->result->passthrough_result;
+
+ if (!isnan(pr->target_score)) {
+ var.len = rspamd_snprintf(numbuf, sizeof(numbuf),
+ "%s \"%s\"; score=%.2f (set by %s)",
+ pr->action->name,
+ pr->message,
+ pr->target_score,
+ pr->module);
+ }
+ else {
+ var.len = rspamd_snprintf(numbuf, sizeof(numbuf),
+ "%s \"%s\"; score=nan (set by %s)",
+ pr->action->name,
+ pr->message,
+ pr->module);
+ }
+ var.begin = numbuf;
+ }
+ else {
+ var.begin = undef;
+ var.len = sizeof(undef) - 1;
+ }
+ break;
+ case RSPAMD_LOG_SETTINGS_ID:
+ if (task->settings_elt) {
+ var.begin = task->settings_elt->name;
+ var.len = strlen(task->settings_elt->name);
+ }
+ else {
+ var.begin = undef;
+ var.len = sizeof(undef) - 1;
+ }
+ break;
+ case RSPAMD_LOG_MEMPOOL_SIZE:
+ var.len = rspamd_snprintf(numbuf, sizeof(numbuf),
+ "%Hz",
+ rspamd_mempool_get_used_size(task->task_pool));
+ var.begin = numbuf;
+ break;
+ case RSPAMD_LOG_MEMPOOL_WASTE:
+ var.len = rspamd_snprintf(numbuf, sizeof(numbuf),
+ "%Hz",
+ rspamd_mempool_get_wasted_size(task->task_pool));
+ var.begin = numbuf;
+ break;
+ default:
+ var = rspamd_task_log_metric_res(task, lf);
+ break;
+ }
+
+ if (var.len > 0) {
+ res = rspamd_task_log_write_var(task, logbuf,
+ &var, (const rspamd_ftok_t *) lf->data);
+ }
+
+ return res;
+}
+
+void rspamd_task_write_log(struct rspamd_task *task)
+{
+ rspamd_fstring_t *logbuf;
+ struct rspamd_log_format *lf;
+ struct rspamd_task **ptask;
+ const gchar *lua_str;
+ gsize lua_str_len;
+ lua_State *L;
+
+ g_assert(task != NULL);
+
+ if (task->cfg->log_format == NULL ||
+ (task->flags & RSPAMD_TASK_FLAG_NO_LOG)) {
+ msg_debug_task("skip logging due to no log flag");
+ return;
+ }
+
+ logbuf = rspamd_fstring_sized_new(1000);
+
+ DL_FOREACH(task->cfg->log_format, lf)
+ {
+ switch (lf->type) {
+ case RSPAMD_LOG_STRING:
+ logbuf = rspamd_fstring_append(logbuf, lf->data, lf->len);
+ break;
+ case RSPAMD_LOG_LUA:
+ L = task->cfg->lua_state;
+ lua_rawgeti(L, LUA_REGISTRYINDEX, GPOINTER_TO_INT(lf->data));
+ ptask = lua_newuserdata(L, sizeof(*ptask));
+ rspamd_lua_setclass(L, "rspamd{task}", -1);
+ *ptask = task;
+
+ if (lua_pcall(L, 1, 1, 0) != 0) {
+ msg_err_task("call to log function failed: %s",
+ lua_tostring(L, -1));
+ lua_pop(L, 1);
+ }
+ else {
+ lua_str = lua_tolstring(L, -1, &lua_str_len);
+
+ if (lua_str != NULL) {
+ logbuf = rspamd_fstring_append(logbuf, lua_str, lua_str_len);
+ }
+ lua_pop(L, 1);
+ }
+ break;
+ default:
+ /* We have a variable in log format */
+ if (lf->flags & RSPAMD_LOG_FMT_FLAG_CONDITION) {
+ if (!rspamd_task_log_check_condition(task, lf)) {
+ continue;
+ }
+ }
+
+ logbuf = rspamd_task_log_variable(task, lf, logbuf);
+ break;
+ }
+ }
+
+ msg_notice_task("%V", logbuf);
+
+ rspamd_fstring_free(logbuf);
+}
+
+gdouble
+rspamd_task_get_required_score(struct rspamd_task *task, struct rspamd_scan_result *m)
+{
+ if (m == NULL) {
+ m = task->result;
+
+ if (m == NULL) {
+ return NAN;
+ }
+ }
+
+ for (guint i = m->nactions; i-- > 0;) {
+ struct rspamd_action_config *action_lim = &m->actions_config[i];
+
+
+ if (!isnan(action_lim->cur_limit) &&
+ !(action_lim->action->flags & (RSPAMD_ACTION_NO_THRESHOLD | RSPAMD_ACTION_HAM))) {
+ return m->actions_config[i].cur_limit;
+ }
+ }
+
+ return NAN;
+}
+
+rspamd_ftok_t *
+rspamd_task_get_request_header(struct rspamd_task *task,
+ const gchar *name)
+{
+ struct rspamd_request_header_chain *ret =
+ rspamd_task_get_request_header_multiple(task, name);
+
+ if (ret) {
+ return ret->hdr;
+ }
+
+ return NULL;
+}
+
+struct rspamd_request_header_chain *
+rspamd_task_get_request_header_multiple(struct rspamd_task *task,
+ const gchar *name)
+{
+ struct rspamd_request_header_chain *ret = NULL;
+ rspamd_ftok_t srch;
+ khiter_t k;
+
+ srch.begin = (gchar *) name;
+ srch.len = strlen(name);
+
+ k = kh_get(rspamd_req_headers_hash, task->request_headers,
+ &srch);
+
+ if (k != kh_end(task->request_headers)) {
+ ret = kh_value(task->request_headers, k);
+ }
+
+ return ret;
+}
+
+
+void rspamd_task_add_request_header(struct rspamd_task *task,
+ rspamd_ftok_t *name, rspamd_ftok_t *value)
+{
+
+ khiter_t k;
+ gint res;
+ struct rspamd_request_header_chain *chain, *nchain;
+
+ k = kh_put(rspamd_req_headers_hash, task->request_headers,
+ name, &res);
+
+ if (res == 0) {
+ /* Existing name */
+ nchain = rspamd_mempool_alloc(task->task_pool, sizeof(*nchain));
+ nchain->hdr = value;
+ nchain->next = NULL;
+ chain = kh_value(task->request_headers, k);
+
+ /* Slow but OK here */
+ LL_APPEND(chain, nchain);
+ }
+ else {
+ nchain = rspamd_mempool_alloc(task->task_pool, sizeof(*nchain));
+ nchain->hdr = value;
+ nchain->next = NULL;
+
+ kh_value(task->request_headers, k) = nchain;
+ }
+}
+
+
+void rspamd_task_profile_set(struct rspamd_task *task, const gchar *key,
+ gdouble value)
+{
+ GHashTable *tbl;
+ gdouble *pval;
+
+ if (key == NULL) {
+ return;
+ }
+
+ tbl = rspamd_mempool_get_variable(task->task_pool, RSPAMD_MEMPOOL_PROFILE);
+
+ if (tbl == NULL) {
+ tbl = g_hash_table_new(rspamd_str_hash, rspamd_str_equal);
+ rspamd_mempool_set_variable(task->task_pool, RSPAMD_MEMPOOL_PROFILE,
+ tbl, (rspamd_mempool_destruct_t) g_hash_table_unref);
+ }
+
+ pval = g_hash_table_lookup(tbl, key);
+
+ if (pval == NULL) {
+ pval = rspamd_mempool_alloc(task->task_pool, sizeof(*pval));
+ *pval = value;
+ g_hash_table_insert(tbl, (void *) key, pval);
+ }
+ else {
+ *pval = value;
+ }
+}
+
+gdouble *
+rspamd_task_profile_get(struct rspamd_task *task, const gchar *key)
+{
+ GHashTable *tbl;
+ gdouble *pval = NULL;
+
+ tbl = rspamd_mempool_get_variable(task->task_pool, RSPAMD_MEMPOOL_PROFILE);
+
+ if (tbl != NULL) {
+ pval = g_hash_table_lookup(tbl, key);
+ }
+
+ return pval;
+}
+
+
+gboolean
+rspamd_task_set_finish_time(struct rspamd_task *task)
+{
+ if (isnan(task->time_real_finish)) {
+ task->time_real_finish = ev_time();
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+const gchar *
+rspamd_task_stage_name(enum rspamd_task_stage stg)
+{
+ const gchar *ret = "unknown stage";
+
+ switch (stg) {
+ case RSPAMD_TASK_STAGE_CONNECT:
+ ret = "connect";
+ break;
+ case RSPAMD_TASK_STAGE_CONNFILTERS:
+ ret = "connection_filter";
+ break;
+ case RSPAMD_TASK_STAGE_READ_MESSAGE:
+ ret = "read_message";
+ break;
+ case RSPAMD_TASK_STAGE_PRE_FILTERS:
+ ret = "prefilters";
+ break;
+ case RSPAMD_TASK_STAGE_PROCESS_MESSAGE:
+ ret = "process_message";
+ break;
+ case RSPAMD_TASK_STAGE_FILTERS:
+ ret = "filters";
+ break;
+ case RSPAMD_TASK_STAGE_CLASSIFIERS_PRE:
+ ret = "classifiers_pre";
+ break;
+ case RSPAMD_TASK_STAGE_CLASSIFIERS:
+ ret = "classifiers";
+ break;
+ case RSPAMD_TASK_STAGE_CLASSIFIERS_POST:
+ ret = "classifiers_post";
+ break;
+ case RSPAMD_TASK_STAGE_COMPOSITES:
+ ret = "composites";
+ break;
+ case RSPAMD_TASK_STAGE_POST_FILTERS:
+ ret = "postfilters";
+ break;
+ case RSPAMD_TASK_STAGE_LEARN_PRE:
+ ret = "learn_pre";
+ break;
+ case RSPAMD_TASK_STAGE_LEARN:
+ ret = "learn";
+ break;
+ case RSPAMD_TASK_STAGE_LEARN_POST:
+ ret = "learn_post";
+ break;
+ case RSPAMD_TASK_STAGE_COMPOSITES_POST:
+ ret = "composites_post";
+ break;
+ case RSPAMD_TASK_STAGE_IDEMPOTENT:
+ ret = "idempotent";
+ break;
+ case RSPAMD_TASK_STAGE_DONE:
+ ret = "done";
+ break;
+ case RSPAMD_TASK_STAGE_REPLIED:
+ ret = "replied";
+ break;
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+void rspamd_task_timeout(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_task *task = (struct rspamd_task *) w->data;
+
+ if (!(task->processed_stages & RSPAMD_TASK_STAGE_FILTERS)) {
+ ev_now_update_if_cheap(task->event_loop);
+ msg_info_task("processing of task time out: %.1fs spent; %.1fs limit; "
+ "forced processing",
+ ev_now(task->event_loop) - task->task_timestamp,
+ w->repeat);
+
+ if (task->cfg->soft_reject_on_timeout) {
+ struct rspamd_action *action, *soft_reject;
+
+ action = rspamd_check_action_metric(task, NULL, NULL);
+
+ if (action->action_type != METRIC_ACTION_REJECT) {
+ soft_reject = rspamd_config_get_action_by_type(task->cfg,
+ METRIC_ACTION_SOFT_REJECT);
+ rspamd_add_passthrough_result(task,
+ soft_reject,
+ 0,
+ NAN,
+ "timeout processing message",
+ "task timeout",
+ 0, NULL);
+ }
+ }
+
+ ev_timer_again(EV_A_ w);
+ task->processed_stages |= RSPAMD_TASK_STAGE_FILTERS;
+ rspamd_session_cleanup(task->s, true);
+ rspamd_task_process(task, RSPAMD_TASK_PROCESS_ALL);
+ rspamd_session_pending(task->s);
+ }
+ else {
+ /* Postprocessing timeout */
+ msg_info_task("post-processing of task time out: %.1f second spent; forced processing",
+ ev_now(task->event_loop) - task->task_timestamp);
+
+ if (task->cfg->soft_reject_on_timeout) {
+ struct rspamd_action *action, *soft_reject;
+
+ action = rspamd_check_action_metric(task, NULL, NULL);
+
+ if (action->action_type != METRIC_ACTION_REJECT) {
+ soft_reject = rspamd_config_get_action_by_type(task->cfg,
+ METRIC_ACTION_SOFT_REJECT);
+ rspamd_add_passthrough_result(task,
+ soft_reject,
+ 0,
+ NAN,
+ "timeout post-processing message",
+ "task timeout",
+ 0, NULL);
+ }
+ }
+
+ ev_timer_stop(EV_A_ w);
+ task->processed_stages |= RSPAMD_TASK_STAGE_DONE;
+ rspamd_session_cleanup(task->s, true);
+ rspamd_task_process(task, RSPAMD_TASK_PROCESS_ALL);
+ rspamd_session_pending(task->s);
+ }
+}
+
+void rspamd_worker_guard_handler(EV_P_ ev_io *w, int revents)
+{
+ struct rspamd_task *task = (struct rspamd_task *) w->data;
+ gchar fake_buf[1024];
+ gssize r;
+
+ r = read(w->fd, fake_buf, sizeof(fake_buf));
+
+ if (r > 0) {
+ msg_warn_task("received extra data after task is loaded, ignoring");
+ }
+ else {
+ if (r == 0) {
+ /*
+ * Poor man approach, that might break things in case of
+ * shutdown (SHUT_WR) but sockets are so bad that there's no
+ * reliable way to distinguish between shutdown(SHUT_WR) and
+ * close.
+ */
+ if (task->cmd != CMD_CHECK_V2 && task->cfg->enable_shutdown_workaround) {
+ msg_info_task("workaround for shutdown enabled, please update "
+ "your client, this support might be removed in future");
+ shutdown(w->fd, SHUT_RD);
+ ev_io_stop(task->event_loop, &task->guard_ev);
+ }
+ else {
+ msg_err_task("the peer has closed connection unexpectedly");
+ rspamd_session_destroy(task->s);
+ }
+ }
+ else if (errno != EAGAIN) {
+ msg_err_task("the peer has closed connection unexpectedly: %s",
+ strerror(errno));
+ rspamd_session_destroy(task->s);
+ }
+ else {
+ return;
+ }
+ }
+}
diff --git a/src/libserver/task.h b/src/libserver/task.h
new file mode 100644
index 0000000..5404a11
--- /dev/null
+++ b/src/libserver/task.h
@@ -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.
+ */
+#ifndef TASK_H_
+#define TASK_H_
+
+#include "config.h"
+#include "libserver/http/http_connection.h"
+#include "async_session.h"
+#include "util.h"
+#include "mem_pool.h"
+#include "dns.h"
+#include "re_cache.h"
+#include "khash.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum rspamd_command {
+ CMD_SKIP = 0,
+ CMD_PING,
+ CMD_CHECK_SPAMC, /* Legacy spamassassin format */
+ CMD_CHECK_RSPAMC, /* Legacy rspamc format (like SA one) */
+ CMD_CHECK, /* Legacy check - metric json reply */
+ CMD_CHECK_V2, /* Modern check - symbols in json reply */
+};
+
+enum rspamd_task_stage {
+ RSPAMD_TASK_STAGE_CONNECT = (1u << 0u),
+ RSPAMD_TASK_STAGE_CONNFILTERS = (1u << 1u),
+ RSPAMD_TASK_STAGE_READ_MESSAGE = (1u << 2u),
+ RSPAMD_TASK_STAGE_PROCESS_MESSAGE = (1u << 3u),
+ RSPAMD_TASK_STAGE_PRE_FILTERS = (1u << 4u),
+ RSPAMD_TASK_STAGE_FILTERS = (1u << 5u),
+ RSPAMD_TASK_STAGE_CLASSIFIERS_PRE = (1u << 6u),
+ RSPAMD_TASK_STAGE_CLASSIFIERS = (1u << 7u),
+ RSPAMD_TASK_STAGE_CLASSIFIERS_POST = (1u << 8u),
+ RSPAMD_TASK_STAGE_COMPOSITES = (1u << 9u),
+ RSPAMD_TASK_STAGE_POST_FILTERS = (1u << 10u),
+ RSPAMD_TASK_STAGE_LEARN_PRE = (1u << 11u),
+ RSPAMD_TASK_STAGE_LEARN = (1u << 12u),
+ RSPAMD_TASK_STAGE_LEARN_POST = (1u << 13u),
+ RSPAMD_TASK_STAGE_COMPOSITES_POST = (1u << 14u),
+ RSPAMD_TASK_STAGE_IDEMPOTENT = (1u << 15u),
+ RSPAMD_TASK_STAGE_DONE = (1u << 16u),
+ RSPAMD_TASK_STAGE_REPLIED = (1u << 17u)
+};
+
+#define RSPAMD_TASK_PROCESS_ALL (RSPAMD_TASK_STAGE_CONNECT | \
+ RSPAMD_TASK_STAGE_CONNFILTERS | \
+ RSPAMD_TASK_STAGE_READ_MESSAGE | \
+ RSPAMD_TASK_STAGE_PRE_FILTERS | \
+ RSPAMD_TASK_STAGE_PROCESS_MESSAGE | \
+ RSPAMD_TASK_STAGE_FILTERS | \
+ RSPAMD_TASK_STAGE_CLASSIFIERS_PRE | \
+ RSPAMD_TASK_STAGE_CLASSIFIERS | \
+ RSPAMD_TASK_STAGE_CLASSIFIERS_POST | \
+ RSPAMD_TASK_STAGE_COMPOSITES | \
+ RSPAMD_TASK_STAGE_POST_FILTERS | \
+ RSPAMD_TASK_STAGE_LEARN_PRE | \
+ RSPAMD_TASK_STAGE_LEARN | \
+ RSPAMD_TASK_STAGE_LEARN_POST | \
+ RSPAMD_TASK_STAGE_COMPOSITES_POST | \
+ RSPAMD_TASK_STAGE_IDEMPOTENT | \
+ RSPAMD_TASK_STAGE_DONE)
+#define RSPAMD_TASK_PROCESS_LEARN (RSPAMD_TASK_STAGE_CONNECT | \
+ RSPAMD_TASK_STAGE_READ_MESSAGE | \
+ RSPAMD_TASK_STAGE_PROCESS_MESSAGE | \
+ RSPAMD_TASK_STAGE_CLASSIFIERS_PRE | \
+ RSPAMD_TASK_STAGE_CLASSIFIERS | \
+ RSPAMD_TASK_STAGE_CLASSIFIERS_POST | \
+ RSPAMD_TASK_STAGE_LEARN_PRE | \
+ RSPAMD_TASK_STAGE_LEARN | \
+ RSPAMD_TASK_STAGE_LEARN_POST | \
+ RSPAMD_TASK_STAGE_DONE)
+
+#define RSPAMD_TASK_FLAG_MIME (1u << 0u)
+#define RSPAMD_TASK_FLAG_SKIP_PROCESS (1u << 1u)
+#define RSPAMD_TASK_FLAG_SKIP (1u << 2u)
+#define RSPAMD_TASK_FLAG_PASS_ALL (1u << 3u)
+#define RSPAMD_TASK_FLAG_NO_LOG (1u << 4u)
+#define RSPAMD_TASK_FLAG_NO_IP (1u << 5u)
+#define RSPAMD_TASK_FLAG_PROCESSING (1u << 6u)
+#define RSPAMD_TASK_FLAG_GTUBE (1u << 7u)
+#define RSPAMD_TASK_FLAG_FILE (1u << 8u)
+#define RSPAMD_TASK_FLAG_NO_STAT (1u << 9u)
+#define RSPAMD_TASK_FLAG_UNLEARN (1u << 10u)
+#define RSPAMD_TASK_FLAG_ALREADY_LEARNED (1u << 11u)
+#define RSPAMD_TASK_FLAG_LEARN_SPAM (1u << 12u)
+#define RSPAMD_TASK_FLAG_LEARN_HAM (1u << 13u)
+#define RSPAMD_TASK_FLAG_LEARN_AUTO (1u << 14u)
+#define RSPAMD_TASK_FLAG_BROKEN_HEADERS (1u << 15u)
+#define RSPAMD_TASK_FLAG_HAS_SPAM_TOKENS (1u << 16u)
+#define RSPAMD_TASK_FLAG_HAS_HAM_TOKENS (1u << 17u)
+#define RSPAMD_TASK_FLAG_EMPTY (1u << 18u)
+#define RSPAMD_TASK_FLAG_PROFILE (1u << 19u)
+#define RSPAMD_TASK_FLAG_GREYLISTED (1u << 20u)
+#define RSPAMD_TASK_FLAG_OWN_POOL (1u << 21u)
+#define RSPAMD_TASK_FLAG_SSL (1u << 22u)
+#define RSPAMD_TASK_FLAG_BAD_UNICODE (1u << 23u)
+#define RSPAMD_TASK_FLAG_MESSAGE_REWRITE (1u << 24u)
+#define RSPAMD_TASK_FLAG_MAX_SHIFT (24u)
+
+
+/* Request has a JSON control block */
+#define RSPAMD_TASK_PROTOCOL_FLAG_HAS_CONTROL (1u << 0u)
+/* Request has been done by a local client */
+#define RSPAMD_TASK_PROTOCOL_FLAG_LOCAL_CLIENT (1u << 1u)
+/* Request has been sent via milter */
+#define RSPAMD_TASK_PROTOCOL_FLAG_MILTER (1u << 2u)
+/* Compress protocol reply */
+#define RSPAMD_TASK_PROTOCOL_FLAG_COMPRESSED (1u << 3u)
+/* Include all URLs */
+#define RSPAMD_TASK_PROTOCOL_FLAG_EXT_URLS (1u << 4u)
+/* Client allows body block (including headers in no FLAG_MILTER) */
+#define RSPAMD_TASK_PROTOCOL_FLAG_BODY_BLOCK (1u << 5u)
+/* Emit groups information */
+#define RSPAMD_TASK_PROTOCOL_FLAG_GROUPS (1u << 6u)
+#define RSPAMD_TASK_PROTOCOL_FLAG_MAX_SHIFT (6u)
+
+#define RSPAMD_TASK_IS_SKIPPED(task) (G_UNLIKELY((task)->flags & RSPAMD_TASK_FLAG_SKIP))
+#define RSPAMD_TASK_IS_SPAMC(task) (G_UNLIKELY((task)->cmd == CMD_CHECK_SPAMC))
+#define RSPAMD_TASK_IS_PROCESSED(task) (G_UNLIKELY((task)->processed_stages & RSPAMD_TASK_STAGE_DONE))
+#define RSPAMD_TASK_IS_CLASSIFIED(task) (((task)->processed_stages & RSPAMD_TASK_STAGE_CLASSIFIERS))
+#define RSPAMD_TASK_IS_EMPTY(task) (G_UNLIKELY((task)->flags & RSPAMD_TASK_FLAG_EMPTY))
+#define RSPAMD_TASK_IS_PROFILING(task) (G_UNLIKELY((task)->flags & RSPAMD_TASK_FLAG_PROFILE))
+#define RSPAMD_TASK_IS_MIME(task) (G_LIKELY((task)->flags & RSPAMD_TASK_FLAG_MIME))
+
+struct rspamd_email_address;
+struct rspamd_lang_detector;
+enum rspamd_newlines_type;
+struct rspamd_message;
+
+struct rspamd_task_data_storage {
+ const gchar *begin;
+ gsize len;
+ gchar *fpath;
+};
+
+struct rspamd_request_header_chain {
+ rspamd_ftok_t *hdr;
+ struct rspamd_request_header_chain *next;
+};
+
+__KHASH_TYPE(rspamd_req_headers_hash, rspamd_ftok_t *, struct rspamd_request_header_chain *);
+
+struct rspamd_lua_cached_entry {
+ gint ref;
+ guint id;
+};
+
+KHASH_INIT(rspamd_task_lua_cache, char *, struct rspamd_lua_cached_entry, 1, kh_str_hash_func, kh_str_hash_equal);
+
+/**
+ * Worker task structure
+ */
+struct rspamd_task {
+ struct rspamd_worker *worker; /**< pointer to worker object */
+ enum rspamd_command cmd; /**< command */
+ gint sock; /**< socket descriptor */
+ guint32 dns_requests; /**< number of DNS requests per this task */
+ guint32 flags; /**< Bit flags */
+ guint32 protocol_flags;
+ guint32 processed_stages; /**< bits of stages that are processed */
+ gchar *helo; /**< helo header value */
+ gchar *queue_id; /**< queue id if specified */
+ rspamd_inet_addr_t *from_addr; /**< from addr for a task */
+ rspamd_inet_addr_t *client_addr; /**< address of connected socket */
+ gchar *deliver_to; /**< address to deliver */
+ gchar *auth_user; /**< SMTP authenticated user */
+ const gchar *hostname; /**< hostname reported by MTA */
+ khash_t(rspamd_req_headers_hash) * request_headers; /**< HTTP headers in a request */
+ struct rspamd_task_data_storage msg; /**< message buffer */
+ struct rspamd_http_connection *http_conn; /**< HTTP server connection */
+ struct rspamd_async_session *s; /**< async session object */
+ struct rspamd_scan_result *result; /**< Metric result */
+ khash_t(rspamd_task_lua_cache) lua_cache; /**< cache of lua objects */
+ GPtrArray *tokens; /**< statistics tokens */
+ GArray *meta_words; /**< rspamd_stat_token_t produced from meta headers
+ (e.g. Subject) */
+
+ GPtrArray *rcpt_envelope; /**< array of rspamd_email_address */
+ struct rspamd_email_address *from_envelope;
+ struct rspamd_email_address *from_envelope_orig;
+
+ ucl_object_t *messages; /**< list of messages that would be reported */
+ struct rspamd_re_runtime *re_rt; /**< regexp runtime */
+ GPtrArray *stat_runtimes; /**< backend runtime */
+ struct rspamd_config *cfg; /**< pointer to config object */
+ GError *err;
+ rspamd_mempool_t *task_pool; /**< memory pool for task */
+ double time_real_finish;
+ ev_tstamp task_timestamp;
+
+ gboolean (*fin_callback)(struct rspamd_task *task, void *arg);
+ /**< callback for filters finalizing */
+ void *fin_arg; /**< argument for fin callback */
+
+ struct rspamd_dns_resolver *resolver; /**< DNS resolver */
+ struct ev_loop *event_loop; /**< Event base */
+ struct ev_timer timeout_ev; /**< Global task timeout */
+ struct ev_io guard_ev; /**< Event for input sanity guard */
+
+ gpointer symcache_runtime; /**< Opaque checkpoint data */
+ ucl_object_t *settings; /**< Settings applied to task */
+ struct rspamd_config_settings_elt *settings_elt; /**< preprocessed settings id elt */
+
+ const gchar *classifier; /**< Classifier to learn (if needed) */
+ struct rspamd_lang_detector *lang_det; /**< Languages detector */
+ struct rspamd_message *message;
+};
+
+/**
+ * Construct new task for worker
+ */
+struct rspamd_task *rspamd_task_new(struct rspamd_worker *worker,
+ struct rspamd_config *cfg,
+ rspamd_mempool_t *pool,
+ struct rspamd_lang_detector *lang_det,
+ struct ev_loop *event_loop,
+ gboolean debug_mem);
+
+/**
+ * Destroy task object and remove its IO dispatcher if it exists
+ */
+void rspamd_task_free(struct rspamd_task *task);
+
+/**
+ * Called if all filters are processed
+ * @return TRUE if session should be terminated
+ */
+gboolean rspamd_task_fin(void *arg);
+
+/**
+ * Load HTTP message with body in `msg` to an rspamd_task
+ * @param task
+ * @param msg
+ * @param start
+ * @param len
+ * @return
+ */
+gboolean rspamd_task_load_message(struct rspamd_task *task,
+ struct rspamd_http_message *msg,
+ const gchar *start, gsize len);
+
+/**
+ * Process task
+ * @param task task to process
+ * @return task has been successfully parsed and processed
+ */
+gboolean rspamd_task_process(struct rspamd_task *task, guint stages);
+
+/**
+ * Return address of sender or NULL
+ * @param task
+ * @return
+ */
+struct rspamd_email_address *rspamd_task_get_sender(struct rspamd_task *task);
+
+/**
+ * Return addresses in the following precedence:
+ * - deliver to
+ * - the first smtp recipient
+ * - the first mime recipient
+ * @param task
+ * @return
+ */
+const gchar *rspamd_task_get_principal_recipient(struct rspamd_task *task);
+
+/**
+ * Add a recipient for a task
+ * @param task task object
+ * @param rcpt string representation of recipient address
+ * @return TRUE if an address has been parsed and added
+ */
+gboolean rspamd_task_add_recipient(struct rspamd_task *task, const gchar *rcpt);
+
+/**
+ * Learn specified statfile with message in a task
+ * @param task worker's task object
+ * @param classifier classifier to learn (or NULL to learn all)
+ * @param err pointer to GError
+ * @return true if learn succeed
+ */
+gboolean rspamd_learn_task_spam(struct rspamd_task *task,
+ gboolean is_spam,
+ const gchar *classifier,
+ GError **err);
+
+/**
+ * Returns required score for a message (usually reject score)
+ * @param task
+ * @param m
+ * @return
+ */
+struct rspamd_scan_result;
+
+gdouble rspamd_task_get_required_score(struct rspamd_task *task,
+ struct rspamd_scan_result *m);
+
+/**
+ * Returns the first header as value for a header
+ * @param task
+ * @param name
+ * @return
+ */
+rspamd_ftok_t *rspamd_task_get_request_header(struct rspamd_task *task,
+ const gchar *name);
+
+/**
+ * Returns all headers with the specific name
+ * @param task
+ * @param name
+ * @return
+ */
+struct rspamd_request_header_chain *rspamd_task_get_request_header_multiple(
+ struct rspamd_task *task,
+ const gchar *name);
+
+/**
+ * Adds a new request header to task (name and value should be mapped to fstring)
+ * @param task
+ * @param name
+ * @param value
+ */
+void rspamd_task_add_request_header(struct rspamd_task *task,
+ rspamd_ftok_t *name, rspamd_ftok_t *value);
+
+/**
+ * Write log line about the specified task if needed
+ */
+void rspamd_task_write_log(struct rspamd_task *task);
+
+/**
+ * Set profiling value for a specific key
+ * @param task
+ * @param key
+ * @param value
+ */
+void rspamd_task_profile_set(struct rspamd_task *task, const gchar *key,
+ gdouble value);
+
+/**
+ * Get value for a specific profiling key
+ * @param task
+ * @param key
+ * @return
+ */
+gdouble *rspamd_task_profile_get(struct rspamd_task *task, const gchar *key);
+
+/**
+ * Sets finishing time for a task if not yet set
+ * @param task
+ * @return
+ */
+gboolean rspamd_task_set_finish_time(struct rspamd_task *task);
+
+/**
+ * Returns task processing stage name
+ * @param stg
+ * @return
+ */
+const gchar *rspamd_task_stage_name(enum rspamd_task_stage stg);
+
+/*
+ * Called on forced timeout
+ */
+void rspamd_task_timeout(EV_P_ ev_timer *w, int revents);
+
+/*
+ * Called on unexpected IO error (e.g. ECONNRESET)
+ */
+void rspamd_worker_guard_handler(EV_P_ ev_io *w, int revents);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* TASK_H_ */
diff --git a/src/libserver/url.c b/src/libserver/url.c
new file mode 100644
index 0000000..0842a1e
--- /dev/null
+++ b/src/libserver/url.c
@@ -0,0 +1,4365 @@
+/*
+ * 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 "url.h"
+#include "util.h"
+#include "rspamd.h"
+#include "message.h"
+#include "multipattern.h"
+#include "contrib/uthash/utlist.h"
+#include "contrib/http-parser/http_parser.h"
+#include <unicode/utf8.h>
+#include <unicode/uchar.h>
+#include <unicode/usprep.h>
+#include <unicode/ucnv.h>
+
+typedef struct url_match_s {
+ const gchar *m_begin;
+ gsize m_len;
+ const gchar *pattern;
+ const gchar *prefix;
+ const gchar *newline_pos;
+ const gchar *prev_newline_pos;
+ gboolean add_prefix;
+ gchar st;
+} url_match_t;
+
+#define URL_MATCHER_FLAG_NOHTML (1u << 0u)
+#define URL_MATCHER_FLAG_TLD_MATCH (1u << 1u)
+#define URL_MATCHER_FLAG_STAR_MATCH (1u << 2u)
+#define URL_MATCHER_FLAG_REGEXP (1u << 3u)
+
+struct url_callback_data;
+
+static const struct {
+ enum rspamd_url_protocol proto;
+ const gchar *name;
+ gsize len;
+} rspamd_url_protocols[] = {
+ {.proto = PROTOCOL_FILE,
+ .name = "file",
+ .len = 4},
+ {.proto = PROTOCOL_FTP,
+ .name = "ftp",
+ .len = 3},
+ {.proto = PROTOCOL_HTTP,
+ .name = "http",
+ .len = 4},
+ {.proto = PROTOCOL_HTTPS,
+ .name = "https",
+ .len = 5},
+ {.proto = PROTOCOL_MAILTO,
+ .name = "mailto",
+ .len = 6},
+ {.proto = PROTOCOL_TELEPHONE,
+ .name = "tel",
+ .len = 3},
+ {.proto = PROTOCOL_TELEPHONE,
+ .name = "callto",
+ .len = 3},
+ {.proto = PROTOCOL_UNKNOWN,
+ .name = NULL,
+ .len = 0}};
+struct url_matcher {
+ const gchar *pattern;
+ const gchar *prefix;
+
+ gboolean (*start)(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match);
+
+ gboolean (*end)(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match);
+
+ gint flags;
+};
+
+static gboolean url_file_start(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match);
+
+static gboolean url_file_end(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match);
+
+static gboolean url_web_start(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match);
+
+static gboolean url_web_end(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match);
+
+static gboolean url_tld_start(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match);
+
+static gboolean url_tld_end(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match);
+
+static gboolean url_email_start(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match);
+
+static gboolean url_email_end(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match);
+
+static gboolean url_tel_start(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match);
+
+static gboolean url_tel_end(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match);
+
+struct url_matcher static_matchers[] = {
+ /* Common prefixes */
+ {"file://", "", url_file_start, url_file_end,
+ 0},
+ {"file:\\\\", "", url_file_start, url_file_end,
+ 0},
+ {"ftp://", "", url_web_start, url_web_end,
+ 0},
+ {"ftp:\\\\", "", url_web_start, url_web_end,
+ 0},
+ {"sftp://", "", url_web_start, url_web_end,
+ 0},
+ {"http:", "", url_web_start, url_web_end,
+ 0},
+ {"https:", "", url_web_start, url_web_end,
+ 0},
+ {"news://", "", url_web_start, url_web_end,
+ 0},
+ {"nntp://", "", url_web_start, url_web_end,
+ 0},
+ {"telnet://", "", url_web_start, url_web_end,
+ 0},
+ {"tel:", "", url_tel_start, url_tel_end,
+ 0},
+ {"webcal://", "", url_web_start, url_web_end,
+ 0},
+ {"mailto:", "", url_email_start, url_email_end,
+ 0},
+ {"callto:", "", url_tel_start, url_tel_end,
+ 0},
+ {"h323:", "", url_web_start, url_web_end,
+ 0},
+ {"sip:", "", url_web_start, url_web_end,
+ 0},
+ {"www\\.[0-9a-z]", "http://", url_web_start, url_web_end,
+ URL_MATCHER_FLAG_REGEXP},
+ {"ftp.", "ftp://", url_web_start, url_web_end,
+ 0},
+ /* Likely emails */
+ {
+ "@", "mailto://", url_email_start, url_email_end,
+ 0}};
+
+struct rspamd_url_flag_name {
+ const gchar *name;
+ gint flag;
+ gint hash;
+} url_flag_names[] = {
+ {"phished", RSPAMD_URL_FLAG_PHISHED, -1},
+ {"numeric", RSPAMD_URL_FLAG_NUMERIC, -1},
+ {"obscured", RSPAMD_URL_FLAG_OBSCURED, -1},
+ {"redirected", RSPAMD_URL_FLAG_REDIRECTED, -1},
+ {"html_displayed", RSPAMD_URL_FLAG_HTML_DISPLAYED, -1},
+ {"text", RSPAMD_URL_FLAG_FROM_TEXT, -1},
+ {"subject", RSPAMD_URL_FLAG_SUBJECT, -1},
+ {"host_encoded", RSPAMD_URL_FLAG_HOSTENCODED, -1},
+ {"schema_encoded", RSPAMD_URL_FLAG_SCHEMAENCODED, -1},
+ {"path_encoded", RSPAMD_URL_FLAG_PATHENCODED, -1},
+ {"query_encoded", RSPAMD_URL_FLAG_QUERYENCODED, -1},
+ {"missing_slashes", RSPAMD_URL_FLAG_MISSINGSLASHES, -1},
+ {"idn", RSPAMD_URL_FLAG_IDN, -1},
+ {"has_port", RSPAMD_URL_FLAG_HAS_PORT, -1},
+ {"has_user", RSPAMD_URL_FLAG_HAS_USER, -1},
+ {"schemaless", RSPAMD_URL_FLAG_SCHEMALESS, -1},
+ {"unnormalised", RSPAMD_URL_FLAG_UNNORMALISED, -1},
+ {"zw_spaces", RSPAMD_URL_FLAG_ZW_SPACES, -1},
+ {"url_displayed", RSPAMD_URL_FLAG_DISPLAY_URL, -1},
+ {"image", RSPAMD_URL_FLAG_IMAGE, -1},
+ {"query", RSPAMD_URL_FLAG_QUERY, -1},
+ {"content", RSPAMD_URL_FLAG_CONTENT, -1},
+ {"no_tld", RSPAMD_URL_FLAG_NO_TLD, -1},
+ {"truncated", RSPAMD_URL_FLAG_TRUNCATED, -1},
+ {"redirect_target", RSPAMD_URL_FLAG_REDIRECT_TARGET, -1},
+ {"invisible", RSPAMD_URL_FLAG_INVISIBLE, -1},
+ {"special", RSPAMD_URL_FLAG_SPECIAL, -1},
+};
+
+
+static inline khint_t rspamd_url_hash(struct rspamd_url *u);
+
+static inline khint_t rspamd_url_host_hash(struct rspamd_url *u);
+static inline bool rspamd_urls_cmp(struct rspamd_url *a, struct rspamd_url *b);
+static inline bool rspamd_urls_host_cmp(struct rspamd_url *a, struct rspamd_url *b);
+
+/* Hash table implementation */
+__KHASH_IMPL(rspamd_url_hash, kh_inline, struct rspamd_url *, char, false,
+ rspamd_url_hash, rspamd_urls_cmp);
+__KHASH_IMPL(rspamd_url_host_hash, kh_inline, struct rspamd_url *, char, false,
+ rspamd_url_host_hash, rspamd_urls_host_cmp);
+
+struct url_callback_data {
+ const gchar *begin;
+ gchar *url_str;
+ rspamd_mempool_t *pool;
+ gint len;
+ enum rspamd_url_find_type how;
+ gboolean prefix_added;
+ guint newline_idx;
+ GArray *matchers;
+ GPtrArray *newlines;
+ const gchar *start;
+ const gchar *fin;
+ const gchar *end;
+ const gchar *last_at;
+ url_insert_function func;
+ void *funcd;
+};
+
+struct url_match_scanner {
+ GArray *matchers_full;
+ GArray *matchers_strict;
+ struct rspamd_multipattern *search_trie_full;
+ struct rspamd_multipattern *search_trie_strict;
+ bool has_tld_file;
+};
+
+struct url_match_scanner *url_scanner = NULL;
+
+enum {
+ IS_LWSP = (1 << 0),
+ IS_DOMAIN = (1 << 1),
+ IS_URLSAFE = (1 << 2),
+ IS_MAILSAFE = (1 << 3),
+ IS_DOMAIN_END = (1 << 4)
+};
+
+static const unsigned int url_scanner_table[256] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, IS_LWSP, IS_LWSP, IS_LWSP, IS_LWSP, IS_LWSP, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, IS_LWSP /* */,
+ IS_MAILSAFE /* ! */, IS_URLSAFE | IS_DOMAIN_END | IS_MAILSAFE /* " */,
+ IS_MAILSAFE /* # */, IS_MAILSAFE /* $ */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* % */, 0 /* & */, IS_MAILSAFE /* ' */,
+ 0 /* ( */, 0 /* ) */, IS_MAILSAFE /* * */,
+ IS_MAILSAFE /* + */, IS_MAILSAFE /* , */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* - */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* . */, IS_DOMAIN_END | IS_MAILSAFE /* / */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 0 */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 1 */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 2 */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 3 */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 4 */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 5 */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 6 */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 7 */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 8 */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 9 */, IS_DOMAIN_END /* : */,
+ 0 /* ; */, IS_URLSAFE | IS_DOMAIN_END /* < */, 0 /* = */,
+ IS_URLSAFE | IS_DOMAIN_END /* > */, IS_DOMAIN_END /* ? */, 0 /* @ */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* A */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* B */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* C */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* D */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* E */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* F */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* G */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* H */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* I */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* J */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* K */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* L */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* M */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* N */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* O */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* P */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* Q */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* R */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* S */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* T */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* U */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* V */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* W */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* X */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* Y */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* Z */, 0 /* [ */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* \ */, 0 /* ] */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* ^ */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* _ */,
+ IS_URLSAFE | IS_DOMAIN_END /* ` */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* a */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* b */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* c */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* d */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* e */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* f */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* g */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* h */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* i */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* j */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* k */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* l */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* m */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* n */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* o */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* p */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* q */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* r */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* s */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* t */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* u */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* v */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* w */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* x */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* y */,
+ IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* z */,
+ IS_URLSAFE | IS_DOMAIN_END | IS_MAILSAFE /* { */,
+ IS_URLSAFE | IS_DOMAIN_END | IS_MAILSAFE /* | */,
+ IS_URLSAFE | IS_DOMAIN_END | IS_MAILSAFE /* } */,
+ IS_URLSAFE | IS_DOMAIN_END | IS_MAILSAFE /* ~ */, 0, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN,
+ IS_URLSAFE | IS_DOMAIN};
+
+#define is_lwsp(x) ((url_scanner_table[(guchar) (x)] & IS_LWSP) != 0)
+#define is_mailsafe(x) ((url_scanner_table[(guchar) (x)] & (IS_MAILSAFE)) != 0)
+#define is_domain(x) ((url_scanner_table[(guchar) (x)] & IS_DOMAIN) != 0)
+#define is_urlsafe(x) ((url_scanner_table[(guchar) (x)] & (IS_URLSAFE)) != 0)
+
+const gchar *
+rspamd_url_strerror(int err)
+{
+ switch (err) {
+ case URI_ERRNO_OK:
+ return "Parsing went well";
+ case URI_ERRNO_EMPTY:
+ return "The URI string was empty";
+ case URI_ERRNO_INVALID_PROTOCOL:
+ return "No protocol was found";
+ case URI_ERRNO_BAD_FORMAT:
+ return "Bad URL format";
+ case URI_ERRNO_BAD_ENCODING:
+ return "Invalid symbols encoded";
+ case URI_ERRNO_INVALID_PORT:
+ return "Port number is bad";
+ case URI_ERRNO_TLD_MISSING:
+ return "TLD part is not detected";
+ case URI_ERRNO_HOST_MISSING:
+ return "Host part is missing";
+ case URI_ERRNO_TOO_LONG:
+ return "URL is too long";
+ }
+
+ return NULL;
+}
+
+static gboolean
+rspamd_url_parse_tld_file(const gchar *fname,
+ struct url_match_scanner *scanner)
+{
+ FILE *f;
+ struct url_matcher m;
+ gchar *linebuf = NULL, *p;
+ gsize buflen = 0;
+ gssize r;
+ gint flags;
+
+ f = fopen(fname, "r");
+
+ if (f == NULL) {
+ msg_err("cannot open TLD file %s: %s", fname, strerror(errno));
+ return FALSE;
+ }
+
+ m.end = url_tld_end;
+ m.start = url_tld_start;
+ m.prefix = "http://";
+
+ while ((r = getline(&linebuf, &buflen, f)) > 0) {
+ if (linebuf[0] == '/' || g_ascii_isspace(linebuf[0])) {
+ /* Skip comment or empty line */
+ continue;
+ }
+
+ g_strchomp(linebuf);
+
+ /* TODO: add support for ! patterns */
+ if (linebuf[0] == '!') {
+ msg_debug("skip '!' patterns from parsing for now: %s", linebuf);
+ continue;
+ }
+
+ flags = URL_MATCHER_FLAG_NOHTML | URL_MATCHER_FLAG_TLD_MATCH;
+
+ if (linebuf[0] == '*') {
+ flags |= URL_MATCHER_FLAG_STAR_MATCH;
+ p = strchr(linebuf, '.');
+
+ if (p == NULL) {
+ msg_err("got bad star line, skip it: %s", linebuf);
+ continue;
+ }
+ p++;
+ }
+ else {
+ p = linebuf;
+ }
+
+ m.flags = flags;
+ rspamd_multipattern_add_pattern(url_scanner->search_trie_full, p,
+ RSPAMD_MULTIPATTERN_TLD | RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8);
+ m.pattern = rspamd_multipattern_get_pattern(url_scanner->search_trie_full,
+ rspamd_multipattern_get_npatterns(url_scanner->search_trie_full) - 1);
+
+ g_array_append_val(url_scanner->matchers_full, m);
+ }
+
+ free(linebuf);
+ fclose(f);
+
+ return TRUE;
+}
+
+static void
+rspamd_url_add_static_matchers(struct url_match_scanner *sc)
+{
+ gint n = G_N_ELEMENTS(static_matchers), i;
+
+ for (i = 0; i < n; i++) {
+ if (static_matchers[i].flags & URL_MATCHER_FLAG_REGEXP) {
+ rspamd_multipattern_add_pattern(url_scanner->search_trie_strict,
+ static_matchers[i].pattern,
+ RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8 |
+ RSPAMD_MULTIPATTERN_RE);
+ }
+ else {
+ rspamd_multipattern_add_pattern(url_scanner->search_trie_strict,
+ static_matchers[i].pattern,
+ RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8);
+ }
+ }
+
+ g_array_append_vals(sc->matchers_strict, static_matchers, n);
+
+ if (sc->matchers_full) {
+ for (i = 0; i < n; i++) {
+ if (static_matchers[i].flags & URL_MATCHER_FLAG_REGEXP) {
+ rspamd_multipattern_add_pattern(url_scanner->search_trie_full,
+ static_matchers[i].pattern,
+ RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8 |
+ RSPAMD_MULTIPATTERN_RE);
+ }
+ else {
+ rspamd_multipattern_add_pattern(url_scanner->search_trie_full,
+ static_matchers[i].pattern,
+ RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8);
+ }
+ }
+ g_array_append_vals(sc->matchers_full, static_matchers, n);
+ }
+}
+
+void rspamd_url_deinit(void)
+{
+ if (url_scanner != NULL) {
+ if (url_scanner->search_trie_full) {
+ rspamd_multipattern_destroy(url_scanner->search_trie_full);
+ g_array_free(url_scanner->matchers_full, TRUE);
+ }
+
+ rspamd_multipattern_destroy(url_scanner->search_trie_strict);
+ g_array_free(url_scanner->matchers_strict, TRUE);
+ g_free(url_scanner);
+
+ url_scanner = NULL;
+ }
+}
+
+void rspamd_url_init(const gchar *tld_file)
+{
+ GError *err = NULL;
+ gboolean ret = TRUE;
+
+ if (url_scanner != NULL) {
+ rspamd_url_deinit();
+ }
+
+ url_scanner = g_malloc(sizeof(struct url_match_scanner));
+
+ url_scanner->matchers_strict = g_array_sized_new(FALSE, TRUE,
+ sizeof(struct url_matcher), G_N_ELEMENTS(static_matchers));
+ url_scanner->search_trie_strict = rspamd_multipattern_create_sized(
+ G_N_ELEMENTS(static_matchers),
+ RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8);
+
+ if (tld_file) {
+ /* Reserve larger multipattern */
+ url_scanner->matchers_full = g_array_sized_new(FALSE, TRUE,
+ sizeof(struct url_matcher), 13000);
+ url_scanner->search_trie_full = rspamd_multipattern_create_sized(13000,
+ RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8);
+ url_scanner->has_tld_file = true;
+ }
+ else {
+ url_scanner->matchers_full = NULL;
+ url_scanner->search_trie_full = NULL;
+ url_scanner->has_tld_file = false;
+ }
+
+ rspamd_url_add_static_matchers(url_scanner);
+
+ if (tld_file != NULL) {
+ ret = rspamd_url_parse_tld_file(tld_file, url_scanner);
+ }
+
+ if (url_scanner->matchers_full && url_scanner->matchers_full->len > 1000) {
+ msg_info("start compiling of %d TLD suffixes; it might take a long time",
+ url_scanner->matchers_full->len);
+ }
+
+ if (!rspamd_multipattern_compile(url_scanner->search_trie_strict, &err)) {
+ msg_err("cannot compile url matcher static patterns, fatal error: %e", err);
+ abort();
+ }
+
+ if (url_scanner->search_trie_full) {
+ if (!rspamd_multipattern_compile(url_scanner->search_trie_full, &err)) {
+ msg_err("cannot compile tld patterns, url matching will be "
+ "incomplete: %e",
+ err);
+ g_error_free(err);
+ ret = FALSE;
+ }
+ }
+
+ if (tld_file != NULL) {
+ if (ret) {
+ msg_info("initialized %ud url match suffixes from '%s'",
+ url_scanner->matchers_full->len - url_scanner->matchers_strict->len,
+ tld_file);
+ }
+ else {
+ msg_err("failed to initialize url tld suffixes from '%s', "
+ "use %ud internal match suffixes",
+ tld_file,
+ url_scanner->matchers_strict->len);
+ }
+ }
+
+ /* Generate hashes for flags */
+ for (gint i = 0; i < G_N_ELEMENTS(url_flag_names); i++) {
+ url_flag_names[i].hash =
+ rspamd_cryptobox_fast_hash_specific(RSPAMD_CRYPTOBOX_HASHFAST_INDEPENDENT,
+ url_flag_names[i].name,
+ strlen(url_flag_names[i].name), 0);
+ }
+ /* Ensure that we have no hashes collisions O(N^2) but this array is small */
+ for (gint i = 0; i < G_N_ELEMENTS(url_flag_names) - 1; i++) {
+ for (gint j = i + 1; j < G_N_ELEMENTS(url_flag_names); j++) {
+ if (url_flag_names[i].hash == url_flag_names[j].hash) {
+ msg_err("collision: both %s and %s map to %d",
+ url_flag_names[i].name, url_flag_names[j].name,
+ url_flag_names[i].hash);
+ abort();
+ }
+ }
+ }
+}
+
+#define SET_U(u, field) \
+ do { \
+ if ((u) != NULL) { \
+ (u)->field_set |= 1 << (field); \
+ (u)->field_data[(field)].len = p - c; \
+ (u)->field_data[(field)].off = c - str; \
+ } \
+ } while (0)
+
+static bool
+is_url_start(gchar c)
+{
+ if (c == '(' ||
+ c == '{' ||
+ c == '[' ||
+ c == '<' ||
+ c == '\'') {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static bool
+is_url_end(gchar c)
+{
+ if (c == ')' ||
+ c == '}' ||
+ c == ']' ||
+ c == '>' ||
+ c == '\'') {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static bool
+is_domain_start(int p)
+{
+ if (g_ascii_isalnum(p) ||
+ p == '[' ||
+ p == '%' ||
+ p == '_' ||
+ (p & 0x80)) {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static const guint max_domain_length = 253;
+static const guint max_dns_label = 63;
+static const guint max_email_user = 64;
+
+static gint
+rspamd_mailto_parse(struct http_parser_url *u,
+ const gchar *str, gsize len,
+ gchar const **end,
+ enum rspamd_url_parse_flags parse_flags, guint *flags)
+{
+ const gchar *p = str, *c = str, *last = str + len;
+ gchar t;
+ gint ret = 1;
+ enum {
+ parse_mailto,
+ parse_slash,
+ parse_slash_slash,
+ parse_semicolon,
+ parse_prefix_question,
+ parse_destination,
+ parse_equal,
+ parse_user,
+ parse_at,
+ parse_domain,
+ parse_suffix_question,
+ parse_query
+ } st = parse_mailto;
+
+ if (u != NULL) {
+ memset(u, 0, sizeof(*u));
+ }
+
+ while (p < last) {
+ t = *p;
+
+ if (p - str > max_email_user + max_domain_length + 1) {
+ goto out;
+ }
+
+ switch (st) {
+ case parse_mailto:
+ if (t == ':') {
+ st = parse_semicolon;
+ SET_U(u, UF_SCHEMA);
+ }
+ p++;
+ break;
+ case parse_semicolon:
+ if (t == '/' || t == '\\') {
+ st = parse_slash;
+ p++;
+ }
+ else {
+ *flags |= RSPAMD_URL_FLAG_MISSINGSLASHES;
+ st = parse_slash_slash;
+ }
+ break;
+ case parse_slash:
+ if (t == '/' || t == '\\') {
+ st = parse_slash_slash;
+ }
+ else {
+ goto out;
+ }
+ p++;
+ break;
+ case parse_slash_slash:
+ if (t == '?') {
+ st = parse_prefix_question;
+ p++;
+ }
+ else if (t != '/' && t != '\\') {
+ c = p;
+ st = parse_user;
+ }
+ else {
+ /* Skip multiple slashes */
+ p++;
+ }
+ break;
+ case parse_prefix_question:
+ if (t == 't') {
+ /* XXX: accept only to= */
+ st = parse_destination;
+ }
+ else {
+ goto out;
+ }
+ break;
+ case parse_destination:
+ if (t == '=') {
+ st = parse_equal;
+ }
+ p++;
+ break;
+ case parse_equal:
+ c = p;
+ st = parse_user;
+ break;
+ case parse_user:
+ if (t == '@') {
+ if (p - c == 0) {
+ goto out;
+ }
+ SET_U(u, UF_USERINFO);
+ st = parse_at;
+ }
+ else if (!is_mailsafe(t)) {
+ goto out;
+ }
+ else if (p - c > max_email_user) {
+ goto out;
+ }
+ p++;
+ break;
+ case parse_at:
+ c = p;
+ st = parse_domain;
+ break;
+ case parse_domain:
+ if (t == '?') {
+ SET_U(u, UF_HOST);
+ st = parse_suffix_question;
+ }
+ else if (!is_domain(t) && t != '.' && t != '_') {
+ goto out;
+ }
+ else if (p - c > max_domain_length) {
+ goto out;
+ }
+ p++;
+ break;
+ case parse_suffix_question:
+ c = p;
+ st = parse_query;
+ break;
+ case parse_query:
+ if (t == '#') {
+ if (p - c != 0) {
+ SET_U(u, UF_QUERY);
+ }
+ c = p + 1;
+ ret = 0;
+
+ goto out;
+ }
+ else if (!(parse_flags & RSPAMD_URL_PARSE_HREF) && is_url_end(t)) {
+ ret = 0;
+ goto out;
+ }
+ else if (is_lwsp(t)) {
+ if (!(parse_flags & RSPAMD_URL_PARSE_CHECK)) {
+ if (g_ascii_isspace(t)) {
+ ret = 0;
+ }
+ goto out;
+ }
+ else {
+ goto out;
+ }
+ }
+ p++;
+ break;
+ }
+ }
+
+ if (st == parse_domain) {
+ if (p - c != 0) {
+ SET_U(u, UF_HOST);
+ ret = 0;
+ }
+ }
+ else if (st == parse_query) {
+ if (p - c > 0) {
+ SET_U(u, UF_QUERY);
+ }
+
+ ret = 0;
+ }
+
+out:
+ if (end != NULL) {
+ *end = p;
+ }
+
+ if ((parse_flags & RSPAMD_URL_PARSE_CHECK)) {
+ return 0;
+ }
+
+ return ret;
+}
+
+static gint
+rspamd_telephone_parse(struct http_parser_url *u,
+ const gchar *str, gsize len,
+ gchar const **end,
+ enum rspamd_url_parse_flags parse_flags,
+ guint *flags)
+{
+ enum {
+ parse_protocol,
+ parse_semicolon,
+ parse_slash,
+ parse_slash_slash,
+ parse_spaces,
+ parse_plus,
+ parse_phone_start,
+ parse_phone,
+ } st = parse_protocol;
+
+ const gchar *p = str, *c = str, *last = str + len;
+ gchar t;
+ gint ret = 1, i;
+ UChar32 uc;
+
+ if (u != NULL) {
+ memset(u, 0, sizeof(*u));
+ }
+
+ while (p < last) {
+ t = *p;
+
+ if (p - str > max_email_user) {
+ goto out;
+ }
+
+ switch (st) {
+ case parse_protocol:
+ if (t == ':') {
+ st = parse_semicolon;
+ SET_U(u, UF_SCHEMA);
+ }
+ p++;
+ break;
+ case parse_semicolon:
+ if (t == '/' || t == '\\') {
+ st = parse_slash;
+ p++;
+ }
+ else {
+ st = parse_slash_slash;
+ }
+ break;
+ case parse_slash:
+ if (t == '/' || t == '\\') {
+ st = parse_slash_slash;
+ }
+ else {
+ goto out;
+ }
+ p++;
+ break;
+ case parse_slash_slash:
+ if (g_ascii_isspace(t)) {
+ st = parse_spaces;
+ p++;
+ }
+ else if (t == '+') {
+ c = p;
+ st = parse_plus;
+ }
+ else if (t == '/') {
+ /* Skip multiple slashes */
+ p++;
+ }
+ else {
+ st = parse_phone_start;
+ c = p;
+ }
+ break;
+ case parse_spaces:
+ if (t == '+') {
+ c = p;
+ st = parse_plus;
+ }
+ else if (!g_ascii_isspace(t)) {
+ st = parse_phone_start;
+ c = p;
+ }
+ else {
+ p++;
+ }
+ break;
+ case parse_plus:
+ c = p;
+ p++;
+ st = parse_phone_start;
+ break;
+ case parse_phone_start:
+ if (*p == '%' || *p == '(' || g_ascii_isdigit(*p)) {
+ st = parse_phone;
+ p++;
+ }
+ else {
+ goto out;
+ }
+ break;
+ case parse_phone:
+ i = p - str;
+ U8_NEXT(str, i, len, uc);
+ p = str + i;
+
+ if (u_isdigit(uc) || uc == '(' || uc == ')' || uc == '[' || uc == ']' || u_isspace(uc) || uc == '%') {
+ /* p is already incremented by U8_NEXT! */
+ }
+ else if (uc <= 0 || is_url_end(uc)) {
+ ret = 0;
+ goto set;
+ }
+ break;
+ }
+ }
+
+set:
+ if (st == parse_phone) {
+ if (p - c != 0) {
+ SET_U(u, UF_HOST);
+ ret = 0;
+ }
+ }
+
+out:
+ if (end != NULL) {
+ *end = p;
+ }
+
+ if ((parse_flags & RSPAMD_URL_PARSE_CHECK)) {
+ return 0;
+ }
+
+ return ret;
+}
+
+static gint
+rspamd_web_parse(struct http_parser_url *u, const gchar *str, gsize len,
+ gchar const **end,
+ enum rspamd_url_parse_flags parse_flags,
+ guint *flags)
+{
+ const gchar *p = str, *c = str, *last = str + len, *slash = NULL,
+ *password_start = NULL, *user_start = NULL;
+ gchar t = 0;
+ UChar32 uc;
+ glong pt;
+ gint ret = 1;
+ gboolean user_seen = FALSE;
+ enum {
+ parse_protocol,
+ parse_slash,
+ parse_slash_slash,
+ parse_semicolon,
+ parse_user,
+ parse_at,
+ parse_multiple_at,
+ parse_password_start,
+ parse_password,
+ parse_domain_start,
+ parse_domain,
+ parse_ipv6,
+ parse_port_password,
+ parse_port,
+ parse_suffix_slash,
+ parse_path,
+ parse_query,
+ parse_part
+ } st = parse_protocol;
+
+ if (u != NULL) {
+ memset(u, 0, sizeof(*u));
+ }
+
+ while (p < last) {
+ t = *p;
+
+ switch (st) {
+ case parse_protocol:
+ if (t == ':') {
+ st = parse_semicolon;
+ SET_U(u, UF_SCHEMA);
+ }
+ else if (!g_ascii_isalnum(t) && t != '+' && t != '-') {
+ if ((parse_flags & RSPAMD_URL_PARSE_CHECK) && p > c) {
+ /* We might have some domain, but no protocol */
+ st = parse_domain_start;
+ p = c;
+ slash = c;
+ break;
+ }
+ else {
+ goto out;
+ }
+ }
+ p++;
+ break;
+ case parse_semicolon:
+ if (t == '/' || t == '\\') {
+ st = parse_slash;
+ p++;
+ }
+ else {
+ st = parse_slash_slash;
+ *(flags) |= RSPAMD_URL_FLAG_MISSINGSLASHES;
+ }
+ break;
+ case parse_slash:
+ if (t == '/' || t == '\\') {
+ st = parse_slash_slash;
+ }
+ else {
+ goto out;
+ }
+ p++;
+ break;
+ case parse_slash_slash:
+
+ if (t != '/' && t != '\\') {
+ c = p;
+ slash = p;
+ st = parse_domain_start;
+
+ /*
+ * Unfortunately, due to brain damage of the RFC 3986 authors,
+ * we have to distinguish two possibilities here:
+ * authority = [ userinfo "@" ] host [ ":" port ]
+ * So if we have @ somewhere before hostname then we must process
+ * with the username state. Otherwise, we have to process via
+ * the hostname state. Unfortunately, there is no way to distinguish
+ * them aside of running NFA or two DFA or performing lookahead.
+ * Lookahead approach looks easier to implement.
+ */
+
+ const char *tp = p;
+ while (tp < last) {
+ if (*tp == '@') {
+ user_seen = TRUE;
+ st = parse_user;
+ break;
+ }
+ else if (*tp == '/' || *tp == '#' || *tp == '?') {
+ st = parse_domain_start;
+ break;
+ }
+
+ tp++;
+ }
+
+ if (st == parse_domain_start && *p == '[') {
+ st = parse_ipv6;
+ p++;
+ c = p;
+ }
+ }
+ else {
+ /* Skip multiple slashes */
+ p++;
+ }
+ break;
+ case parse_ipv6:
+ if (t == ']') {
+ if (p - c == 0) {
+ goto out;
+ }
+ SET_U(u, UF_HOST);
+ p++;
+
+ if (*p == ':') {
+ st = parse_port;
+ c = p + 1;
+ }
+ else if (*p == '/' || *p == '\\') {
+ st = parse_path;
+ c = p + 1;
+ }
+ else if (*p == '?') {
+ st = parse_query;
+ c = p + 1;
+ }
+ else if (*p == '#') {
+ st = parse_part;
+ c = p + 1;
+ }
+ else if (p != last) {
+ goto out;
+ }
+ }
+ else if (!g_ascii_isxdigit(t) && t != ':' && t != '.') {
+ goto out;
+ }
+ p++;
+ break;
+ case parse_user:
+ if (t == ':') {
+ if (p - c == 0) {
+ goto out;
+ }
+ user_start = c;
+ st = parse_password_start;
+ }
+ else if (t == '@') {
+ /* No password */
+ if (p - c == 0) {
+ /* We have multiple at in fact */
+ st = parse_multiple_at;
+ user_seen = TRUE;
+ *flags |= RSPAMD_URL_FLAG_OBSCURED;
+
+ continue;
+ }
+
+ SET_U(u, UF_USERINFO);
+ *flags |= RSPAMD_URL_FLAG_HAS_USER;
+ st = parse_at;
+ }
+ else if (!g_ascii_isgraph(t)) {
+ goto out;
+ }
+ else if (p - c > max_email_user) {
+ goto out;
+ }
+
+ p++;
+ break;
+ case parse_multiple_at:
+ if (t != '@') {
+ if (p - c == 0) {
+ goto out;
+ }
+
+ /* For now, we ignore all that stuff as it is bogus */
+ /* Off by one */
+ p--;
+ SET_U(u, UF_USERINFO);
+ p++;
+ *flags |= RSPAMD_URL_FLAG_HAS_USER;
+ st = parse_at;
+ }
+ else {
+ p++;
+ }
+ break;
+ case parse_password_start:
+ if (t == '@') {
+ /* Empty password */
+ SET_U(u, UF_USERINFO);
+ if (u != NULL && u->field_data[UF_USERINFO].len > 0) {
+ /* Eat semicolon */
+ u->field_data[UF_USERINFO].len--;
+ }
+ *flags |= RSPAMD_URL_FLAG_HAS_USER;
+ st = parse_at;
+ }
+ else {
+ c = p;
+ password_start = p;
+ st = parse_password;
+ }
+ p++;
+ break;
+ case parse_password:
+ if (t == '@') {
+ /* XXX: password is not stored */
+ if (u != NULL) {
+ if (u->field_data[UF_USERINFO].len == 0 && password_start && user_start && password_start > user_start + 1) {
+ *flags |= RSPAMD_URL_FLAG_HAS_USER;
+ u->field_set |= 1u << (UF_USERINFO);
+ u->field_data[UF_USERINFO].len =
+ password_start - user_start - 1;
+ u->field_data[UF_USERINFO].off =
+ user_start - str;
+ }
+ }
+ st = parse_at;
+ }
+ else if (!g_ascii_isgraph(t)) {
+ goto out;
+ }
+ else if (p - c > max_domain_length) {
+ goto out;
+ }
+ p++;
+ break;
+ case parse_at:
+ c = p;
+
+ if (t == '@') {
+ *flags |= RSPAMD_URL_FLAG_OBSCURED;
+ p++;
+ }
+ else if (t == '[') {
+ st = parse_ipv6;
+ p++;
+ c = p;
+ }
+ else {
+ st = parse_domain_start;
+ }
+ break;
+ case parse_domain_start:
+ if (is_domain_start(t)) {
+ st = parse_domain;
+ }
+ else {
+ goto out;
+ }
+ break;
+ case parse_domain:
+ if (p - c > max_domain_length) {
+ /* Too large domain */
+ goto out;
+ }
+ if (t == '/' || t == '\\' || t == ':' || t == '?' || t == '#') {
+ if (p - c == 0) {
+ goto out;
+ }
+ if (t == '/' || t == '\\') {
+ SET_U(u, UF_HOST);
+ st = parse_suffix_slash;
+ }
+ else if (t == '?') {
+ SET_U(u, UF_HOST);
+ st = parse_query;
+ c = p + 1;
+ }
+ else if (t == '#') {
+ SET_U(u, UF_HOST);
+ st = parse_part;
+ c = p + 1;
+ }
+ else if (t == ':' && !user_seen) {
+ /*
+ * Here we can have both port and password, hence we need
+ * to apply some heuristic here
+ */
+ st = parse_port_password;
+ }
+ else {
+ /*
+ * We can go only for parsing port here
+ */
+ SET_U(u, UF_HOST);
+ st = parse_port;
+ c = p + 1;
+ }
+ p++;
+ }
+ else {
+ if (is_url_end(t) || is_url_start(t)) {
+ goto set;
+ }
+ else if (*p == '@' && !user_seen) {
+ /* We need to fallback and test user */
+ p = slash;
+ user_seen = TRUE;
+ st = parse_user;
+ }
+ else if (*p != '.' && *p != '-' && *p != '_' && *p != '%') {
+ if (*p & 0x80) {
+ guint i = 0;
+
+ U8_NEXT(((const guchar *) p), i, last - p, uc);
+
+ if (uc < 0) {
+ /* Bad utf8 */
+ goto out;
+ }
+
+ if (!u_isalnum(uc)) {
+ /* Bad symbol */
+ if (IS_ZERO_WIDTH_SPACE(uc)) {
+ (*flags) |= RSPAMD_URL_FLAG_ZW_SPACES;
+ }
+ else {
+ if (!u_isgraph(uc)) {
+ if (!(parse_flags & RSPAMD_URL_PARSE_CHECK)) {
+ goto out;
+ }
+ else {
+ goto set;
+ }
+ }
+ }
+ }
+ else {
+ (*flags) |= RSPAMD_URL_FLAG_IDN;
+ }
+
+ p = p + i;
+ }
+ else if (is_urlsafe(*p)) {
+ p++;
+ }
+ else {
+ if (parse_flags & RSPAMD_URL_PARSE_HREF) {
+ /* We have to use all shit we are given here */
+ p++;
+ (*flags) |= RSPAMD_URL_FLAG_OBSCURED;
+ }
+ else {
+ if (!(parse_flags & RSPAMD_URL_PARSE_CHECK)) {
+ goto out;
+ }
+ else {
+ goto set;
+ }
+ }
+ }
+ }
+ else {
+ p++;
+ }
+ }
+ break;
+ case parse_port_password:
+ if (g_ascii_isdigit(t)) {
+ const gchar *tmp = p;
+
+ while (tmp < last) {
+ if (!g_ascii_isdigit(*tmp)) {
+ if (*tmp == '/' || *tmp == '#' || *tmp == '?' ||
+ is_url_end(*tmp) || g_ascii_isspace(*tmp)) {
+ /* Port + something */
+ st = parse_port;
+ c = slash;
+ p--;
+ SET_U(u, UF_HOST);
+ p++;
+ c = p;
+ break;
+ }
+ else {
+ /* Not a port, bad character at the end */
+ break;
+ }
+ }
+ tmp++;
+ }
+
+ if (tmp == last) {
+ /* Host + port only */
+ st = parse_port;
+ c = slash;
+ p--;
+ SET_U(u, UF_HOST);
+ p++;
+ c = p;
+ }
+
+ if (st != parse_port) {
+ /* Fallback to user:password */
+ p = slash;
+ c = slash;
+ user_seen = TRUE;
+ st = parse_user;
+ }
+ }
+ else {
+ /* Rewind back */
+ p = slash;
+ c = slash;
+ user_seen = TRUE;
+ st = parse_user;
+ }
+ break;
+ case parse_port:
+ if (t == '/' || t == '\\') {
+ pt = strtoul(c, NULL, 10);
+ if (pt == 0 || pt > 65535) {
+ goto out;
+ }
+ if (u != NULL) {
+ u->port = pt;
+ *flags |= RSPAMD_URL_FLAG_HAS_PORT;
+ }
+ st = parse_suffix_slash;
+ }
+ else if (t == '?') {
+ pt = strtoul(c, NULL, 10);
+ if (pt == 0 || pt > 65535) {
+ goto out;
+ }
+ if (u != NULL) {
+ u->port = pt;
+ *flags |= RSPAMD_URL_FLAG_HAS_PORT;
+ }
+
+ c = p + 1;
+ st = parse_query;
+ }
+ else if (t == '#') {
+ pt = strtoul(c, NULL, 10);
+ if (pt == 0 || pt > 65535) {
+ goto out;
+ }
+ if (u != NULL) {
+ u->port = pt;
+ *flags |= RSPAMD_URL_FLAG_HAS_PORT;
+ }
+
+ c = p + 1;
+ st = parse_part;
+ }
+ else if (is_url_end(t)) {
+ goto set;
+ }
+ else if (!g_ascii_isdigit(t)) {
+ if (!(parse_flags & RSPAMD_URL_PARSE_CHECK) ||
+ !g_ascii_isspace(t)) {
+ goto out;
+ }
+ else {
+ goto set;
+ }
+ }
+ p++;
+ break;
+ case parse_suffix_slash:
+ if (t != '/' && t != '\\') {
+ c = p;
+ st = parse_path;
+ }
+ else {
+ /* Skip extra slashes */
+ p++;
+ }
+ break;
+ case parse_path:
+ if (t == '?') {
+ if (p - c != 0) {
+ SET_U(u, UF_PATH);
+ }
+ c = p + 1;
+ st = parse_query;
+ }
+ else if (t == '#') {
+ /* No query, just fragment */
+ if (p - c != 0) {
+ SET_U(u, UF_PATH);
+ }
+ c = p + 1;
+ st = parse_part;
+ }
+ else if (!(parse_flags & RSPAMD_URL_PARSE_HREF) && is_url_end(t)) {
+ goto set;
+ }
+ else if (is_lwsp(t)) {
+ if (!(parse_flags & RSPAMD_URL_PARSE_CHECK)) {
+ if (g_ascii_isspace(t)) {
+ goto set;
+ }
+ goto out;
+ }
+ else {
+ goto set;
+ }
+ }
+ p++;
+ break;
+ case parse_query:
+ if (t == '#') {
+ if (p - c != 0) {
+ SET_U(u, UF_QUERY);
+ }
+ c = p + 1;
+ st = parse_part;
+ }
+ else if (!(parse_flags & RSPAMD_URL_PARSE_HREF) && is_url_end(t)) {
+ goto set;
+ }
+ else if (is_lwsp(t)) {
+ if (!(parse_flags & RSPAMD_URL_PARSE_CHECK)) {
+ if (g_ascii_isspace(t)) {
+ goto set;
+ }
+ goto out;
+ }
+ else {
+ goto set;
+ }
+ }
+ p++;
+ break;
+ case parse_part:
+ if (!(parse_flags & RSPAMD_URL_PARSE_HREF) && is_url_end(t)) {
+ goto set;
+ }
+ else if (is_lwsp(t)) {
+ if (!(parse_flags & RSPAMD_URL_PARSE_CHECK)) {
+ if (g_ascii_isspace(t)) {
+ goto set;
+ }
+ goto out;
+ }
+ else {
+ goto set;
+ }
+ }
+ p++;
+ break;
+ }
+ }
+
+set:
+ /* Parse remaining */
+ switch (st) {
+ case parse_domain:
+ if (p - c == 0 || !is_domain(*(p - 1)) || !is_domain(*c)) {
+ goto out;
+ }
+ SET_U(u, UF_HOST);
+ ret = 0;
+
+ break;
+ case parse_port:
+ pt = strtoul(c, NULL, 10);
+ if (pt == 0 || pt > 65535) {
+ goto out;
+ }
+ if (u != NULL) {
+ u->port = pt;
+ }
+
+ ret = 0;
+ break;
+ case parse_suffix_slash:
+ /* Url ends with '/' */
+ ret = 0;
+ break;
+ case parse_path:
+ if (p - c > 0) {
+ SET_U(u, UF_PATH);
+ }
+ ret = 0;
+ break;
+ case parse_query:
+ if (p - c > 0) {
+ SET_U(u, UF_QUERY);
+ }
+ ret = 0;
+ break;
+ case parse_part:
+ if (p - c > 0) {
+ SET_U(u, UF_FRAGMENT);
+ }
+ ret = 0;
+ break;
+ case parse_ipv6:
+ if (t != ']') {
+ ret = 1;
+ }
+ else {
+ /* e.g. http://[::] */
+ ret = 0;
+ }
+ break;
+ default:
+ /* Error state */
+ ret = 1;
+ break;
+ }
+out:
+ if (end != NULL) {
+ *end = p;
+ }
+
+ return ret;
+}
+
+#undef SET_U
+
+static gint
+rspamd_tld_trie_callback(struct rspamd_multipattern *mp,
+ guint strnum,
+ gint match_start,
+ gint match_pos,
+ const gchar *text,
+ gsize len,
+ void *context)
+{
+ struct url_matcher *matcher;
+ const gchar *start, *pos, *p;
+ struct rspamd_url *url = context;
+ gint ndots;
+
+ matcher = &g_array_index(url_scanner->matchers_full, struct url_matcher,
+ strnum);
+ ndots = 1;
+
+ if (matcher->flags & URL_MATCHER_FLAG_STAR_MATCH) {
+ /* Skip one more tld component */
+ ndots++;
+ }
+
+ pos = text + match_start;
+ p = pos - 1;
+ start = rspamd_url_host_unsafe(url);
+
+ if (*pos != '.' || match_pos != (gint) url->hostlen) {
+ /* Something weird has been found */
+ if (match_pos == (gint) url->hostlen - 1) {
+ pos = rspamd_url_host_unsafe(url) + match_pos;
+ if (*pos == '.') {
+ /* This is dot at the end of domain */
+ url->hostlen--;
+ }
+ else {
+ return 0;
+ }
+ }
+ else {
+ return 0;
+ }
+ }
+
+ /* Now we need to find top level domain */
+ pos = start;
+ while (p >= start && ndots > 0) {
+ if (*p == '.') {
+ ndots--;
+ pos = p + 1;
+ }
+ else {
+ pos = p;
+ }
+
+ p--;
+ }
+
+ if ((ndots == 0 || p == start - 1) &&
+ url->tldlen < rspamd_url_host_unsafe(url) + url->hostlen - pos) {
+ url->tldshift = (pos - url->string);
+ url->tldlen = rspamd_url_host_unsafe(url) + url->hostlen - pos;
+ }
+
+ return 0;
+}
+
+static void
+rspamd_url_regen_from_inet_addr(struct rspamd_url *uri, const void *addr, int af,
+ rspamd_mempool_t *pool)
+{
+ gchar *strbuf, *p;
+ const gchar *start_offset;
+ gsize slen = uri->urllen - uri->hostlen;
+ goffset r = 0;
+
+ if (af == AF_INET) {
+ slen += INET_ADDRSTRLEN;
+ }
+ else {
+ slen += INET6_ADDRSTRLEN;
+ }
+
+ if (uri->flags & RSPAMD_URL_FLAG_HAS_PORT) {
+ slen += sizeof("65535") - 1;
+ }
+
+ /* Allocate new string to build it from IP */
+ strbuf = rspamd_mempool_alloc(pool, slen + 1);
+ r += rspamd_snprintf(strbuf + r, slen - r, "%*s",
+ (gint) (uri->hostshift),
+ uri->string);
+
+ uri->hostshift = r;
+ uri->tldshift = r;
+ start_offset = strbuf + r;
+ inet_ntop(af, addr, strbuf + r, slen - r + 1);
+ uri->hostlen = strlen(start_offset);
+ r += uri->hostlen;
+ uri->tldlen = uri->hostlen;
+ uri->flags |= RSPAMD_URL_FLAG_NUMERIC;
+
+ /* Reconstruct URL */
+ if (uri->flags & RSPAMD_URL_FLAG_HAS_PORT && uri->ext) {
+ p = strbuf + r;
+ start_offset = p + 1;
+ r += rspamd_snprintf(strbuf + r, slen - r, ":%ud",
+ (unsigned int) uri->ext->port);
+ }
+ if (uri->datalen > 0) {
+ p = strbuf + r;
+ start_offset = p + 1;
+ r += rspamd_snprintf(strbuf + r, slen - r, "/%*s",
+ (gint) uri->datalen,
+ rspamd_url_data_unsafe(uri));
+ uri->datashift = start_offset - strbuf;
+ }
+ else {
+ /* Add trailing slash if needed */
+ if (uri->hostlen + uri->hostshift < uri->urllen &&
+ *(rspamd_url_host_unsafe(uri) + uri->hostlen) == '/') {
+ r += rspamd_snprintf(strbuf + r, slen - r, "/");
+ }
+ }
+
+ if (uri->querylen > 0) {
+ p = strbuf + r;
+ start_offset = p + 1;
+ r += rspamd_snprintf(strbuf + r, slen - r, "?%*s",
+ (gint) uri->querylen,
+ rspamd_url_query_unsafe(uri));
+ uri->queryshift = start_offset - strbuf;
+ }
+ if (uri->fragmentlen > 0) {
+ p = strbuf + r;
+ start_offset = p + 1;
+ r += rspamd_snprintf(strbuf + r, slen - r, "#%*s",
+ (gint) uri->fragmentlen,
+ rspamd_url_fragment_unsafe(uri));
+ uri->fragmentshift = start_offset - strbuf;
+ }
+
+ uri->string = strbuf;
+ uri->urllen = r;
+}
+
+static gboolean
+rspamd_url_maybe_regenerate_from_ip(struct rspamd_url *uri, rspamd_mempool_t *pool)
+{
+ const gchar *p, *end, *c;
+ gchar *errstr;
+ struct in_addr in4;
+ struct in6_addr in6;
+ gboolean ret = FALSE, check_num = TRUE;
+ guint32 n, dots, t = 0, i = 0, shift, nshift;
+
+ p = rspamd_url_host_unsafe(uri);
+ end = p + uri->hostlen;
+
+ if (*p == '[' && *(end - 1) == ']') {
+ p++;
+ end--;
+ }
+
+ while (*(end - 1) == '.' && end > p) {
+ end--;
+ }
+
+ if (end - p == 0 || end - p > INET6_ADDRSTRLEN) {
+ return FALSE;
+ }
+
+ if (rspamd_str_has_8bit(p, end - p)) {
+ return FALSE;
+ }
+
+ if (rspamd_parse_inet_address_ip4(p, end - p, &in4)) {
+ rspamd_url_regen_from_inet_addr(uri, &in4, AF_INET, pool);
+ ret = TRUE;
+ }
+ else if (rspamd_parse_inet_address_ip6(p, end - p, &in6)) {
+ rspamd_url_regen_from_inet_addr(uri, &in6, AF_INET6, pool);
+ ret = TRUE;
+ }
+ else {
+ /* Heuristics for broken urls */
+ gchar buf[INET6_ADDRSTRLEN + 1];
+ /* Try also numeric notation */
+ c = p;
+ n = 0;
+ dots = 0;
+ shift = 0;
+
+ while (p <= end && check_num) {
+ if (shift < 32 &&
+ ((*p == '.' && dots < 3) || (p == end && dots <= 3))) {
+ if (p - c + 1 >= (gint) sizeof(buf)) {
+ msg_debug_pool("invalid numeric url %*.s...: too long",
+ INET6_ADDRSTRLEN, c);
+ return FALSE;
+ }
+
+ rspamd_strlcpy(buf, c, p - c + 1);
+ c = p + 1;
+
+ if (p < end && *p == '.') {
+ dots++;
+ }
+
+ glong long_n = strtol(buf, &errstr, 0);
+
+ if ((errstr == NULL || *errstr == '\0') && long_n >= 0) {
+
+ t = long_n; /* Truncate as windows does */
+ /*
+ * Even if we have zero, we need to shift by 1 octet
+ */
+ nshift = (t == 0 ? shift + 8 : shift);
+
+ /*
+ * Here we count number of octets encoded in this element
+ */
+ for (i = 0; i < 4; i++) {
+ if ((t >> (8 * i)) > 0) {
+ nshift += 8;
+ }
+ else {
+ break;
+ }
+ }
+ /*
+ * Here we need to find the proper shift of the previous
+ * components, so we check possible cases:
+ * 1) 1 octet - just use it applying shift
+ * 2) 2 octets - convert to big endian 16 bit number
+ * 3) 3 octets - convert to big endian 24 bit number
+ * 4) 4 octets - convert to big endian 32 bit number
+ */
+ switch (i) {
+ case 4:
+ t = GUINT32_TO_BE(t);
+ break;
+ case 3:
+ t = (GUINT32_TO_BE(t & 0xFFFFFFU)) >> 8;
+ break;
+ case 2:
+ t = GUINT16_TO_BE(t & 0xFFFFU);
+ break;
+ default:
+ t = t & 0xFF;
+ break;
+ }
+
+ if (p != end) {
+ n |= t << shift;
+
+ shift = nshift;
+ }
+ }
+ else {
+ check_num = FALSE;
+ }
+ }
+
+ p++;
+ }
+
+ /* The last component should be last according to url normalization:
+ * 192.168.1 -> 192.168.0.1
+ * 192 -> 0.0.0.192
+ * 192.168 -> 192.0.0.168
+ */
+ shift = 8 * (4 - i);
+
+ if (shift < 32) {
+ n |= t << shift;
+ }
+
+ if (check_num) {
+ if (dots <= 4) {
+ memcpy(&in4, &n, sizeof(in4));
+ rspamd_url_regen_from_inet_addr(uri, &in4, AF_INET, pool);
+ uri->flags |= RSPAMD_URL_FLAG_OBSCURED;
+ ret = TRUE;
+ }
+ else if (end - c > (gint) sizeof(buf) - 1) {
+ rspamd_strlcpy(buf, c, end - c + 1);
+
+ if (inet_pton(AF_INET6, buf, &in6) == 1) {
+ rspamd_url_regen_from_inet_addr(uri, &in6, AF_INET6, pool);
+ uri->flags |= RSPAMD_URL_FLAG_OBSCURED;
+ ret = TRUE;
+ }
+ }
+ }
+ }
+
+ return ret;
+}
+
+static void
+rspamd_url_shift(struct rspamd_url *uri, gsize nlen,
+ enum http_parser_url_fields field)
+{
+ guint old_shift, shift = 0;
+ gint remain;
+
+ /* Shift remaining data */
+ switch (field) {
+ case UF_SCHEMA:
+ if (nlen >= uri->protocollen) {
+ return;
+ }
+ else {
+ shift = uri->protocollen - nlen;
+ }
+
+ old_shift = uri->protocollen;
+ uri->protocollen -= shift;
+ remain = uri->urllen - uri->protocollen;
+ g_assert(remain >= 0);
+ memmove(uri->string + uri->protocollen, uri->string + old_shift,
+ remain);
+ uri->urllen -= shift;
+ uri->flags |= RSPAMD_URL_FLAG_SCHEMAENCODED;
+ break;
+ case UF_HOST:
+ if (nlen >= uri->hostlen) {
+ return;
+ }
+ else {
+ shift = uri->hostlen - nlen;
+ }
+
+ old_shift = uri->hostlen;
+ uri->hostlen -= shift;
+ remain = (uri->urllen - (uri->hostshift)) - old_shift;
+ g_assert(remain >= 0);
+ memmove(rspamd_url_host_unsafe(uri) + uri->hostlen,
+ rspamd_url_host_unsafe(uri) + old_shift,
+ remain);
+ uri->urllen -= shift;
+ uri->flags |= RSPAMD_URL_FLAG_HOSTENCODED;
+ break;
+ case UF_PATH:
+ if (nlen >= uri->datalen) {
+ return;
+ }
+ else {
+ shift = uri->datalen - nlen;
+ }
+
+ old_shift = uri->datalen;
+ uri->datalen -= shift;
+ remain = (uri->urllen - (uri->datashift)) - old_shift;
+ g_assert(remain >= 0);
+ memmove(rspamd_url_data_unsafe(uri) + uri->datalen,
+ rspamd_url_data_unsafe(uri) + old_shift,
+ remain);
+ uri->urllen -= shift;
+ uri->flags |= RSPAMD_URL_FLAG_PATHENCODED;
+ break;
+ case UF_QUERY:
+ if (nlen >= uri->querylen) {
+ return;
+ }
+ else {
+ shift = uri->querylen - nlen;
+ }
+
+ old_shift = uri->querylen;
+ uri->querylen -= shift;
+ remain = (uri->urllen - (uri->queryshift)) - old_shift;
+ g_assert(remain >= 0);
+ memmove(rspamd_url_query_unsafe(uri) + uri->querylen,
+ rspamd_url_query_unsafe(uri) + old_shift,
+ remain);
+ uri->urllen -= shift;
+ uri->flags |= RSPAMD_URL_FLAG_QUERYENCODED;
+ break;
+ case UF_FRAGMENT:
+ if (nlen >= uri->fragmentlen) {
+ return;
+ }
+ else {
+ shift = uri->fragmentlen - nlen;
+ }
+
+ uri->fragmentlen -= shift;
+ uri->urllen -= shift;
+ break;
+ default:
+ break;
+ }
+
+ /* Now adjust lengths and offsets */
+ switch (field) {
+ case UF_SCHEMA:
+ if (uri->userlen > 0) {
+ uri->usershift -= shift;
+ }
+ if (uri->hostlen > 0) {
+ uri->hostshift -= shift;
+ }
+ /* Go forward */
+ /* FALLTHRU */
+ case UF_HOST:
+ if (uri->datalen > 0) {
+ uri->datashift -= shift;
+ }
+ /* Go forward */
+ /* FALLTHRU */
+ case UF_PATH:
+ if (uri->querylen > 0) {
+ uri->queryshift -= shift;
+ }
+ /* Go forward */
+ /* FALLTHRU */
+ case UF_QUERY:
+ if (uri->fragmentlen > 0) {
+ uri->fragmentshift -= shift;
+ }
+ /* Go forward */
+ /* FALLTHRU */
+ case UF_FRAGMENT:
+ default:
+ break;
+ }
+}
+
+static void
+rspamd_telephone_normalise_inplace(struct rspamd_url *uri)
+{
+ gchar *t, *h, *end;
+ gint i = 0, w, orig_len;
+ UChar32 uc;
+
+ t = rspamd_url_host_unsafe(uri);
+ h = t;
+ end = t + uri->hostlen;
+ orig_len = uri->hostlen;
+
+ if (*h == '+') {
+ h++;
+ t++;
+ }
+
+ while (h < end) {
+ i = 0;
+ U8_NEXT(h, i, end - h, uc);
+
+ if (u_isdigit(uc)) {
+ w = 0;
+ U8_APPEND_UNSAFE(t, w, uc);
+ t += w;
+ }
+
+ h += i;
+ }
+
+ uri->hostlen = t - rspamd_url_host_unsafe(uri);
+ uri->urllen -= (orig_len - uri->hostlen);
+}
+
+static inline bool
+is_idna_label_dot(UChar ch)
+{
+ switch (ch) {
+ case 0x3002:
+ case 0xFF0E:
+ case 0xFF61:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/*
+ * All credits for this investigation should go to
+ * Dr. Hajime Shimada and Mr. Shirakura as they have revealed this case in their
+ * research.
+ */
+
+/*
+ * This function replaces unsafe IDNA dots in host labels. Unfortunately,
+ * IDNA extends dot definition from '.' to multiple other characters that
+ * should be treated equally.
+ * This function replaces such dots and returns `true` if these dots are found.
+ * In this case, it should be treated as obfuscation attempt.
+ */
+static bool
+rspamd_url_remove_dots(struct rspamd_url *uri)
+{
+ const gchar *hstart = rspamd_url_host_unsafe(uri);
+ gchar *t;
+ UChar32 uc;
+ gint i = 0, hlen;
+ bool ret = false;
+
+ if (uri->hostlen == 0) {
+ return false;
+ }
+
+ hlen = uri->hostlen;
+ t = rspamd_url_host_unsafe(uri);
+
+ while (i < hlen) {
+ gint prev_i = i;
+ U8_NEXT(hstart, i, hlen, uc);
+
+ if (is_idna_label_dot(uc)) {
+ *t++ = '.';
+ ret = true;
+ }
+ else {
+ if (ret) {
+ /* We have to shift the remaining stuff */
+ while (prev_i < i) {
+ *t++ = *(hstart + prev_i);
+ prev_i++;
+ }
+ }
+ else {
+ t += (i - prev_i);
+ }
+ }
+ }
+
+ if (ret) {
+ rspamd_url_shift(uri, t - hstart, UF_HOST);
+ }
+
+ return ret;
+}
+
+enum uri_errno
+rspamd_url_parse(struct rspamd_url *uri,
+ gchar *uristring, gsize len,
+ rspamd_mempool_t *pool,
+ enum rspamd_url_parse_flags parse_flags)
+{
+ struct http_parser_url u;
+ gchar *p;
+ const gchar *end;
+ guint complen, ret, flags = 0;
+ gsize unquoted_len = 0;
+
+ memset(uri, 0, sizeof(*uri));
+ memset(&u, 0, sizeof(u));
+ uri->count = 1;
+ /* Undefine order */
+ uri->order = -1;
+ uri->part_order = -1;
+
+ if (*uristring == '\0') {
+ return URI_ERRNO_EMPTY;
+ }
+
+ if (len >= G_MAXUINT16 / 2) {
+ flags |= RSPAMD_URL_FLAG_TRUNCATED;
+ len = G_MAXUINT16 / 2;
+ }
+
+ p = uristring;
+ uri->protocol = PROTOCOL_UNKNOWN;
+
+ if (len > sizeof("mailto:") - 1) {
+ /* For mailto: urls we also need to add slashes to make it a valid URL */
+ if (g_ascii_strncasecmp(p, "mailto:", sizeof("mailto:") - 1) == 0) {
+ ret = rspamd_mailto_parse(&u, uristring, len, &end, parse_flags,
+ &flags);
+ }
+ else if (g_ascii_strncasecmp(p, "tel:", sizeof("tel:") - 1) == 0 ||
+ g_ascii_strncasecmp(p, "callto:", sizeof("callto:") - 1) == 0) {
+ ret = rspamd_telephone_parse(&u, uristring, len, &end, parse_flags,
+ &flags);
+ uri->protocol = PROTOCOL_TELEPHONE;
+ }
+ else {
+ ret = rspamd_web_parse(&u, uristring, len, &end, parse_flags,
+ &flags);
+ }
+ }
+ else {
+ ret = rspamd_web_parse(&u, uristring, len, &end, parse_flags, &flags);
+ }
+
+ if (ret != 0) {
+ return URI_ERRNO_BAD_FORMAT;
+ }
+
+ if (end > uristring && (guint) (end - uristring) != len) {
+ len = end - uristring;
+ }
+
+ uri->raw = p;
+ uri->rawlen = len;
+
+ if (flags & RSPAMD_URL_FLAG_MISSINGSLASHES) {
+ len += 2;
+ uri->string = rspamd_mempool_alloc(pool, len + 1);
+ memcpy(uri->string, p, u.field_data[UF_SCHEMA].len);
+ memcpy(uri->string + u.field_data[UF_SCHEMA].len, "://", 3);
+ rspamd_strlcpy(uri->string + u.field_data[UF_SCHEMA].len + 3,
+ p + u.field_data[UF_SCHEMA].len + 1,
+ len - 2 - u.field_data[UF_SCHEMA].len);
+ /* Compensate slashes added */
+ for (int i = UF_SCHEMA + 1; i < UF_MAX; i++) {
+ if (u.field_set & (1 << i)) {
+ u.field_data[i].off += 2;
+ }
+ }
+ }
+ else {
+ uri->string = rspamd_mempool_alloc(pool, len + 1);
+ rspamd_strlcpy(uri->string, p, len + 1);
+ }
+
+ uri->urllen = len;
+ uri->flags = flags;
+
+ for (guint i = 0; i < UF_MAX; i++) {
+ if (u.field_set & (1 << i)) {
+ guint shift = u.field_data[i].off;
+ complen = u.field_data[i].len;
+
+ if (complen >= G_MAXUINT16) {
+ /* Too large component length */
+ return URI_ERRNO_BAD_FORMAT;
+ }
+
+ switch (i) {
+ case UF_SCHEMA:
+ uri->protocollen = u.field_data[i].len;
+ break;
+ case UF_HOST:
+ uri->hostshift = shift;
+ uri->hostlen = complen;
+ break;
+ case UF_PATH:
+ uri->datashift = shift;
+ uri->datalen = complen;
+ break;
+ case UF_QUERY:
+ uri->queryshift = shift;
+ uri->querylen = complen;
+ break;
+ case UF_FRAGMENT:
+ uri->fragmentshift = shift;
+ uri->fragmentlen = complen;
+ break;
+ case UF_USERINFO:
+ uri->usershift = shift;
+ uri->userlen = complen;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ /* Port is 'special' in case of url_parser as it is not a part of UF_* macro logic */
+ if (u.port != 0) {
+ if (!uri->ext) {
+ uri->ext = rspamd_mempool_alloc0_type(pool, struct rspamd_url_ext);
+ }
+ uri->flags |= RSPAMD_URL_FLAG_HAS_PORT;
+ uri->ext->port = u.port;
+ }
+
+ if (!uri->hostlen) {
+ return URI_ERRNO_HOST_MISSING;
+ }
+
+ /* Now decode url symbols */
+ unquoted_len = rspamd_url_decode(uri->string,
+ uri->string,
+ uri->protocollen);
+ rspamd_url_shift(uri, unquoted_len, UF_SCHEMA);
+ unquoted_len = rspamd_url_decode(rspamd_url_host_unsafe(uri),
+ rspamd_url_host_unsafe(uri), uri->hostlen);
+
+ rspamd_url_normalise_propagate_flags(pool, rspamd_url_host_unsafe(uri),
+ &unquoted_len, uri->flags);
+
+ rspamd_url_shift(uri, unquoted_len, UF_HOST);
+
+ if (rspamd_url_remove_dots(uri)) {
+ uri->flags |= RSPAMD_URL_FLAG_OBSCURED;
+ }
+
+ if (uri->protocol & (PROTOCOL_HTTP | PROTOCOL_HTTPS | PROTOCOL_MAILTO | PROTOCOL_FTP | PROTOCOL_FILE)) {
+ /* Ensure that hostname starts with something sane (exclude numeric urls) */
+ const gchar *host = rspamd_url_host_unsafe(uri);
+
+ if (!(is_domain_start(host[0]) || host[0] == ':')) {
+ return URI_ERRNO_BAD_FORMAT;
+ }
+ }
+
+ /* Apply nameprep algorithm */
+ static UStringPrepProfile *nameprep = NULL;
+ UErrorCode uc_err = U_ZERO_ERROR;
+
+ if (nameprep == NULL) {
+ /* Open and cache profile */
+ nameprep = usprep_openByType(USPREP_RFC3491_NAMEPREP, &uc_err);
+
+ g_assert(U_SUCCESS(uc_err));
+ }
+
+ UChar *utf16_hostname, *norm_utf16;
+ gint32 utf16_len, norm_utf16_len, norm_utf8_len;
+ UParseError parse_error;
+
+ utf16_hostname = rspamd_mempool_alloc(pool, uri->hostlen * sizeof(UChar));
+ struct UConverter *utf8_conv = rspamd_get_utf8_converter();
+
+ utf16_len = ucnv_toUChars(utf8_conv, utf16_hostname, uri->hostlen,
+ rspamd_url_host_unsafe(uri), uri->hostlen, &uc_err);
+
+ if (!U_SUCCESS(uc_err)) {
+
+ return URI_ERRNO_BAD_FORMAT;
+ }
+
+ norm_utf16 = rspamd_mempool_alloc(pool, utf16_len * sizeof(UChar));
+ norm_utf16_len = usprep_prepare(nameprep, utf16_hostname, utf16_len,
+ norm_utf16, utf16_len, USPREP_DEFAULT, &parse_error, &uc_err);
+
+ if (!U_SUCCESS(uc_err)) {
+
+ return URI_ERRNO_BAD_FORMAT;
+ }
+
+ /* Convert back to utf8, sigh... */
+ norm_utf8_len = ucnv_fromUChars(utf8_conv,
+ rspamd_url_host_unsafe(uri), uri->hostlen,
+ norm_utf16, norm_utf16_len, &uc_err);
+
+ if (!U_SUCCESS(uc_err)) {
+
+ return URI_ERRNO_BAD_FORMAT;
+ }
+
+ /* Final shift of lengths */
+ rspamd_url_shift(uri, norm_utf8_len, UF_HOST);
+
+ /* Process data part */
+ if (uri->datalen) {
+ unquoted_len = rspamd_url_decode(rspamd_url_data_unsafe(uri),
+ rspamd_url_data_unsafe(uri), uri->datalen);
+
+ rspamd_url_normalise_propagate_flags(pool, rspamd_url_data_unsafe(uri),
+ &unquoted_len, uri->flags);
+
+ rspamd_url_shift(uri, unquoted_len, UF_PATH);
+ /* We now normalize path */
+ rspamd_normalize_path_inplace(rspamd_url_data_unsafe(uri),
+ uri->datalen, &unquoted_len);
+ rspamd_url_shift(uri, unquoted_len, UF_PATH);
+ }
+
+ if (uri->querylen) {
+ unquoted_len = rspamd_url_decode(rspamd_url_query_unsafe(uri),
+ rspamd_url_query_unsafe(uri),
+ uri->querylen);
+
+ rspamd_url_normalise_propagate_flags(pool, rspamd_url_query_unsafe(uri),
+ &unquoted_len, uri->flags);
+ rspamd_url_shift(uri, unquoted_len, UF_QUERY);
+ }
+
+ if (uri->fragmentlen) {
+ unquoted_len = rspamd_url_decode(rspamd_url_fragment_unsafe(uri),
+ rspamd_url_fragment_unsafe(uri),
+ uri->fragmentlen);
+
+ rspamd_url_normalise_propagate_flags(pool, rspamd_url_fragment_unsafe(uri),
+ &unquoted_len, uri->flags);
+ rspamd_url_shift(uri, unquoted_len, UF_FRAGMENT);
+ }
+
+ rspamd_str_lc(uri->string, uri->protocollen);
+ unquoted_len = rspamd_str_lc_utf8(rspamd_url_host_unsafe(uri), uri->hostlen);
+ rspamd_url_shift(uri, unquoted_len, UF_HOST);
+
+ if (uri->protocol == PROTOCOL_UNKNOWN) {
+ for (int i = 0; i < G_N_ELEMENTS(rspamd_url_protocols); i++) {
+ if (uri->protocollen == rspamd_url_protocols[i].len) {
+ if (memcmp(uri->string,
+ rspamd_url_protocols[i].name, uri->protocollen) == 0) {
+ uri->protocol = rspamd_url_protocols[i].proto;
+ break;
+ }
+ }
+ }
+ }
+
+ if (uri->protocol & (PROTOCOL_HTTP | PROTOCOL_HTTPS | PROTOCOL_MAILTO | PROTOCOL_FTP | PROTOCOL_FILE)) {
+ /* Find TLD part */
+ if (url_scanner->search_trie_full) {
+ rspamd_multipattern_lookup(url_scanner->search_trie_full,
+ rspamd_url_host_unsafe(uri), uri->hostlen,
+ rspamd_tld_trie_callback, uri, NULL);
+ }
+
+ if (uri->tldlen == 0) {
+ /*
+ * If we have not detected eSLD, but there are no dots in the hostname,
+ * then we should treat the whole hostname as eSLD - a rule of thumb
+ *
+ * We also check that a hostname ends with a permitted character, and all characters are forming
+ * DNS label. We also need to check for a numeric IP within this check.
+ */
+ const char *dot_pos = memchr(rspamd_url_host_unsafe(uri), '.', uri->hostlen);
+ bool is_whole_hostname_tld = false;
+
+ if (uri->hostlen > 0 && (dot_pos == NULL || dot_pos == rspamd_url_host_unsafe(uri) + uri->hostlen - 1)) {
+ bool all_chars_domain = true;
+
+ for (int i = 0; i < uri->hostlen; i++) {
+ if (!is_domain(rspamd_url_host_unsafe(uri)[i])) {
+ all_chars_domain = false;
+ break;
+ }
+ }
+
+ char last_c = rspamd_url_host_unsafe(uri)[uri->hostlen - 1];
+
+ if (all_chars_domain) {
+ /* Also check the last character to be either a dot or alphanumeric character */
+ if (last_c != '.' && !g_ascii_isalnum(last_c)) {
+ all_chars_domain = false;
+ }
+ }
+
+ if (all_chars_domain) {
+ /* Additionally check for a numeric IP as we can have some number here... */
+ rspamd_url_maybe_regenerate_from_ip(uri, pool);
+
+ if (last_c == '.' && uri->hostlen > 1) {
+ /* Skip the last dot */
+ uri->tldlen = uri->hostlen - 1;
+ }
+ else {
+ uri->tldlen = uri->hostlen;
+ }
+
+ uri->tldshift = uri->hostshift;
+ is_whole_hostname_tld = true;
+ }
+ }
+
+ if (!is_whole_hostname_tld) {
+ if (uri->protocol != PROTOCOL_MAILTO) {
+ if (url_scanner->has_tld_file && !(parse_flags & RSPAMD_URL_PARSE_HREF)) {
+ /* Ignore URL's without TLD if it is not a numeric URL */
+ if (!rspamd_url_maybe_regenerate_from_ip(uri, pool)) {
+ return URI_ERRNO_TLD_MISSING;
+ }
+ }
+ else {
+ if (!rspamd_url_maybe_regenerate_from_ip(uri, pool)) {
+ /* Assume tld equal to host */
+ uri->tldshift = uri->hostshift;
+ uri->tldlen = uri->hostlen;
+ }
+ else if (uri->flags & RSPAMD_URL_FLAG_SCHEMALESS) {
+ /* Ignore urls with both no schema and no tld */
+ return URI_ERRNO_TLD_MISSING;
+ }
+
+ uri->flags |= RSPAMD_URL_FLAG_NO_TLD;
+ }
+ }
+ else {
+ /* Ignore IP like domains for mailto, as it is really never supported */
+ return URI_ERRNO_TLD_MISSING;
+ }
+ }
+ }
+
+ /* Replace stupid '\' with '/' after schema */
+ if (uri->protocol & (PROTOCOL_HTTP | PROTOCOL_HTTPS | PROTOCOL_FTP) &&
+ uri->protocollen > 0 && uri->urllen > uri->protocollen + 2) {
+
+ gchar *pos = &uri->string[uri->protocollen],
+ *host_start = rspamd_url_host_unsafe(uri);
+
+ while (pos < host_start) {
+ if (*pos == '\\') {
+ *pos = '/';
+ uri->flags |= RSPAMD_URL_FLAG_OBSCURED;
+ }
+ pos++;
+ }
+ }
+ }
+ else if (uri->protocol & PROTOCOL_TELEPHONE) {
+ /* We need to normalise phone number: remove all spaces and braces */
+ rspamd_telephone_normalise_inplace(uri);
+
+ if (rspamd_url_host_unsafe(uri)[0] == '+') {
+ uri->tldshift = uri->hostshift + 1;
+ uri->tldlen = uri->hostlen - 1;
+ }
+ else {
+ uri->tldshift = uri->hostshift;
+ uri->tldlen = uri->hostlen;
+ }
+ }
+
+ if (uri->protocol == PROTOCOL_UNKNOWN) {
+ if (!(parse_flags & RSPAMD_URL_PARSE_HREF)) {
+ return URI_ERRNO_INVALID_PROTOCOL;
+ }
+ else {
+ /* Hack, hack, hack */
+ uri->protocol = PROTOCOL_UNKNOWN;
+ }
+ }
+
+ return URI_ERRNO_OK;
+}
+
+struct tld_trie_cbdata {
+ const gchar *begin;
+ gsize len;
+ rspamd_ftok_t *out;
+};
+
+static gint
+rspamd_tld_trie_find_callback(struct rspamd_multipattern *mp,
+ guint strnum,
+ gint match_start,
+ gint match_pos,
+ const gchar *text,
+ gsize len,
+ void *context)
+{
+ struct url_matcher *matcher;
+ const gchar *start, *pos, *p;
+ struct tld_trie_cbdata *cbdata = context;
+ gint ndots = 1;
+
+ matcher = &g_array_index(url_scanner->matchers_full, struct url_matcher,
+ strnum);
+
+ if (matcher->flags & URL_MATCHER_FLAG_STAR_MATCH) {
+ /* Skip one more tld component */
+ ndots = 2;
+ }
+
+ pos = text + match_start;
+ p = pos - 1;
+ start = text;
+
+ if (*pos != '.' || match_pos != (gint) cbdata->len) {
+ /* Something weird has been found */
+ if (match_pos != (gint) cbdata->len - 1) {
+ /* Search more */
+ return 0;
+ }
+ }
+
+ /* Now we need to find top level domain */
+ pos = start;
+
+ while (p >= start && ndots > 0) {
+ if (*p == '.') {
+ ndots--;
+ pos = p + 1;
+ }
+ else {
+ pos = p;
+ }
+
+ p--;
+ }
+
+ if (ndots == 0 || p == start - 1) {
+ if (cbdata->begin + cbdata->len - pos > cbdata->out->len) {
+ cbdata->out->begin = pos;
+ cbdata->out->len = cbdata->begin + cbdata->len - pos;
+ }
+ }
+
+ return 0;
+}
+
+gboolean
+rspamd_url_find_tld(const gchar *in, gsize inlen, rspamd_ftok_t *out)
+{
+ struct tld_trie_cbdata cbdata;
+
+ g_assert(in != NULL);
+ g_assert(out != NULL);
+ g_assert(url_scanner != NULL);
+
+ cbdata.begin = in;
+ cbdata.len = inlen;
+ cbdata.out = out;
+ out->len = 0;
+
+ if (url_scanner->search_trie_full) {
+ rspamd_multipattern_lookup(url_scanner->search_trie_full, in, inlen,
+ rspamd_tld_trie_find_callback, &cbdata, NULL);
+ }
+
+ if (out->len > 0) {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static const gchar url_braces[] = {
+ '(', ')',
+ '{', '}',
+ '[', ']',
+ '<', '>',
+ '|', '|',
+ '\'', '\''};
+
+
+static gboolean
+url_file_start(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match)
+{
+ match->m_begin = pos;
+
+ if (pos > cb->begin) {
+ match->st = *(pos - 1);
+ }
+ else {
+ match->st = '\0';
+ }
+
+ return TRUE;
+}
+
+static gboolean
+url_file_end(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match)
+{
+ const gchar *p;
+ gchar stop;
+ guint i;
+
+ p = pos + strlen(match->pattern);
+ stop = *p;
+ if (*p == '/') {
+ p++;
+ }
+
+ for (i = 0; i < G_N_ELEMENTS(url_braces) / 2; i += 2) {
+ if (*p == url_braces[i]) {
+ stop = url_braces[i + 1];
+ break;
+ }
+ }
+
+ while (p < cb->end && *p != stop && is_urlsafe(*p)) {
+ p++;
+ }
+
+ if (p == cb->begin) {
+ return FALSE;
+ }
+ match->m_len = p - match->m_begin;
+
+ return TRUE;
+}
+
+static gboolean
+url_tld_start(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match)
+{
+ const gchar *p = pos;
+ guint processed = 0;
+ static const guint max_shift = 253 + sizeof("https://");
+
+ /* Try to find the start of the url by finding any non-urlsafe character or whitespace/punctuation */
+ while (p >= cb->begin) {
+ if (!is_domain(*p) || g_ascii_isspace(*p) || is_url_start(*p) ||
+ p == match->prev_newline_pos) {
+ if (!is_url_start(*p) && !g_ascii_isspace(*p) &&
+ p != match->prev_newline_pos) {
+ return FALSE;
+ }
+
+ if (p != match->prev_newline_pos) {
+ match->st = *p;
+
+ p++;
+ }
+ else {
+ match->st = '\n';
+ }
+
+ if (!g_ascii_isalnum(*p)) {
+ /* Urls cannot start with strange symbols */
+ return FALSE;
+ }
+
+ match->m_begin = p;
+ return TRUE;
+ }
+ else if (p == cb->begin && p != pos) {
+ match->st = '\0';
+ match->m_begin = p;
+
+ return TRUE;
+ }
+ else if (*p == '.') {
+ if (p == cb->begin) {
+ /* Urls cannot start with a dot */
+ return FALSE;
+ }
+ if (!g_ascii_isalnum(p[1])) {
+ /* Wrong we have an invalid character after dot */
+ return FALSE;
+ }
+ }
+ else if (*p == '/') {
+ /* Urls cannot contain '/' in their body */
+ return FALSE;
+ }
+
+ p--;
+ processed++;
+
+ if (processed > max_shift) {
+ /* Too long */
+ return FALSE;
+ }
+ }
+
+ return FALSE;
+}
+
+static gboolean
+url_tld_end(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match)
+{
+ const gchar *p;
+ gboolean ret = FALSE;
+
+ p = pos + match->m_len;
+
+ if (p == cb->end) {
+ match->m_len = p - match->m_begin;
+ return TRUE;
+ }
+ else if (*p == '/' || *p == ':' || is_url_end(*p) || is_lwsp(*p) ||
+ (match->st != '<' && p == match->newline_pos)) {
+ /* Parse arguments, ports by normal way by url default function */
+ p = match->m_begin;
+ /* Check common prefix */
+ if (g_ascii_strncasecmp(p, "http://", sizeof("http://") - 1) == 0) {
+ ret = url_web_end(cb,
+ match->m_begin + sizeof("http://") - 1,
+ match);
+ }
+ else {
+ ret = url_web_end(cb, match->m_begin, match);
+ }
+ }
+ else if (*p == '.') {
+ p++;
+ if (p < cb->end) {
+ if (g_ascii_isspace(*p) || *p == '/' ||
+ *p == '?' || *p == ':') {
+ ret = url_web_end(cb, match->m_begin, match);
+ }
+ }
+ }
+
+ if (ret) {
+ /* Check sanity of match found */
+ if (match->m_begin + match->m_len <= pos) {
+ return FALSE;
+ }
+ }
+
+ return ret;
+}
+
+static gboolean
+url_web_start(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match)
+{
+ /* Check what we have found */
+ if (pos > cb->begin) {
+ if (g_ascii_strncasecmp(pos, "www", 3) == 0) {
+
+ if (!(is_url_start(*(pos - 1)) ||
+ g_ascii_isspace(*(pos - 1)) ||
+ pos - 1 == match->prev_newline_pos ||
+ (*(pos - 1) & 0x80))) { /* Chinese trick */
+ return FALSE;
+ }
+ }
+ else {
+ guchar prev = *(pos - 1);
+
+ if (g_ascii_isalnum(prev)) {
+ /* Part of another url */
+ return FALSE;
+ }
+ }
+ }
+
+ if (*pos == '.') {
+ /* Urls cannot start with . */
+ return FALSE;
+ }
+
+ if (pos > cb->begin) {
+ match->st = *(pos - 1);
+ }
+ else {
+ match->st = '\0';
+ }
+
+ match->m_begin = pos;
+
+ return TRUE;
+}
+
+static gboolean
+url_web_end(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match)
+{
+ const gchar *last = NULL;
+ gint len = cb->end - pos;
+ guint flags = 0;
+
+ if (match->newline_pos && match->st != '<') {
+ /* We should also limit our match end to the newline */
+ len = MIN(len, match->newline_pos - pos);
+ }
+
+ if (rspamd_web_parse(NULL, pos, len, &last,
+ RSPAMD_URL_PARSE_CHECK, &flags) != 0) {
+ return FALSE;
+ }
+
+ if (last < cb->end && (*last == '>' && last != match->newline_pos)) {
+ /* We need to ensure that url also starts with '>' */
+ if (match->st != '<') {
+ if (last + 1 < cb->end) {
+ if (g_ascii_isspace(last[1])) {
+ return FALSE;
+ }
+ }
+ else {
+ return FALSE;
+ }
+ }
+ }
+
+ match->m_len = (last - pos);
+ cb->fin = last + 1;
+
+ return TRUE;
+}
+
+
+static gboolean
+url_email_start(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match)
+{
+ if (!match->prefix || match->prefix[0] == '\0') {
+ /* We have mailto:// at the beginning */
+ match->m_begin = pos;
+
+ if (pos >= cb->begin + 1) {
+ match->st = *(pos - 1);
+ }
+ else {
+ match->st = '\0';
+ }
+ }
+ else {
+ /* Just '@' */
+
+ /* Check if this match is a part of the previous mailto: email */
+ if (cb->last_at != NULL && cb->last_at == pos) {
+ cb->last_at = NULL;
+ return FALSE;
+ }
+ else if (pos == cb->begin) {
+ /* Just @ at the start of input */
+ return FALSE;
+ }
+
+ match->st = '\0';
+ }
+
+ return TRUE;
+}
+
+static gboolean
+url_email_end(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match)
+{
+ const gchar *last = NULL;
+ struct http_parser_url u;
+ gint len = cb->end - pos;
+ guint flags = 0;
+
+ if (match->newline_pos && match->st != '<') {
+ /* We should also limit our match end to the newline */
+ len = MIN(len, match->newline_pos - pos);
+ }
+
+ if (!match->prefix || match->prefix[0] == '\0') {
+ /* We have mailto:// at the beginning */
+ if (rspamd_mailto_parse(&u, pos, len, &last,
+ RSPAMD_URL_PARSE_CHECK, &flags) != 0) {
+ return FALSE;
+ }
+
+ if (!(u.field_set & (1 << UF_USERINFO))) {
+ return FALSE;
+ }
+
+ cb->last_at = match->m_begin + u.field_data[UF_USERINFO].off +
+ u.field_data[UF_USERINFO].len;
+
+ g_assert(*cb->last_at == '@');
+ match->m_len = (last - pos);
+
+ return TRUE;
+ }
+ else {
+ const gchar *c, *p;
+ /*
+ * Here we have just '@', so we need to find both start and end of the
+ * pattern
+ */
+ g_assert(*pos == '@');
+
+ if (pos >= cb->end - 2 || pos < cb->begin + 1) {
+ /* Boundary violation */
+ return FALSE;
+ }
+
+ /* Check the next character after `@` */
+ if (!g_ascii_isalnum(pos[1]) || !g_ascii_isalnum(*(pos - 1))) {
+ return FALSE;
+ }
+
+
+ c = pos - 1;
+ while (c > cb->begin) {
+ if (!is_mailsafe(*c)) {
+ break;
+ }
+ if (c == match->prev_newline_pos) {
+ break;
+ }
+
+ c--;
+ }
+ /* Rewind to the first alphanumeric character */
+ while (c < pos && !g_ascii_isalnum(*c)) {
+ c++;
+ }
+
+ /* Find the end of email */
+ p = pos + 1;
+ while (p < cb->end && is_domain(*p)) {
+ if (p == match->newline_pos) {
+ break;
+ }
+
+ p++;
+ }
+
+ /* Rewind it again to avoid bad emails to be detected */
+ while (p > pos && p < cb->end && !g_ascii_isalnum(*p)) {
+ p--;
+ }
+
+ if (p < cb->end && g_ascii_isalnum(*p) &&
+ (match->newline_pos == NULL || p < match->newline_pos)) {
+ p++;
+ }
+
+ if (p > c) {
+ match->m_begin = c;
+ match->m_len = p - c;
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static gboolean
+url_tel_start(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match)
+{
+ match->m_begin = pos;
+
+ if (pos >= cb->begin + 1) {
+ match->st = *(pos - 1);
+ }
+ else {
+ match->st = '\0';
+ }
+
+ return TRUE;
+}
+
+static gboolean
+url_tel_end(struct url_callback_data *cb,
+ const gchar *pos,
+ url_match_t *match)
+{
+ const gchar *last = NULL;
+ struct http_parser_url u;
+ gint len = cb->end - pos;
+ guint flags = 0;
+
+ if (match->newline_pos && match->st != '<') {
+ /* We should also limit our match end to the newline */
+ len = MIN(len, match->newline_pos - pos);
+ }
+
+ if (rspamd_telephone_parse(&u, pos, len, &last,
+ RSPAMD_URL_PARSE_CHECK, &flags) != 0) {
+ return FALSE;
+ }
+
+ if (!(u.field_set & (1 << UF_HOST))) {
+ return FALSE;
+ }
+
+ match->m_len = (last - pos);
+
+ return TRUE;
+}
+
+
+static gboolean
+rspamd_url_trie_is_match(struct url_matcher *matcher, const gchar *pos,
+ const gchar *end, const gchar *newline_pos)
+{
+ if (matcher->flags & URL_MATCHER_FLAG_TLD_MATCH) {
+ /* Immediately check pos for valid chars */
+ if (pos < end) {
+ if (pos != newline_pos && !g_ascii_isspace(*pos) && *pos != '/' && *pos != '?' &&
+ *pos != ':' && !is_url_end(*pos)) {
+ if (*pos == '.') {
+ /* We allow . at the end of the domain however */
+ pos++;
+ if (pos < end) {
+ if (!g_ascii_isspace(*pos) && *pos != '/' &&
+ *pos != '?' && *pos != ':' && !is_url_end(*pos)) {
+ return FALSE;
+ }
+ }
+ }
+ else {
+ return FALSE;
+ }
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+static gint
+rspamd_url_trie_callback(struct rspamd_multipattern *mp,
+ guint strnum,
+ gint match_start,
+ gint match_pos,
+ const gchar *text,
+ gsize len,
+ void *context)
+{
+ struct url_matcher *matcher;
+ url_match_t m;
+ const gchar *pos, *newline_pos = NULL;
+ struct url_callback_data *cb = context;
+
+ pos = text + match_pos;
+
+ if (cb->fin > pos) {
+ /* Already seen */
+ return 0;
+ }
+
+ matcher = &g_array_index(cb->matchers, struct url_matcher,
+ strnum);
+
+ if ((matcher->flags & URL_MATCHER_FLAG_NOHTML) && cb->how == RSPAMD_URL_FIND_STRICT) {
+ /* Do not try to match non-html like urls in html texts */
+ return 0;
+ }
+
+ memset(&m, 0, sizeof(m));
+ m.m_begin = text + match_start;
+ m.m_len = match_pos - match_start;
+
+ if (cb->newlines && cb->newlines->len > 0) {
+ newline_pos = g_ptr_array_index(cb->newlines, cb->newline_idx);
+
+ while (pos > newline_pos && cb->newline_idx < cb->newlines->len) {
+ cb->newline_idx++;
+ newline_pos = g_ptr_array_index(cb->newlines, cb->newline_idx);
+ }
+
+ if (pos > newline_pos) {
+ newline_pos = NULL;
+ }
+
+ if (cb->newline_idx > 0) {
+ m.prev_newline_pos = g_ptr_array_index(cb->newlines,
+ cb->newline_idx - 1);
+ }
+ }
+
+ if (!rspamd_url_trie_is_match(matcher, pos, cb->end, newline_pos)) {
+ return 0;
+ }
+
+ m.pattern = matcher->pattern;
+ m.prefix = matcher->prefix;
+ m.add_prefix = FALSE;
+ m.newline_pos = newline_pos;
+ pos = cb->begin + match_start;
+
+ if (matcher->start(cb, pos, &m) &&
+ matcher->end(cb, pos, &m)) {
+ if (m.add_prefix || matcher->prefix[0] != '\0') {
+ cb->len = m.m_len + strlen(matcher->prefix);
+ cb->url_str = rspamd_mempool_alloc(cb->pool, cb->len + 1);
+ cb->len = rspamd_snprintf(cb->url_str,
+ cb->len + 1,
+ "%s%*s",
+ m.prefix,
+ (gint) m.m_len,
+ m.m_begin);
+ cb->prefix_added = TRUE;
+ }
+ else {
+ cb->url_str = rspamd_mempool_alloc(cb->pool, m.m_len + 1);
+ rspamd_strlcpy(cb->url_str, m.m_begin, m.m_len + 1);
+ }
+
+ cb->start = m.m_begin;
+
+ if (pos > cb->fin) {
+ cb->fin = pos;
+ }
+
+ return 1;
+ }
+ else {
+ cb->url_str = NULL;
+ }
+
+ /* Continue search */
+ return 0;
+}
+
+gboolean
+rspamd_url_find(rspamd_mempool_t *pool,
+ const gchar *begin, gsize len,
+ gchar **url_str,
+ enum rspamd_url_find_type how,
+ goffset *url_pos,
+ gboolean *prefix_added)
+{
+ struct url_callback_data cb;
+ gint ret;
+
+ memset(&cb, 0, sizeof(cb));
+ cb.begin = begin;
+ cb.end = begin + len;
+ cb.how = how;
+ cb.pool = pool;
+
+ if (how == RSPAMD_URL_FIND_ALL) {
+ if (url_scanner->search_trie_full) {
+ cb.matchers = url_scanner->matchers_full;
+ ret = rspamd_multipattern_lookup(url_scanner->search_trie_full,
+ begin, len,
+ rspamd_url_trie_callback, &cb, NULL);
+ }
+ else {
+ cb.matchers = url_scanner->matchers_strict;
+ ret = rspamd_multipattern_lookup(url_scanner->search_trie_strict,
+ begin, len,
+ rspamd_url_trie_callback, &cb, NULL);
+ }
+ }
+ else {
+ cb.matchers = url_scanner->matchers_strict;
+ ret = rspamd_multipattern_lookup(url_scanner->search_trie_strict,
+ begin, len,
+ rspamd_url_trie_callback, &cb, NULL);
+ }
+
+ if (ret) {
+ if (url_str) {
+ *url_str = cb.url_str;
+ }
+
+ if (url_pos) {
+ *url_pos = cb.start - begin;
+ }
+
+ if (prefix_added) {
+ *prefix_added = cb.prefix_added;
+ }
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gint
+rspamd_url_trie_generic_callback_common(struct rspamd_multipattern *mp,
+ guint strnum,
+ gint match_start,
+ gint match_pos,
+ const gchar *text,
+ gsize len,
+ void *context,
+ gboolean multiple)
+{
+ struct rspamd_url *url;
+ struct url_matcher *matcher;
+ url_match_t m;
+ const gchar *pos, *newline_pos = NULL;
+ struct url_callback_data *cb = context;
+ gint rc;
+ rspamd_mempool_t *pool;
+
+ pos = text + match_pos;
+
+ if (cb->fin > pos) {
+ /* Already seen */
+ return 0;
+ }
+
+ matcher = &g_array_index(cb->matchers, struct url_matcher,
+ strnum);
+ pool = cb->pool;
+
+ if ((matcher->flags & URL_MATCHER_FLAG_NOHTML) && cb->how == RSPAMD_URL_FIND_STRICT) {
+ /* Do not try to match non-html like urls in html texts, continue matching */
+ return 0;
+ }
+
+ memset(&m, 0, sizeof(m));
+
+
+ /* Find the next newline after our pos */
+ if (cb->newlines && cb->newlines->len > 0) {
+ newline_pos = g_ptr_array_index(cb->newlines, cb->newline_idx);
+
+ while (pos > newline_pos && cb->newline_idx < cb->newlines->len - 1) {
+ cb->newline_idx++;
+ newline_pos = g_ptr_array_index(cb->newlines, cb->newline_idx);
+ }
+
+ if (pos > newline_pos) {
+ newline_pos = NULL;
+ }
+ if (cb->newline_idx > 0) {
+ m.prev_newline_pos = g_ptr_array_index(cb->newlines,
+ cb->newline_idx - 1);
+ }
+ }
+
+ if (!rspamd_url_trie_is_match(matcher, pos, text + len, newline_pos)) {
+ /* Mismatch, continue */
+ return 0;
+ }
+
+ pos = cb->begin + match_start;
+ m.pattern = matcher->pattern;
+ m.prefix = matcher->prefix;
+ m.add_prefix = FALSE;
+ m.m_begin = text + match_start;
+ m.m_len = match_pos - match_start;
+ m.newline_pos = newline_pos;
+
+ if (matcher->start(cb, pos, &m) &&
+ matcher->end(cb, pos, &m)) {
+ if (m.add_prefix || matcher->prefix[0] != '\0') {
+ cb->len = m.m_len + strlen(matcher->prefix);
+ cb->url_str = rspamd_mempool_alloc(cb->pool, cb->len + 1);
+ cb->len = rspamd_snprintf(cb->url_str,
+ cb->len + 1,
+ "%s%*s",
+ m.prefix,
+ (gint) m.m_len,
+ m.m_begin);
+ cb->prefix_added = TRUE;
+ }
+ else {
+ cb->url_str = rspamd_mempool_alloc(cb->pool, m.m_len + 1);
+ cb->len = rspamd_strlcpy(cb->url_str, m.m_begin, m.m_len + 1);
+ }
+
+ cb->start = m.m_begin;
+
+ if (pos > cb->fin) {
+ cb->fin = pos;
+ }
+
+ url = rspamd_mempool_alloc0(pool, sizeof(struct rspamd_url));
+ g_strstrip(cb->url_str);
+ rc = rspamd_url_parse(url, cb->url_str,
+ strlen(cb->url_str), pool,
+ RSPAMD_URL_PARSE_TEXT);
+
+ if (rc == URI_ERRNO_OK && url->hostlen > 0) {
+ if (cb->prefix_added) {
+ url->flags |= RSPAMD_URL_FLAG_SCHEMALESS;
+ cb->prefix_added = FALSE;
+ }
+
+ if (cb->func) {
+ if (!cb->func(url, cb->start - text, (m.m_begin + m.m_len) - text,
+ cb->funcd)) {
+ /* We need to stop here in any case! */
+ return -1;
+ }
+ }
+ }
+ else if (rc != URI_ERRNO_OK) {
+ msg_debug_pool_check("extract of url '%s' failed: %s",
+ cb->url_str,
+ rspamd_url_strerror(rc));
+ }
+ }
+ else {
+ cb->url_str = NULL;
+ /* Continue search if no pattern has been found */
+ return 0;
+ }
+
+ /* Continue search if required (return 0 means continue) */
+ return !multiple;
+}
+
+static gint
+rspamd_url_trie_generic_callback_multiple(struct rspamd_multipattern *mp,
+ guint strnum,
+ gint match_start,
+ gint match_pos,
+ const gchar *text,
+ gsize len,
+ void *context)
+{
+ return rspamd_url_trie_generic_callback_common(mp, strnum, match_start,
+ match_pos, text, len, context, TRUE);
+}
+
+static gint
+rspamd_url_trie_generic_callback_single(struct rspamd_multipattern *mp,
+ guint strnum,
+ gint match_start,
+ gint match_pos,
+ const gchar *text,
+ gsize len,
+ void *context)
+{
+ return rspamd_url_trie_generic_callback_common(mp, strnum, match_start,
+ match_pos, text, len, context, FALSE);
+}
+
+struct rspamd_url_mimepart_cbdata {
+ struct rspamd_task *task;
+ struct rspamd_mime_text_part *part;
+ gsize url_len;
+ uint16_t *cur_url_order; /* Global ordering */
+ uint16_t cur_part_order; /* Per part ordering */
+};
+
+static gboolean
+rspamd_url_query_callback(struct rspamd_url *url, gsize start_offset,
+ gsize end_offset, gpointer ud)
+{
+ struct rspamd_url_mimepart_cbdata *cbd =
+ (struct rspamd_url_mimepart_cbdata *) ud;
+ struct rspamd_task *task;
+
+ task = cbd->task;
+
+ if (url->protocol == PROTOCOL_MAILTO) {
+ if (url->userlen == 0) {
+ return FALSE;
+ }
+ }
+ /* Also check max urls */
+ if (cbd->task->cfg && cbd->task->cfg->max_urls > 0) {
+ if (kh_size(MESSAGE_FIELD(task, urls)) > cbd->task->cfg->max_urls) {
+ msg_err_task("part has too many URLs, we cannot process more: "
+ "%d urls extracted ",
+ (guint) kh_size(MESSAGE_FIELD(task, urls)));
+
+ return FALSE;
+ }
+ }
+
+ url->flags |= RSPAMD_URL_FLAG_QUERY;
+
+
+ if (rspamd_url_set_add_or_increase(MESSAGE_FIELD(task, urls), url, false)) {
+ if (cbd->part && cbd->part->mime_part->urls) {
+ g_ptr_array_add(cbd->part->mime_part->urls, url);
+ }
+
+ url->part_order = cbd->cur_part_order++;
+
+ if (cbd->cur_url_order) {
+ url->order = (*cbd->cur_url_order)++;
+ }
+ }
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_url_text_part_callback(struct rspamd_url *url, gsize start_offset,
+ gsize end_offset, gpointer ud)
+{
+ struct rspamd_url_mimepart_cbdata *cbd =
+ (struct rspamd_url_mimepart_cbdata *) ud;
+ struct rspamd_process_exception *ex;
+ struct rspamd_task *task;
+
+ task = cbd->task;
+ ex = rspamd_mempool_alloc0(task->task_pool, sizeof(struct rspamd_process_exception));
+
+ ex->pos = start_offset;
+ ex->len = end_offset - start_offset;
+ ex->type = RSPAMD_EXCEPTION_URL;
+ ex->ptr = url;
+
+ cbd->url_len += ex->len;
+
+ if (cbd->part->utf_stripped_content &&
+ cbd->url_len > cbd->part->utf_stripped_content->len * 10) {
+ /* Absurd case, stop here now */
+ msg_err_task("part has too many URLs, we cannot process more: %z url len; "
+ "%d stripped content length",
+ cbd->url_len, cbd->part->utf_stripped_content->len);
+
+ return FALSE;
+ }
+
+ if (url->protocol == PROTOCOL_MAILTO) {
+ if (url->userlen == 0) {
+ return FALSE;
+ }
+ }
+ /* Also check max urls */
+ if (cbd->task->cfg && cbd->task->cfg->max_urls > 0) {
+ if (kh_size(MESSAGE_FIELD(task, urls)) > cbd->task->cfg->max_urls) {
+ msg_err_task("part has too many URLs, we cannot process more: "
+ "%d urls extracted ",
+ (guint) kh_size(MESSAGE_FIELD(task, urls)));
+
+ return FALSE;
+ }
+ }
+
+ url->flags |= RSPAMD_URL_FLAG_FROM_TEXT;
+
+ if (rspamd_url_set_add_or_increase(MESSAGE_FIELD(task, urls), url, false) &&
+ cbd->part->mime_part->urls) {
+ url->part_order = cbd->cur_part_order++;
+
+ if (cbd->cur_url_order) {
+ url->order = (*cbd->cur_url_order)++;
+ }
+ g_ptr_array_add(cbd->part->mime_part->urls, url);
+ }
+
+ cbd->part->exceptions = g_list_prepend(
+ cbd->part->exceptions,
+ ex);
+
+ /* We also search the query for additional url inside */
+ if (url->querylen > 0) {
+ rspamd_url_find_multiple(task->task_pool,
+ rspamd_url_query_unsafe(url), url->querylen,
+ RSPAMD_URL_FIND_ALL, NULL,
+ rspamd_url_query_callback, cbd);
+ }
+
+ return TRUE;
+}
+
+void rspamd_url_text_extract(rspamd_mempool_t *pool,
+ struct rspamd_task *task,
+ struct rspamd_mime_text_part *part,
+ uint16_t *cur_url_order,
+ enum rspamd_url_find_type how)
+{
+ struct rspamd_url_mimepart_cbdata mcbd;
+
+ if (part->utf_stripped_content == NULL || part->utf_stripped_content->len == 0) {
+ msg_warn_task("got empty text part");
+ return;
+ }
+
+ mcbd.task = task;
+ mcbd.part = part;
+ mcbd.url_len = 0;
+ mcbd.cur_url_order = cur_url_order;
+ mcbd.cur_part_order = 0;
+
+ rspamd_url_find_multiple(task->task_pool, part->utf_stripped_content->data,
+ part->utf_stripped_content->len, how, part->newlines,
+ rspamd_url_text_part_callback, &mcbd);
+}
+
+void rspamd_url_find_multiple(rspamd_mempool_t *pool,
+ const gchar *in,
+ gsize inlen,
+ enum rspamd_url_find_type how,
+ GPtrArray *nlines,
+ url_insert_function func,
+ gpointer ud)
+{
+ struct url_callback_data cb;
+
+ g_assert(in != NULL);
+
+ if (inlen == 0) {
+ inlen = strlen(in);
+ }
+
+ memset(&cb, 0, sizeof(cb));
+ cb.begin = in;
+ cb.end = in + inlen;
+ cb.how = how;
+ cb.pool = pool;
+
+ cb.funcd = ud;
+ cb.func = func;
+ cb.newlines = nlines;
+
+ if (how == RSPAMD_URL_FIND_ALL) {
+ if (url_scanner->search_trie_full) {
+ cb.matchers = url_scanner->matchers_full;
+ rspamd_multipattern_lookup(url_scanner->search_trie_full,
+ in, inlen,
+ rspamd_url_trie_generic_callback_multiple, &cb, NULL);
+ }
+ else {
+ cb.matchers = url_scanner->matchers_strict;
+ rspamd_multipattern_lookup(url_scanner->search_trie_strict,
+ in, inlen,
+ rspamd_url_trie_generic_callback_multiple, &cb, NULL);
+ }
+ }
+ else {
+ cb.matchers = url_scanner->matchers_strict;
+ rspamd_multipattern_lookup(url_scanner->search_trie_strict,
+ in, inlen,
+ rspamd_url_trie_generic_callback_multiple, &cb, NULL);
+ }
+}
+
+void rspamd_url_find_single(rspamd_mempool_t *pool,
+ const gchar *in,
+ gsize inlen,
+ enum rspamd_url_find_type how,
+ url_insert_function func,
+ gpointer ud)
+{
+ struct url_callback_data cb;
+
+ g_assert(in != NULL);
+
+ if (inlen == 0) {
+ inlen = strlen(in);
+ }
+
+ /*
+ * We might have a situation when we need to parse URLs on config file
+ * parsing, but there is no valid url_scanner loaded. Hence, we just load
+ * some defaults and it should be fine...
+ */
+ if (url_scanner == NULL) {
+ rspamd_url_init(NULL);
+ }
+
+ memset(&cb, 0, sizeof(cb));
+ cb.begin = in;
+ cb.end = in + inlen;
+ cb.how = how;
+ cb.pool = pool;
+
+ cb.funcd = ud;
+ cb.func = func;
+
+ if (how == RSPAMD_URL_FIND_ALL) {
+ if (url_scanner->search_trie_full) {
+ cb.matchers = url_scanner->matchers_full;
+ rspamd_multipattern_lookup(url_scanner->search_trie_full,
+ in, inlen,
+ rspamd_url_trie_generic_callback_single, &cb, NULL);
+ }
+ else {
+ cb.matchers = url_scanner->matchers_strict;
+ rspamd_multipattern_lookup(url_scanner->search_trie_strict,
+ in, inlen,
+ rspamd_url_trie_generic_callback_single, &cb, NULL);
+ }
+ }
+ else {
+ cb.matchers = url_scanner->matchers_strict;
+ rspamd_multipattern_lookup(url_scanner->search_trie_strict,
+ in, inlen,
+ rspamd_url_trie_generic_callback_single, &cb, NULL);
+ }
+}
+
+
+gboolean
+rspamd_url_task_subject_callback(struct rspamd_url *url, gsize start_offset,
+ gsize end_offset, gpointer ud)
+{
+ struct rspamd_task *task = ud;
+ gchar *url_str = NULL;
+ struct rspamd_url *query_url;
+ gint rc;
+ gboolean prefix_added;
+
+ /* It is just a displayed URL, we should not check it for certain things */
+ url->flags |= RSPAMD_URL_FLAG_HTML_DISPLAYED | RSPAMD_URL_FLAG_SUBJECT;
+
+ if (url->protocol == PROTOCOL_MAILTO) {
+ if (url->userlen == 0) {
+ return FALSE;
+ }
+ }
+
+ rspamd_url_set_add_or_increase(MESSAGE_FIELD(task, urls), url, false);
+
+ /* We also search the query for additional url inside */
+ if (url->querylen > 0) {
+ if (rspamd_url_find(task->task_pool, rspamd_url_query_unsafe(url), url->querylen,
+ &url_str, RSPAMD_URL_FIND_ALL, NULL, &prefix_added)) {
+
+ query_url = rspamd_mempool_alloc0(task->task_pool,
+ sizeof(struct rspamd_url));
+ rc = rspamd_url_parse(query_url,
+ url_str,
+ strlen(url_str),
+ task->task_pool,
+ RSPAMD_URL_PARSE_TEXT);
+
+ if (rc == URI_ERRNO_OK &&
+ url->hostlen > 0) {
+ msg_debug_task("found url %s in query of url"
+ " %*s",
+ url_str, url->querylen, rspamd_url_query_unsafe(url));
+
+ if (prefix_added) {
+ query_url->flags |= RSPAMD_URL_FLAG_SCHEMALESS;
+ }
+
+ if (query_url->protocol == PROTOCOL_MAILTO) {
+ if (query_url->userlen == 0) {
+ return TRUE;
+ }
+ }
+
+ rspamd_url_set_add_or_increase(MESSAGE_FIELD(task, urls),
+ query_url, false);
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+static inline khint_t
+rspamd_url_hash(struct rspamd_url *url)
+{
+ if (url->urllen > 0) {
+ return (khint_t) rspamd_cryptobox_fast_hash(url->string, url->urllen,
+ rspamd_hash_seed());
+ }
+
+ return 0;
+}
+
+static inline khint_t
+rspamd_url_host_hash(struct rspamd_url *url)
+{
+ if (url->hostlen > 0) {
+ return (khint_t) rspamd_cryptobox_fast_hash(rspamd_url_host_unsafe(url),
+ url->hostlen,
+ rspamd_hash_seed());
+ }
+
+ return 0;
+}
+
+/* Compare two emails for building emails tree */
+static inline bool
+rspamd_emails_cmp(struct rspamd_url *u1, struct rspamd_url *u2)
+{
+ gint r;
+
+ if (u1->hostlen != u2->hostlen || u1->hostlen == 0) {
+ return FALSE;
+ }
+ else {
+ if ((r = rspamd_lc_cmp(rspamd_url_host_unsafe(u1),
+ rspamd_url_host_unsafe(u2), u1->hostlen)) == 0) {
+ if (u1->userlen != u2->userlen || u1->userlen == 0) {
+ return FALSE;
+ }
+ else {
+ return (rspamd_lc_cmp(rspamd_url_user_unsafe(u1),
+ rspamd_url_user_unsafe(u2),
+ u1->userlen) == 0);
+ }
+ }
+ else {
+ return r == 0;
+ }
+ }
+
+ return FALSE;
+}
+
+static inline bool
+rspamd_urls_cmp(struct rspamd_url *u1, struct rspamd_url *u2)
+{
+ int r = 0;
+
+ if (u1->protocol != u2->protocol || u1->urllen != u2->urllen) {
+ return false;
+ }
+ else {
+ if (u1->protocol & PROTOCOL_MAILTO) {
+ return rspamd_emails_cmp(u1, u2);
+ }
+
+ r = memcmp(u1->string, u2->string, u1->urllen);
+ }
+
+ return r == 0;
+}
+
+static inline bool
+rspamd_urls_host_cmp(struct rspamd_url *u1, struct rspamd_url *u2)
+{
+ int r = 0;
+
+ if (u1->hostlen != u2->hostlen) {
+ return false;
+ }
+ else {
+ r = memcmp(rspamd_url_host_unsafe(u1), rspamd_url_host_unsafe(u2),
+ u1->hostlen);
+ }
+
+ return r == 0;
+}
+
+gsize rspamd_url_decode(gchar *dst, const gchar *src, gsize size)
+{
+ gchar *d, ch, c, decoded;
+ const gchar *s;
+ enum {
+ sw_usual = 0,
+ sw_quoted,
+ sw_quoted_second
+ } state;
+
+ d = dst;
+ s = src;
+
+ state = 0;
+ decoded = 0;
+
+ while (size--) {
+
+ ch = *s++;
+
+ switch (state) {
+ case sw_usual:
+
+ if (ch == '%') {
+ state = sw_quoted;
+ break;
+ }
+ else if (ch == '+') {
+ *d++ = ' ';
+ }
+ else {
+ *d++ = ch;
+ }
+ break;
+
+ case sw_quoted:
+
+ if (ch >= '0' && ch <= '9') {
+ decoded = (ch - '0');
+ state = sw_quoted_second;
+ break;
+ }
+
+ c = (ch | 0x20);
+ if (c >= 'a' && c <= 'f') {
+ decoded = (c - 'a' + 10);
+ state = sw_quoted_second;
+ break;
+ }
+
+ /* the invalid quoted character */
+
+ state = sw_usual;
+
+ *d++ = ch;
+
+ break;
+
+ case sw_quoted_second:
+
+ state = sw_usual;
+
+ if (ch >= '0' && ch <= '9') {
+ ch = ((decoded << 4) + ch - '0');
+ *d++ = ch;
+
+ break;
+ }
+
+ c = (u_char) (ch | 0x20);
+ if (c >= 'a' && c <= 'f') {
+ ch = ((decoded << 4) + c - 'a' + 10);
+
+ *d++ = ch;
+ break;
+ }
+
+ /* the invalid quoted character */
+ break;
+ }
+ }
+
+ return (d - dst);
+}
+
+enum rspamd_url_char_class {
+ RSPAMD_URL_UNRESERVED = (1 << 0),
+ RSPAMD_URL_SUBDELIM = (1 << 1),
+ RSPAMD_URL_PATHSAFE = (1 << 2),
+ RSPAMD_URL_QUERYSAFE = (1 << 3),
+ RSPAMD_URL_FRAGMENTSAFE = (1 << 4),
+ RSPAMD_URL_HOSTSAFE = (1 << 5),
+ RSPAMD_URL_USERSAFE = (1 << 6),
+};
+
+#define RSPAMD_URL_FLAGS_HOSTSAFE (RSPAMD_URL_UNRESERVED | RSPAMD_URL_HOSTSAFE | RSPAMD_URL_SUBDELIM)
+#define RSPAMD_URL_FLAGS_USERSAFE (RSPAMD_URL_UNRESERVED | RSPAMD_URL_USERSAFE | RSPAMD_URL_SUBDELIM)
+#define RSPAMD_URL_FLAGS_PATHSAFE (RSPAMD_URL_UNRESERVED | RSPAMD_URL_PATHSAFE | RSPAMD_URL_SUBDELIM)
+#define RSPAMD_URL_FLAGS_QUERYSAFE (RSPAMD_URL_UNRESERVED | RSPAMD_URL_QUERYSAFE | RSPAMD_URL_SUBDELIM)
+#define RSPAMD_URL_FLAGS_FRAGMENTSAFE (RSPAMD_URL_UNRESERVED | RSPAMD_URL_FRAGMENTSAFE | RSPAMD_URL_SUBDELIM)
+
+static const unsigned char rspamd_url_encoding_classes[256] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0 /* */, RSPAMD_URL_SUBDELIM /* ! */, 0 /* " */, 0 /* # */,
+ RSPAMD_URL_SUBDELIM /* $ */, 0 /* % */, RSPAMD_URL_SUBDELIM /* & */,
+ RSPAMD_URL_SUBDELIM /* ' */, RSPAMD_URL_SUBDELIM /* ( */,
+ RSPAMD_URL_SUBDELIM /* ) */, RSPAMD_URL_SUBDELIM /* * */,
+ RSPAMD_URL_SUBDELIM /* + */, RSPAMD_URL_SUBDELIM /* , */,
+ RSPAMD_URL_UNRESERVED /* - */, RSPAMD_URL_UNRESERVED /* . */,
+ RSPAMD_URL_PATHSAFE | RSPAMD_URL_QUERYSAFE | RSPAMD_URL_FRAGMENTSAFE /* / */,
+ RSPAMD_URL_UNRESERVED /* 0 */, RSPAMD_URL_UNRESERVED /* 1 */,
+ RSPAMD_URL_UNRESERVED /* 2 */, RSPAMD_URL_UNRESERVED /* 3 */,
+ RSPAMD_URL_UNRESERVED /* 4 */, RSPAMD_URL_UNRESERVED /* 5 */,
+ RSPAMD_URL_UNRESERVED /* 6 */, RSPAMD_URL_UNRESERVED /* 7 */,
+ RSPAMD_URL_UNRESERVED /* 8 */, RSPAMD_URL_UNRESERVED /* 9 */,
+ RSPAMD_URL_USERSAFE | RSPAMD_URL_HOSTSAFE | RSPAMD_URL_PATHSAFE | RSPAMD_URL_QUERYSAFE | RSPAMD_URL_FRAGMENTSAFE /* : */,
+ RSPAMD_URL_SUBDELIM /* ; */, 0 /* < */, RSPAMD_URL_SUBDELIM /* = */, 0 /* > */,
+ RSPAMD_URL_QUERYSAFE | RSPAMD_URL_FRAGMENTSAFE /* ? */,
+ RSPAMD_URL_PATHSAFE | RSPAMD_URL_QUERYSAFE | RSPAMD_URL_FRAGMENTSAFE /* @ */,
+ RSPAMD_URL_UNRESERVED /* A */, RSPAMD_URL_UNRESERVED /* B */,
+ RSPAMD_URL_UNRESERVED /* C */, RSPAMD_URL_UNRESERVED /* D */,
+ RSPAMD_URL_UNRESERVED /* E */, RSPAMD_URL_UNRESERVED /* F */,
+ RSPAMD_URL_UNRESERVED /* G */, RSPAMD_URL_UNRESERVED /* H */,
+ RSPAMD_URL_UNRESERVED /* I */, RSPAMD_URL_UNRESERVED /* J */,
+ RSPAMD_URL_UNRESERVED /* K */, RSPAMD_URL_UNRESERVED /* L */,
+ RSPAMD_URL_UNRESERVED /* M */, RSPAMD_URL_UNRESERVED /* N */,
+ RSPAMD_URL_UNRESERVED /* O */, RSPAMD_URL_UNRESERVED /* P */,
+ RSPAMD_URL_UNRESERVED /* Q */, RSPAMD_URL_UNRESERVED /* R */,
+ RSPAMD_URL_UNRESERVED /* S */, RSPAMD_URL_UNRESERVED /* T */,
+ RSPAMD_URL_UNRESERVED /* U */, RSPAMD_URL_UNRESERVED /* V */,
+ RSPAMD_URL_UNRESERVED /* W */, RSPAMD_URL_UNRESERVED /* X */,
+ RSPAMD_URL_UNRESERVED /* Y */, RSPAMD_URL_UNRESERVED /* Z */,
+ RSPAMD_URL_HOSTSAFE /* [ */, 0 /* \ */, RSPAMD_URL_HOSTSAFE /* ] */, 0 /* ^ */,
+ RSPAMD_URL_UNRESERVED /* _ */, 0 /* ` */, RSPAMD_URL_UNRESERVED /* a */,
+ RSPAMD_URL_UNRESERVED /* b */, RSPAMD_URL_UNRESERVED /* c */,
+ RSPAMD_URL_UNRESERVED /* d */, RSPAMD_URL_UNRESERVED /* e */,
+ RSPAMD_URL_UNRESERVED /* f */, RSPAMD_URL_UNRESERVED /* g */,
+ RSPAMD_URL_UNRESERVED /* h */, RSPAMD_URL_UNRESERVED /* i */,
+ RSPAMD_URL_UNRESERVED /* j */, RSPAMD_URL_UNRESERVED /* k */,
+ RSPAMD_URL_UNRESERVED /* l */, RSPAMD_URL_UNRESERVED /* m */,
+ RSPAMD_URL_UNRESERVED /* n */, RSPAMD_URL_UNRESERVED /* o */,
+ RSPAMD_URL_UNRESERVED /* p */, RSPAMD_URL_UNRESERVED /* q */,
+ RSPAMD_URL_UNRESERVED /* r */, RSPAMD_URL_UNRESERVED /* s */,
+ RSPAMD_URL_UNRESERVED /* t */, RSPAMD_URL_UNRESERVED /* u */,
+ RSPAMD_URL_UNRESERVED /* v */, RSPAMD_URL_UNRESERVED /* w */,
+ RSPAMD_URL_UNRESERVED /* x */, RSPAMD_URL_UNRESERVED /* y */,
+ RSPAMD_URL_UNRESERVED /* z */, 0 /* { */, 0 /* | */, 0 /* } */,
+ RSPAMD_URL_UNRESERVED /* ~ */, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0};
+
+#define CHECK_URL_COMPONENT(beg, len, flags) \
+ do { \
+ for (i = 0; i < (len); i++) { \
+ if ((rspamd_url_encoding_classes[(guchar) (beg)[i]] & (flags)) == 0) { \
+ dlen += 2; \
+ } \
+ } \
+ } while (0)
+
+#define ENCODE_URL_COMPONENT(beg, len, flags) \
+ do { \
+ for (i = 0; i < (len) && dend > d; i++) { \
+ if ((rspamd_url_encoding_classes[(guchar) (beg)[i]] & (flags)) == 0) { \
+ *d++ = '%'; \
+ *d++ = hexdigests[(guchar) ((beg)[i] >> 4) & 0xf]; \
+ *d++ = hexdigests[(guchar) (beg)[i] & 0xf]; \
+ } \
+ else { \
+ *d++ = (beg)[i]; \
+ } \
+ } \
+ } while (0)
+
+const gchar *
+rspamd_url_encode(struct rspamd_url *url, gsize *pdlen,
+ rspamd_mempool_t *pool)
+{
+ guchar *dest, *d, *dend;
+ static const gchar hexdigests[16] = "0123456789ABCDEF";
+ guint i;
+ gsize dlen = 0;
+
+ g_assert(pdlen != NULL && url != NULL && pool != NULL);
+
+ CHECK_URL_COMPONENT(rspamd_url_host_unsafe(url), url->hostlen,
+ RSPAMD_URL_FLAGS_HOSTSAFE);
+ CHECK_URL_COMPONENT(rspamd_url_user_unsafe(url), url->userlen,
+ RSPAMD_URL_FLAGS_USERSAFE);
+ CHECK_URL_COMPONENT(rspamd_url_data_unsafe(url), url->datalen,
+ RSPAMD_URL_FLAGS_PATHSAFE);
+ CHECK_URL_COMPONENT(rspamd_url_query_unsafe(url), url->querylen,
+ RSPAMD_URL_FLAGS_QUERYSAFE);
+ CHECK_URL_COMPONENT(rspamd_url_fragment_unsafe(url), url->fragmentlen,
+ RSPAMD_URL_FLAGS_FRAGMENTSAFE);
+
+ if (dlen == 0) {
+ *pdlen = url->urllen;
+
+ return url->string;
+ }
+
+ /* Need to encode */
+ dlen += url->urllen + sizeof("telephone://"); /* Protocol hack */
+ dest = rspamd_mempool_alloc(pool, dlen + 1);
+ d = dest;
+ dend = d + dlen;
+
+ if (url->protocollen > 0) {
+ if (!(url->protocol & PROTOCOL_UNKNOWN)) {
+ const gchar *known_proto = rspamd_url_protocol_name(url->protocol);
+ d += rspamd_snprintf((gchar *) d, dend - d,
+ "%s://",
+ known_proto);
+ }
+ else {
+ d += rspamd_snprintf((gchar *) d, dend - d,
+ "%*s://",
+ (gint) url->protocollen, url->string);
+ }
+ }
+ else {
+ d += rspamd_snprintf((gchar *) d, dend - d, "http://");
+ }
+
+ if (url->userlen > 0) {
+ ENCODE_URL_COMPONENT(rspamd_url_user_unsafe(url), url->userlen,
+ RSPAMD_URL_FLAGS_USERSAFE);
+ *d++ = '@';
+ }
+
+ ENCODE_URL_COMPONENT(rspamd_url_host_unsafe(url), url->hostlen,
+ RSPAMD_URL_FLAGS_HOSTSAFE);
+
+ if (url->datalen > 0) {
+ *d++ = '/';
+ ENCODE_URL_COMPONENT(rspamd_url_data_unsafe(url), url->datalen,
+ RSPAMD_URL_FLAGS_PATHSAFE);
+ }
+
+ if (url->querylen > 0) {
+ *d++ = '?';
+ ENCODE_URL_COMPONENT(rspamd_url_query_unsafe(url), url->querylen,
+ RSPAMD_URL_FLAGS_QUERYSAFE);
+ }
+
+ if (url->fragmentlen > 0) {
+ *d++ = '#';
+ ENCODE_URL_COMPONENT(rspamd_url_fragment_unsafe(url), url->fragmentlen,
+ RSPAMD_URL_FLAGS_FRAGMENTSAFE);
+ }
+
+ *pdlen = (d - dest);
+
+ return (const gchar *) dest;
+}
+
+gboolean
+rspamd_url_is_domain(int c)
+{
+ return is_domain((guchar) c);
+}
+
+const gchar *
+rspamd_url_protocol_name(enum rspamd_url_protocol proto)
+{
+ const gchar *ret = "unknown";
+
+ switch (proto) {
+ case PROTOCOL_HTTP:
+ ret = "http";
+ break;
+ case PROTOCOL_HTTPS:
+ ret = "https";
+ break;
+ case PROTOCOL_FTP:
+ ret = "ftp";
+ break;
+ case PROTOCOL_FILE:
+ ret = "file";
+ break;
+ case PROTOCOL_MAILTO:
+ ret = "mailto";
+ break;
+ case PROTOCOL_TELEPHONE:
+ ret = "telephone";
+ break;
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+enum rspamd_url_protocol
+rspamd_url_protocol_from_string(const gchar *str)
+{
+ enum rspamd_url_protocol ret = PROTOCOL_UNKNOWN;
+
+ if (strcmp(str, "http") == 0) {
+ ret = PROTOCOL_HTTP;
+ }
+ else if (strcmp(str, "https") == 0) {
+ ret = PROTOCOL_HTTPS;
+ }
+ else if (strcmp(str, "mailto") == 0) {
+ ret = PROTOCOL_MAILTO;
+ }
+ else if (strcmp(str, "ftp") == 0) {
+ ret = PROTOCOL_FTP;
+ }
+ else if (strcmp(str, "file") == 0) {
+ ret = PROTOCOL_FILE;
+ }
+ else if (strcmp(str, "telephone") == 0) {
+ ret = PROTOCOL_TELEPHONE;
+ }
+
+ return ret;
+}
+
+
+bool rspamd_url_set_add_or_increase(khash_t(rspamd_url_hash) * set,
+ struct rspamd_url *u,
+ bool enforce_replace)
+{
+ khiter_t k;
+ gint r;
+
+ k = kh_get(rspamd_url_hash, set, u);
+
+ if (k != kh_end(set)) {
+ /* Existing url */
+ struct rspamd_url *ex = kh_key(set, k);
+#define SUSPICIOUS_URL_FLAGS (RSPAMD_URL_FLAG_PHISHED | RSPAMD_URL_FLAG_OBSCURED | RSPAMD_URL_FLAG_ZW_SPACES)
+ if (enforce_replace) {
+ kh_key(set, k) = u;
+ u->count++;
+ }
+ else {
+ if (u->flags & SUSPICIOUS_URL_FLAGS) {
+ if (!(ex->flags & SUSPICIOUS_URL_FLAGS)) {
+ /* Propagate new url to an old one */
+ kh_key(set, k) = u;
+ u->count++;
+ }
+ else {
+ ex->count++;
+ }
+ }
+ else {
+ ex->count++;
+ }
+ }
+
+ return false;
+ }
+ else {
+ k = kh_put(rspamd_url_hash, set, u, &r);
+ }
+
+ return true;
+}
+
+struct rspamd_url *
+rspamd_url_set_add_or_return(khash_t(rspamd_url_hash) * set,
+ struct rspamd_url *u)
+{
+ khiter_t k;
+ gint r;
+
+ if (set) {
+ k = kh_get(rspamd_url_hash, set, u);
+
+ if (k != kh_end(set)) {
+ return kh_key(set, k);
+ }
+ else {
+ k = kh_put(rspamd_url_hash, set, u, &r);
+
+ return kh_key(set, k);
+ }
+ }
+
+ return NULL;
+}
+
+bool rspamd_url_host_set_add(khash_t(rspamd_url_host_hash) * set,
+ struct rspamd_url *u)
+{
+ gint r;
+
+ if (set) {
+ kh_put(rspamd_url_host_hash, set, u, &r);
+
+ if (r == 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+bool rspamd_url_set_has(khash_t(rspamd_url_hash) * set, struct rspamd_url *u)
+{
+ khiter_t k;
+
+ if (set) {
+ k = kh_get(rspamd_url_hash, set, u);
+
+ if (k == kh_end(set)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+bool rspamd_url_host_set_has(khash_t(rspamd_url_host_hash) * set, struct rspamd_url *u)
+{
+ khiter_t k;
+
+ if (set) {
+ k = kh_get(rspamd_url_host_hash, set, u);
+
+ if (k == kh_end(set)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+bool rspamd_url_flag_from_string(const gchar *str, gint *flag)
+{
+ gint h = rspamd_cryptobox_fast_hash_specific(RSPAMD_CRYPTOBOX_HASHFAST_INDEPENDENT,
+ str, strlen(str), 0);
+
+ for (int i = 0; i < G_N_ELEMENTS(url_flag_names); i++) {
+ if (url_flag_names[i].hash == h) {
+ *flag |= url_flag_names[i].flag;
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+const gchar *
+rspamd_url_flag_to_string(int flag)
+{
+ for (int i = 0; i < G_N_ELEMENTS(url_flag_names); i++) {
+ if (url_flag_names[i].flag & flag) {
+ return url_flag_names[i].name;
+ }
+ }
+
+ return NULL;
+}
+
+inline int
+rspamd_url_cmp(const struct rspamd_url *u1, const struct rspamd_url *u2)
+{
+ int min_len = MIN(u1->urllen, u2->urllen);
+ int r;
+
+ if (u1->protocol != u2->protocol) {
+ return u1->protocol - u2->protocol;
+ }
+
+ if (u1->protocol & PROTOCOL_MAILTO) {
+ /* Emails specialisation (hosts must be compared in a case insensitive matter */
+ min_len = MIN(u1->hostlen, u2->hostlen);
+
+ if ((r = rspamd_lc_cmp(rspamd_url_host_unsafe(u1),
+ rspamd_url_host_unsafe(u2), min_len)) == 0) {
+ if (u1->hostlen == u2->hostlen) {
+ if (u1->userlen != u2->userlen || u1->userlen == 0) {
+ r = (int) u1->userlen - (int) u2->userlen;
+ }
+ else {
+ r = memcmp(rspamd_url_user_unsafe(u1),
+ rspamd_url_user_unsafe(u2),
+ u1->userlen);
+ }
+ }
+ else {
+ r = u1->hostlen - u2->hostlen;
+ }
+ }
+ }
+ else {
+ if (u1->urllen != u2->urllen) {
+ /* Different length, compare common part and then compare length */
+ r = memcmp(u1->string, u2->string, min_len);
+
+ if (r == 0) {
+ r = u1->urllen - u2->urllen;
+ }
+ }
+ else {
+ /* Equal length */
+ r = memcmp(u1->string, u2->string, u1->urllen);
+ }
+ }
+
+ return r;
+}
+
+int rspamd_url_cmp_qsort(const void *_u1, const void *_u2)
+{
+ const struct rspamd_url *u1 = *(struct rspamd_url **) _u1,
+ *u2 = *(struct rspamd_url **) _u2;
+
+ return rspamd_url_cmp(u1, u2);
+}
diff --git a/src/libserver/url.h b/src/libserver/url.h
new file mode 100644
index 0000000..d1fb8c9
--- /dev/null
+++ b/src/libserver/url.h
@@ -0,0 +1,430 @@
+/* URL check functions */
+#ifndef URL_H
+#define URL_H
+
+#include "config.h"
+#include "mem_pool.h"
+#include "khash.h"
+#include "fstring.h"
+#include "libutil/cxx/utf8_util.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rspamd_task;
+struct rspamd_mime_text_part;
+
+enum rspamd_url_flags {
+ RSPAMD_URL_FLAG_PHISHED = 1u << 0u,
+ RSPAMD_URL_FLAG_NUMERIC = 1u << 1u,
+ RSPAMD_URL_FLAG_OBSCURED = 1u << 2u,
+ RSPAMD_URL_FLAG_REDIRECTED = 1u << 3u,
+ RSPAMD_URL_FLAG_HTML_DISPLAYED = 1u << 4u,
+ RSPAMD_URL_FLAG_FROM_TEXT = 1u << 5u,
+ RSPAMD_URL_FLAG_SUBJECT = 1u << 6u,
+ RSPAMD_URL_FLAG_HOSTENCODED = 1u << 7u,
+ RSPAMD_URL_FLAG_SCHEMAENCODED = 1u << 8u,
+ RSPAMD_URL_FLAG_PATHENCODED = 1u << 9u,
+ RSPAMD_URL_FLAG_QUERYENCODED = 1u << 10u,
+ RSPAMD_URL_FLAG_MISSINGSLASHES = 1u << 11u,
+ RSPAMD_URL_FLAG_IDN = 1u << 12u,
+ RSPAMD_URL_FLAG_HAS_PORT = 1u << 13u,
+ RSPAMD_URL_FLAG_HAS_USER = 1u << 14u,
+ RSPAMD_URL_FLAG_SCHEMALESS = 1u << 15u,
+ RSPAMD_URL_FLAG_UNNORMALISED = 1u << 16u,
+ RSPAMD_URL_FLAG_ZW_SPACES = 1u << 17u,
+ RSPAMD_URL_FLAG_DISPLAY_URL = 1u << 18u,
+ RSPAMD_URL_FLAG_IMAGE = 1u << 19u,
+ RSPAMD_URL_FLAG_QUERY = 1u << 20u,
+ RSPAMD_URL_FLAG_CONTENT = 1u << 21u,
+ RSPAMD_URL_FLAG_NO_TLD = 1u << 22u,
+ RSPAMD_URL_FLAG_TRUNCATED = 1u << 23u,
+ RSPAMD_URL_FLAG_REDIRECT_TARGET = 1u << 24u,
+ RSPAMD_URL_FLAG_INVISIBLE = 1u << 25u,
+ RSPAMD_URL_FLAG_SPECIAL = 1u << 26u,
+
+};
+#define RSPAMD_URL_MAX_FLAG_SHIFT (26u)
+
+struct rspamd_url_tag {
+ const gchar *data;
+ struct rspamd_url_tag *prev, *next;
+};
+
+struct rspamd_url_ext;
+/**
+ * URL structure
+ */
+struct rspamd_url {
+ char *string;
+ char *raw;
+ struct rspamd_url_ext *ext;
+
+ uint32_t flags;
+
+ uint8_t protocol;
+ uint8_t protocollen;
+
+ uint16_t hostshift;
+ uint16_t datashift;
+ uint16_t queryshift;
+ uint16_t fragmentshift;
+ uint16_t tldshift;
+ guint16 usershift;
+ guint16 userlen;
+
+ uint16_t hostlen;
+ uint16_t datalen;
+ uint16_t querylen;
+ uint16_t fragmentlen;
+ uint16_t tldlen;
+ uint16_t count;
+ uint16_t urllen;
+ uint16_t rawlen;
+
+ /* Absolute order of the URL in a message */
+ uint16_t order;
+ /* Order of the URL in a specific part of message */
+ uint16_t part_order;
+};
+
+/**
+ * Rarely used url fields
+ */
+struct rspamd_url_ext {
+ gchar *visible_part;
+ struct rspamd_url *linked_url;
+
+ guint16 port;
+};
+
+#define rspamd_url_user(u) ((u)->userlen > 0 ? (u)->string + (u)->usershift : NULL)
+#define rspamd_url_user_unsafe(u) ((u)->string + (u)->usershift)
+
+#define rspamd_url_host(u) ((u)->hostlen > 0 ? (u)->string + (u)->hostshift : NULL)
+#define rspamd_url_host_unsafe(u) ((u)->string + (u)->hostshift)
+#define rspamd_url_tld_unsafe(u) ((u)->string + (u)->tldshift)
+
+#define rspamd_url_data_unsafe(u) ((u)->string + (u)->datashift)
+#define rspamd_url_query_unsafe(u) ((u)->string + (u)->queryshift)
+#define rspamd_url_fragment_unsafe(u) ((u)->string + (u)->fragmentshift)
+
+enum uri_errno {
+ URI_ERRNO_OK = 0, /* Parsing went well */
+ URI_ERRNO_EMPTY, /* The URI string was empty */
+ URI_ERRNO_INVALID_PROTOCOL, /* No protocol was found */
+ URI_ERRNO_INVALID_PORT, /* Port number is bad */
+ URI_ERRNO_BAD_ENCODING, /* Bad characters encoding */
+ URI_ERRNO_BAD_FORMAT,
+ URI_ERRNO_TLD_MISSING,
+ URI_ERRNO_HOST_MISSING,
+ URI_ERRNO_TOO_LONG,
+};
+
+enum rspamd_url_protocol {
+ PROTOCOL_FILE = 1u << 0u,
+ PROTOCOL_FTP = 1u << 1u,
+ PROTOCOL_HTTP = 1u << 2u,
+ PROTOCOL_HTTPS = 1u << 3u,
+ PROTOCOL_MAILTO = 1u << 4u,
+ PROTOCOL_TELEPHONE = 1u << 5u,
+ PROTOCOL_UNKNOWN = 1u << 7u,
+};
+
+enum rspamd_url_parse_flags {
+ RSPAMD_URL_PARSE_TEXT = 0u,
+ RSPAMD_URL_PARSE_HREF = (1u << 0u),
+ RSPAMD_URL_PARSE_CHECK = (1u << 1u),
+};
+
+enum rspamd_url_find_type {
+ RSPAMD_URL_FIND_ALL = 0,
+ RSPAMD_URL_FIND_STRICT,
+};
+
+/**
+ * Initialize url library
+ * @param cfg
+ */
+void rspamd_url_init(const gchar *tld_file);
+
+void rspamd_url_deinit(void);
+
+/*
+ * Parse urls inside text
+ * @param pool memory pool
+ * @param task task object
+ * @param part current text part
+ * @param is_html turn on html heuristic
+ */
+void rspamd_url_text_extract(rspamd_mempool_t *pool,
+ struct rspamd_task *task,
+ struct rspamd_mime_text_part *part,
+ uint16_t *cur_order,
+ enum rspamd_url_find_type how);
+
+/*
+ * Parse a single url into an uri structure
+ * @param pool memory pool
+ * @param uristring text form of url
+ * @param uri url object, must be pre allocated
+ */
+enum uri_errno rspamd_url_parse(struct rspamd_url *uri,
+ gchar *uristring,
+ gsize len,
+ rspamd_mempool_t *pool,
+ enum rspamd_url_parse_flags flags);
+
+/*
+ * Try to extract url from a text
+ * @param pool memory pool
+ * @param begin begin of text
+ * @param len length of text
+ * @param start storage for start position of url found (or NULL)
+ * @param end storage for end position of url found (or NULL)
+ * @param url_str storage for url string(or NULL)
+ * @return TRUE if url is found in specified text
+ */
+gboolean rspamd_url_find(rspamd_mempool_t *pool,
+ const gchar *begin, gsize len,
+ gchar **url_str,
+ enum rspamd_url_find_type how,
+ goffset *url_pos,
+ gboolean *prefix_added);
+
+/*
+ * Return text representation of url parsing error
+ */
+const gchar *rspamd_url_strerror(int err);
+
+
+/**
+ * Find TLD for a specified host string
+ * @param in input host
+ * @param inlen length of input
+ * @param out output rspamd_ftok_t with tld position
+ * @return TRUE if tld has been found
+ */
+gboolean rspamd_url_find_tld(const gchar *in, gsize inlen, rspamd_ftok_t *out);
+
+typedef gboolean (*url_insert_function)(struct rspamd_url *url,
+ gsize start_offset, gsize end_offset, void *ud);
+
+/**
+ * Search for multiple urls in text and call `func` for each url found
+ * @param pool
+ * @param in
+ * @param inlen
+ * @param is_html
+ * @param func
+ * @param ud
+ */
+void rspamd_url_find_multiple(rspamd_mempool_t *pool,
+ const gchar *in, gsize inlen,
+ enum rspamd_url_find_type how,
+ GPtrArray *nlines,
+ url_insert_function func,
+ gpointer ud);
+
+/**
+ * Search for a single url in text and call `func` for each url found
+ * @param pool
+ * @param in
+ * @param inlen
+ * @param is_html
+ * @param func
+ * @param ud
+ */
+void rspamd_url_find_single(rspamd_mempool_t *pool,
+ const gchar *in, gsize inlen,
+ enum rspamd_url_find_type how,
+ url_insert_function func,
+ gpointer ud);
+
+/**
+ * Generic callback to insert URLs into rspamd_task
+ * @param url
+ * @param start_offset
+ * @param end_offset
+ * @param ud
+ */
+gboolean rspamd_url_task_subject_callback(struct rspamd_url *url,
+ gsize start_offset,
+ gsize end_offset, gpointer ud);
+
+/**
+ * Decode URL encoded string in-place and return new length of a string, src and dst are NULL terminated
+ * @param dst
+ * @param src
+ * @param size
+ * @return
+ */
+gsize rspamd_url_decode(gchar *dst, const gchar *src, gsize size);
+
+/**
+ * Encode url if needed. In this case, memory is allocated from the specific pool.
+ * Returns pointer to begin and encoded length in `dlen`
+ * @param url
+ * @param pool
+ * @return
+ */
+const gchar *rspamd_url_encode(struct rspamd_url *url, gsize *dlen,
+ rspamd_mempool_t *pool);
+
+
+/**
+ * Returns if a character is domain character
+ * @param c
+ * @return
+ */
+gboolean rspamd_url_is_domain(int c);
+
+/**
+ * Returns symbolic name for protocol
+ * @param proto
+ * @return
+ */
+const gchar *rspamd_url_protocol_name(enum rspamd_url_protocol proto);
+
+
+/**
+ * Converts string to a numeric protocol
+ * @param str
+ * @return
+ */
+enum rspamd_url_protocol rspamd_url_protocol_from_string(const gchar *str);
+
+/**
+ * Converts string to a url flag
+ * @param str
+ * @param flag
+ * @return
+ */
+bool rspamd_url_flag_from_string(const gchar *str, gint *flag);
+
+/**
+ * Converts url flag to a string
+ * @param flag
+ * @return
+ */
+const gchar *rspamd_url_flag_to_string(int flag);
+
+/* Defines sets of urls indexed by url as is */
+KHASH_DECLARE(rspamd_url_hash, struct rspamd_url *, char);
+KHASH_DECLARE(rspamd_url_host_hash, struct rspamd_url *, char);
+
+/* Convenience functions for url sets */
+/**
+ * Add an url to set or increase the existing url count
+ * @param set
+ * @param u
+ * @return true if a new url has been added
+ */
+bool rspamd_url_set_add_or_increase(khash_t(rspamd_url_hash) * set,
+ struct rspamd_url *u,
+ bool enforce_replace);
+
+/**
+ * Same as rspamd_url_set_add_or_increase but returns the existing url if found
+ * @param set
+ * @param u
+ * @return
+ */
+struct rspamd_url *rspamd_url_set_add_or_return(khash_t(rspamd_url_hash) * set,
+ struct rspamd_url *u);
+/**
+ * Helper for url host set
+ * @param set
+ * @param u
+ * @return
+ */
+bool rspamd_url_host_set_add(khash_t(rspamd_url_host_hash) * set,
+ struct rspamd_url *u);
+/**
+ * Checks if a url is in set
+ * @param set
+ * @param u
+ * @return
+ */
+bool rspamd_url_set_has(khash_t(rspamd_url_hash) * set, struct rspamd_url *u);
+
+bool rspamd_url_host_set_has(khash_t(rspamd_url_host_hash) * set, struct rspamd_url *u);
+
+/**
+ * Compares two urls (similar to C comparison functions) lexicographically
+ * @param u1
+ * @param u2
+ * @return
+ */
+int rspamd_url_cmp(const struct rspamd_url *u1, const struct rspamd_url *u2);
+
+/**
+ * Same but used for qsort to sort `struct rspamd_url *[]` array
+ * @param u1
+ * @param u2
+ * @return
+ */
+int rspamd_url_cmp_qsort(const void *u1, const void *u2);
+
+/**
+ * Returns a port for some url
+ * @param u
+ * @return
+ */
+static RSPAMD_PURE_FUNCTION inline uint16_t rspamd_url_get_port(struct rspamd_url *u)
+{
+ if ((u->flags & RSPAMD_URL_FLAG_HAS_PORT) && u->ext) {
+ return u->ext->port;
+ }
+ else {
+ /* Assume standard port */
+ if (u->protocol == PROTOCOL_HTTPS) {
+ return 443;
+ }
+ else {
+ return 80;
+ }
+ }
+}
+
+/**
+ * Returns a port for some url if it is set
+ * @param u
+ * @return
+ */
+static RSPAMD_PURE_FUNCTION inline uint16_t rspamd_url_get_port_if_special(struct rspamd_url *u)
+{
+ if ((u->flags & RSPAMD_URL_FLAG_HAS_PORT) && u->ext) {
+ return u->ext->port;
+ }
+
+ return 0;
+}
+
+/**
+ * Normalize unicode input and set out url flags as appropriate
+ * @param pool
+ * @param input
+ * @param len_out (must be &var)
+ * @param url_flags_out (must be just a var with no dereference)
+ */
+#define rspamd_url_normalise_propagate_flags(pool, input, len_out, url_flags_out) \
+ do { \
+ enum rspamd_utf8_normalise_result norm_res; \
+ norm_res = rspamd_normalise_unicode_inplace((input), (len_out)); \
+ if (norm_res & RSPAMD_UNICODE_NORM_UNNORMAL) { \
+ url_flags_out |= RSPAMD_URL_FLAG_UNNORMALISED; \
+ } \
+ if (norm_res & RSPAMD_UNICODE_NORM_ZERO_SPACES) { \
+ url_flags_out |= RSPAMD_URL_FLAG_ZW_SPACES; \
+ } \
+ if (norm_res & (RSPAMD_UNICODE_NORM_ERROR)) { \
+ url_flags_out |= RSPAMD_URL_FLAG_OBSCURED; \
+ } \
+ } while (0)
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/libserver/worker_util.c b/src/libserver/worker_util.c
new file mode 100644
index 0000000..74a3cf8
--- /dev/null
+++ b/src/libserver/worker_util.c
@@ -0,0 +1,2313 @@
+/*
+ * 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 "rspamd.h"
+#include "lua/lua_common.h"
+#include "worker_util.h"
+#include "unix-std.h"
+#include "utlist.h"
+#include "ottery.h"
+#include "rspamd_control.h"
+#include "libserver/maps/map.h"
+#include "libserver/maps/map_private.h"
+#include "libserver/http/http_private.h"
+#include "libserver/http/http_router.h"
+#include "libutil/rrd.h"
+
+/* sys/resource.h */
+#ifdef HAVE_SYS_RESOURCE_H
+#include <sys/resource.h>
+#endif
+/* pwd and grp */
+#ifdef HAVE_PWD_H
+#include <pwd.h>
+#endif
+#ifdef HAVE_GRP_H
+#include <grp.h>
+#endif
+#ifdef HAVE_LIBUTIL_H
+#include <libutil.h>
+#endif
+#include "zlib.h"
+
+#ifdef HAVE_UCONTEXT_H
+#include <ucontext.h>
+#elif defined(HAVE_SYS_UCONTEXT_H)
+#include <sys/ucontext.h>
+#endif
+
+#ifdef HAVE_SYS_WAIT_H
+#include <sys/wait.h>
+#include <math.h>
+
+#endif
+
+#include "contrib/libev/ev.h"
+#include "libstat/stat_api.h"
+
+struct rspamd_worker *rspamd_current_worker = NULL;
+
+/* Forward declaration */
+static void rspamd_worker_heartbeat_start(struct rspamd_worker *,
+ struct ev_loop *);
+
+static void rspamd_worker_ignore_signal(struct rspamd_worker_signal_handler *);
+/**
+ * Return worker's control structure by its type
+ * @param type
+ * @return worker's control structure or NULL
+ */
+worker_t *
+rspamd_get_worker_by_type(struct rspamd_config *cfg, GQuark type)
+{
+ worker_t **pwrk;
+
+ pwrk = cfg->compiled_workers;
+ while (pwrk && *pwrk) {
+ if (rspamd_check_worker(cfg, *pwrk)) {
+ if (g_quark_from_string((*pwrk)->name) == type) {
+ return *pwrk;
+ }
+ }
+
+ pwrk++;
+ }
+
+ return NULL;
+}
+
+static void
+rspamd_worker_check_finished(EV_P_ ev_timer *w, int revents)
+{
+ int *pnchecks = (int *) w->data;
+
+ if (*pnchecks > SOFT_SHUTDOWN_TIME * 10) {
+ msg_warn("terminating worker before finishing of terminate handlers");
+ ev_break(EV_A_ EVBREAK_ONE);
+ }
+ else {
+ int refcount = ev_active_cnt(EV_A);
+
+ if (refcount == 1) {
+ ev_break(EV_A_ EVBREAK_ONE);
+ }
+ else {
+ ev_timer_again(EV_A_ w);
+ }
+ }
+}
+
+static gboolean
+rspamd_worker_finalize(gpointer user_data)
+{
+ struct rspamd_task *task = user_data;
+
+ if (!(task->flags & RSPAMD_TASK_FLAG_PROCESSING)) {
+ msg_info_task("finishing actions has been processed, terminating");
+ /* ev_break (task->event_loop, EVBREAK_ALL); */
+ task->worker->state = rspamd_worker_wanna_die;
+ rspamd_session_destroy(task->s);
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+gboolean
+rspamd_worker_call_finish_handlers(struct rspamd_worker *worker)
+{
+ struct rspamd_task *task;
+ struct rspamd_config *cfg = worker->srv->cfg;
+ struct rspamd_abstract_worker_ctx *ctx;
+ struct rspamd_config_cfg_lua_script *sc;
+
+ if (cfg->on_term_scripts) {
+ ctx = (struct rspamd_abstract_worker_ctx *) worker->ctx;
+ /* Create a fake task object for async events */
+ task = rspamd_task_new(worker, cfg, NULL, NULL, ctx->event_loop, FALSE);
+ task->resolver = ctx->resolver;
+ task->flags |= RSPAMD_TASK_FLAG_PROCESSING;
+ task->s = rspamd_session_create(task->task_pool,
+ rspamd_worker_finalize,
+ NULL,
+ (event_finalizer_t) rspamd_task_free,
+ task);
+
+ DL_FOREACH(cfg->on_term_scripts, sc)
+ {
+ lua_call_finish_script(sc, task);
+ }
+
+ task->flags &= ~RSPAMD_TASK_FLAG_PROCESSING;
+
+ if (rspamd_session_pending(task->s)) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static void
+rspamd_worker_terminate_handlers(struct rspamd_worker *w)
+{
+ if (w->nconns == 0 &&
+ (!(w->flags & RSPAMD_WORKER_SCANNER) || w->srv->cfg->on_term_scripts == NULL)) {
+ /*
+ * We are here either:
+ * - No active connections are represented
+ * - No term scripts are registered
+ * - Worker is not a scanner, so it can die safely
+ */
+ w->state = rspamd_worker_wanna_die;
+ }
+ else {
+ if (w->nconns > 0) {
+ /*
+ * Wait until all connections are terminated
+ */
+ w->state = rspamd_worker_wait_connections;
+ }
+ else {
+ /*
+ * Start finish scripts
+ */
+ if (w->state != rspamd_worker_wait_final_scripts) {
+ w->state = rspamd_worker_wait_final_scripts;
+
+ if ((w->flags & RSPAMD_WORKER_SCANNER) &&
+ rspamd_worker_call_finish_handlers(w)) {
+ msg_info("performing async finishing actions");
+ w->state = rspamd_worker_wait_final_scripts;
+ }
+ else {
+ /*
+ * We are done now
+ */
+ msg_info("no async finishing actions, terminating");
+ w->state = rspamd_worker_wanna_die;
+ }
+ }
+ }
+ }
+}
+
+static void
+rspamd_worker_on_delayed_shutdown(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_worker *worker = (struct rspamd_worker *) w->data;
+
+ worker->state = rspamd_worker_wanna_die;
+ ev_timer_stop(EV_A_ w);
+ ev_break(loop, EVBREAK_ALL);
+}
+
+static void
+rspamd_worker_shutdown_check(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_worker *worker = (struct rspamd_worker *) w->data;
+
+ if (worker->state != rspamd_worker_wanna_die) {
+ rspamd_worker_terminate_handlers(worker);
+
+ if (worker->state == rspamd_worker_wanna_die) {
+ /* We are done, kill event loop */
+ ev_timer_stop(EV_A_ w);
+ ev_break(EV_A_ EVBREAK_ALL);
+ }
+ else {
+ /* Try again later */
+ ev_timer_again(EV_A_ w);
+ }
+ }
+ else {
+ ev_timer_stop(EV_A_ w);
+ ev_break(EV_A_ EVBREAK_ALL);
+ }
+}
+
+/*
+ * Config reload is designed by sending sigusr2 to active workers and pending shutdown of them
+ */
+static gboolean
+rspamd_worker_usr2_handler(struct rspamd_worker_signal_handler *sigh, void *arg)
+{
+ /* Do not accept new connections, preparing to end worker's process */
+ if (sigh->worker->state == rspamd_worker_state_running) {
+ static ev_timer shutdown_ev, shutdown_check_ev;
+ ev_tstamp shutdown_ts;
+
+ if (sigh->worker->flags & RSPAMD_WORKER_NO_TERMINATE_DELAY) {
+ shutdown_ts = 0.0;
+ }
+ else {
+ shutdown_ts = MAX(SOFT_SHUTDOWN_TIME,
+ sigh->worker->srv->cfg->task_timeout * 2.0);
+ }
+
+ rspamd_worker_ignore_signal(sigh);
+ sigh->worker->state = rspamd_worker_state_terminating;
+
+ rspamd_default_log_function(G_LOG_LEVEL_INFO,
+ sigh->worker->srv->server_pool->tag.tagname,
+ sigh->worker->srv->server_pool->tag.uid,
+ G_STRFUNC,
+ "worker's shutdown is pending in %.2f sec",
+ shutdown_ts);
+
+ /* Soft shutdown timer */
+ shutdown_ev.data = sigh->worker;
+ ev_timer_init(&shutdown_ev, rspamd_worker_on_delayed_shutdown,
+ shutdown_ts, 0.0);
+ ev_timer_start(sigh->event_loop, &shutdown_ev);
+
+ if (!(sigh->worker->flags & RSPAMD_WORKER_NO_TERMINATE_DELAY)) {
+ /* This timer checks if we are ready to die and is called frequently */
+ shutdown_check_ev.data = sigh->worker;
+ ev_timer_init(&shutdown_check_ev, rspamd_worker_shutdown_check,
+ 0.5, 0.5);
+ ev_timer_start(sigh->event_loop, &shutdown_check_ev);
+ }
+
+ rspamd_worker_stop_accept(sigh->worker);
+ }
+
+ /* No more signals */
+ return FALSE;
+}
+
+/*
+ * Reopen log is designed by sending sigusr1 to active workers and pending shutdown of them
+ */
+static gboolean
+rspamd_worker_usr1_handler(struct rspamd_worker_signal_handler *sigh, void *arg)
+{
+ struct rspamd_main *rspamd_main = sigh->worker->srv;
+
+ rspamd_log_reopen(sigh->worker->srv->logger, rspamd_main->cfg, -1, -1);
+ msg_info_main("logging reinitialised");
+
+ /* Get more signals */
+ return TRUE;
+}
+
+static gboolean
+rspamd_worker_term_handler(struct rspamd_worker_signal_handler *sigh, void *arg)
+{
+ if (sigh->worker->state == rspamd_worker_state_running) {
+ static ev_timer shutdown_ev, shutdown_check_ev;
+ ev_tstamp shutdown_ts;
+
+ if (sigh->worker->flags & RSPAMD_WORKER_NO_TERMINATE_DELAY) {
+ shutdown_ts = 0.0;
+ }
+ else {
+ shutdown_ts = MAX(SOFT_SHUTDOWN_TIME,
+ sigh->worker->srv->cfg->task_timeout * 2.0);
+ }
+
+ rspamd_worker_ignore_signal(sigh);
+ sigh->worker->state = rspamd_worker_state_terminating;
+ rspamd_default_log_function(G_LOG_LEVEL_INFO,
+ sigh->worker->srv->server_pool->tag.tagname,
+ sigh->worker->srv->server_pool->tag.uid,
+ G_STRFUNC,
+ "terminating after receiving signal %s",
+ g_strsignal(sigh->signo));
+
+ rspamd_worker_stop_accept(sigh->worker);
+ rspamd_worker_terminate_handlers(sigh->worker);
+
+ /* Check if we are ready to die */
+ if (sigh->worker->state != rspamd_worker_wanna_die) {
+ /* This timer is called when we have no choices but to die */
+ shutdown_ev.data = sigh->worker;
+ ev_timer_init(&shutdown_ev, rspamd_worker_on_delayed_shutdown,
+ shutdown_ts, 0.0);
+ ev_timer_start(sigh->event_loop, &shutdown_ev);
+
+ if (!(sigh->worker->flags & RSPAMD_WORKER_NO_TERMINATE_DELAY)) {
+ /* This timer checks if we are ready to die and is called frequently */
+ shutdown_check_ev.data = sigh->worker;
+ ev_timer_init(&shutdown_check_ev, rspamd_worker_shutdown_check,
+ 0.5, 0.5);
+ ev_timer_start(sigh->event_loop, &shutdown_check_ev);
+ }
+ }
+ else {
+ /* Flag to die has been already set */
+ ev_break(sigh->event_loop, EVBREAK_ALL);
+ }
+ }
+
+ /* Stop reacting on signals */
+ return FALSE;
+}
+
+static void
+rspamd_worker_signal_handle(EV_P_ ev_signal *w, int revents)
+{
+ struct rspamd_worker_signal_handler *sigh =
+ (struct rspamd_worker_signal_handler *) w->data;
+ struct rspamd_worker_signal_handler_elt *cb, *cbtmp;
+
+ /* Call all signal handlers registered */
+ DL_FOREACH_SAFE(sigh->cb, cb, cbtmp)
+ {
+ if (!cb->handler(sigh, cb->handler_data)) {
+ DL_DELETE(sigh->cb, cb);
+ g_free(cb);
+ }
+ }
+}
+
+static void
+rspamd_worker_ignore_signal(struct rspamd_worker_signal_handler *sigh)
+{
+ sigset_t set;
+
+ ev_signal_stop(sigh->event_loop, &sigh->ev_sig);
+ sigemptyset(&set);
+ sigaddset(&set, sigh->signo);
+ sigprocmask(SIG_BLOCK, &set, NULL);
+}
+
+static void
+rspamd_worker_default_signal(int signo)
+{
+ struct sigaction sig;
+
+ sigemptyset(&sig.sa_mask);
+ sigaddset(&sig.sa_mask, signo);
+ sig.sa_handler = SIG_DFL;
+ sig.sa_flags = 0;
+ sigaction(signo, &sig, NULL);
+}
+
+static void
+rspamd_sigh_free(void *p)
+{
+ struct rspamd_worker_signal_handler *sigh = p;
+ struct rspamd_worker_signal_handler_elt *cb, *tmp;
+
+ DL_FOREACH_SAFE(sigh->cb, cb, tmp)
+ {
+ DL_DELETE(sigh->cb, cb);
+ g_free(cb);
+ }
+
+ ev_signal_stop(sigh->event_loop, &sigh->ev_sig);
+ rspamd_worker_default_signal(sigh->signo);
+ g_free(sigh);
+}
+
+void rspamd_worker_set_signal_handler(int signo, struct rspamd_worker *worker,
+ struct ev_loop *event_loop,
+ rspamd_worker_signal_cb_t handler,
+ void *handler_data)
+{
+ struct rspamd_worker_signal_handler *sigh;
+ struct rspamd_worker_signal_handler_elt *cb;
+
+ sigh = g_hash_table_lookup(worker->signal_events, GINT_TO_POINTER(signo));
+
+ if (sigh == NULL) {
+ sigh = g_malloc0(sizeof(*sigh));
+ sigh->signo = signo;
+ sigh->worker = worker;
+ sigh->event_loop = event_loop;
+ sigh->enabled = TRUE;
+
+ sigh->ev_sig.data = sigh;
+ ev_signal_init(&sigh->ev_sig, rspamd_worker_signal_handle, signo);
+ ev_signal_start(event_loop, &sigh->ev_sig);
+
+ g_hash_table_insert(worker->signal_events,
+ GINT_TO_POINTER(signo),
+ sigh);
+ }
+
+ cb = g_malloc0(sizeof(*cb));
+ cb->handler = handler;
+ cb->handler_data = handler_data;
+ DL_APPEND(sigh->cb, cb);
+}
+
+void rspamd_worker_init_signals(struct rspamd_worker *worker,
+ struct ev_loop *event_loop)
+{
+ /* A set of terminating signals */
+ rspamd_worker_set_signal_handler(SIGTERM, worker, event_loop,
+ rspamd_worker_term_handler, NULL);
+ rspamd_worker_set_signal_handler(SIGINT, worker, event_loop,
+ rspamd_worker_term_handler, NULL);
+ rspamd_worker_set_signal_handler(SIGHUP, worker, event_loop,
+ rspamd_worker_term_handler, NULL);
+
+ /* Special purpose signals */
+ rspamd_worker_set_signal_handler(SIGUSR1, worker, event_loop,
+ rspamd_worker_usr1_handler, NULL);
+ rspamd_worker_set_signal_handler(SIGUSR2, worker, event_loop,
+ rspamd_worker_usr2_handler, NULL);
+}
+
+
+struct ev_loop *
+rspamd_prepare_worker(struct rspamd_worker *worker, const char *name,
+ rspamd_accept_handler hdl)
+{
+ struct ev_loop *event_loop;
+ GList *cur;
+ struct rspamd_worker_listen_socket *ls;
+ struct rspamd_worker_accept_event *accept_ev;
+
+ worker->signal_events = g_hash_table_new_full(g_direct_hash, g_direct_equal,
+ NULL, rspamd_sigh_free);
+
+ event_loop = ev_loop_new(rspamd_config_ev_backend_get(worker->srv->cfg));
+
+ worker->srv->event_loop = event_loop;
+
+ rspamd_worker_init_signals(worker, event_loop);
+ rspamd_control_worker_add_default_cmd_handlers(worker, event_loop);
+ rspamd_worker_heartbeat_start(worker, event_loop);
+ rspamd_redis_pool_config(worker->srv->cfg->redis_pool,
+ worker->srv->cfg, event_loop);
+
+ /* Accept all sockets */
+ if (hdl) {
+ cur = worker->cf->listen_socks;
+
+ while (cur) {
+ ls = cur->data;
+
+ if (ls->fd != -1) {
+ accept_ev = g_malloc0(sizeof(*accept_ev));
+ accept_ev->event_loop = event_loop;
+ accept_ev->accept_ev.data = worker;
+ ev_io_init(&accept_ev->accept_ev, hdl, ls->fd, EV_READ);
+ ev_io_start(event_loop, &accept_ev->accept_ev);
+
+ DL_APPEND(worker->accept_events, accept_ev);
+ }
+
+ cur = g_list_next(cur);
+ }
+ }
+
+ return event_loop;
+}
+
+void rspamd_worker_stop_accept(struct rspamd_worker *worker)
+{
+ struct rspamd_worker_accept_event *cur, *tmp;
+
+ /* Remove all events */
+ DL_FOREACH_SAFE(worker->accept_events, cur, tmp)
+ {
+
+ if (ev_can_stop(&cur->accept_ev)) {
+ ev_io_stop(cur->event_loop, &cur->accept_ev);
+ }
+
+
+ if (ev_can_stop(&cur->throttling_ev)) {
+ ev_timer_stop(cur->event_loop, &cur->throttling_ev);
+ }
+
+ g_free(cur);
+ }
+
+ /* XXX: we need to do it much later */
+#if 0
+ g_hash_table_iter_init (&it, worker->signal_events);
+
+ while (g_hash_table_iter_next (&it, &k, &v)) {
+ sigh = (struct rspamd_worker_signal_handler *)v;
+ g_hash_table_iter_steal (&it);
+
+ if (sigh->enabled) {
+ event_del (&sigh->ev);
+ }
+
+ g_free (sigh);
+ }
+
+ g_hash_table_unref (worker->signal_events);
+#endif
+}
+
+static rspamd_fstring_t *
+rspamd_controller_maybe_compress(struct rspamd_http_connection_entry *entry,
+ rspamd_fstring_t *buf, struct rspamd_http_message *msg)
+{
+ if (entry->support_gzip) {
+ if (rspamd_fstring_gzip(&buf)) {
+ rspamd_http_message_add_header(msg, "Content-Encoding", "gzip");
+ }
+ }
+
+ return buf;
+}
+
+void rspamd_controller_send_error(struct rspamd_http_connection_entry *entry,
+ gint code, const gchar *error_msg, ...)
+{
+ struct rspamd_http_message *msg;
+ va_list args;
+ rspamd_fstring_t *reply;
+
+ msg = rspamd_http_new_message(HTTP_RESPONSE);
+
+ va_start(args, error_msg);
+ msg->status = rspamd_fstring_new();
+ rspamd_vprintf_fstring(&msg->status, error_msg, args);
+ va_end(args);
+
+ msg->date = time(NULL);
+ msg->code = code;
+ reply = rspamd_fstring_sized_new(msg->status->len + 16);
+ rspamd_printf_fstring(&reply, "{\"error\":\"%V\"}", msg->status);
+ rspamd_http_message_set_body_from_fstring_steal(msg,
+ rspamd_controller_maybe_compress(entry, reply, msg));
+ rspamd_http_connection_reset(entry->conn);
+ rspamd_http_router_insert_headers(entry->rt, msg);
+ rspamd_http_connection_write_message(entry->conn,
+ msg,
+ NULL,
+ "application/json",
+ entry,
+ entry->rt->timeout);
+ entry->is_reply = TRUE;
+}
+
+void rspamd_controller_send_openmetrics(struct rspamd_http_connection_entry *entry,
+ rspamd_fstring_t *str)
+{
+ struct rspamd_http_message *msg;
+
+ msg = rspamd_http_new_message(HTTP_RESPONSE);
+ msg->date = time(NULL);
+ msg->code = 200;
+ msg->status = rspamd_fstring_new_init("OK", 2);
+
+ rspamd_http_message_set_body_from_fstring_steal(msg,
+ rspamd_controller_maybe_compress(entry, str, msg));
+ rspamd_http_connection_reset(entry->conn);
+ rspamd_http_router_insert_headers(entry->rt, msg);
+ rspamd_http_connection_write_message(entry->conn,
+ msg,
+ NULL,
+ "application/openmetrics-text; version=1.0.0; charset=utf-8",
+ entry,
+ entry->rt->timeout);
+ entry->is_reply = TRUE;
+}
+
+void rspamd_controller_send_string(struct rspamd_http_connection_entry *entry,
+ const gchar *str)
+{
+ struct rspamd_http_message *msg;
+ rspamd_fstring_t *reply;
+
+ msg = rspamd_http_new_message(HTTP_RESPONSE);
+ msg->date = time(NULL);
+ msg->code = 200;
+ msg->status = rspamd_fstring_new_init("OK", 2);
+
+ if (str) {
+ reply = rspamd_fstring_new_init(str, strlen(str));
+ }
+ else {
+ reply = rspamd_fstring_new_init("null", 4);
+ }
+
+ rspamd_http_message_set_body_from_fstring_steal(msg,
+ rspamd_controller_maybe_compress(entry, reply, msg));
+ rspamd_http_connection_reset(entry->conn);
+ rspamd_http_router_insert_headers(entry->rt, msg);
+ rspamd_http_connection_write_message(entry->conn,
+ msg,
+ NULL,
+ "application/json",
+ entry,
+ entry->rt->timeout);
+ entry->is_reply = TRUE;
+}
+
+void rspamd_controller_send_ucl(struct rspamd_http_connection_entry *entry,
+ ucl_object_t *obj)
+{
+ struct rspamd_http_message *msg;
+ rspamd_fstring_t *reply;
+
+ msg = rspamd_http_new_message(HTTP_RESPONSE);
+ msg->date = time(NULL);
+ msg->code = 200;
+ msg->status = rspamd_fstring_new_init("OK", 2);
+ reply = rspamd_fstring_sized_new(BUFSIZ);
+ rspamd_ucl_emit_fstring(obj, UCL_EMIT_JSON_COMPACT, &reply);
+ rspamd_http_message_set_body_from_fstring_steal(msg,
+ rspamd_controller_maybe_compress(entry, reply, msg));
+ rspamd_http_connection_reset(entry->conn);
+ rspamd_http_router_insert_headers(entry->rt, msg);
+ rspamd_http_connection_write_message(entry->conn,
+ msg,
+ NULL,
+ "application/json",
+ entry,
+ entry->rt->timeout);
+ entry->is_reply = TRUE;
+}
+
+static void
+rspamd_worker_drop_priv(struct rspamd_main *rspamd_main)
+{
+ if (rspamd_main->is_privileged) {
+ if (setgid(rspamd_main->workers_gid) == -1) {
+ msg_err_main("cannot setgid to %d (%s), aborting",
+ (gint) rspamd_main->workers_gid,
+ strerror(errno));
+ exit(-errno);
+ }
+
+ if (rspamd_main->cfg->rspamd_user &&
+ initgroups(rspamd_main->cfg->rspamd_user,
+ rspamd_main->workers_gid) == -1) {
+ msg_err_main("initgroups failed (%s), aborting", strerror(errno));
+ exit(-errno);
+ }
+
+ if (setuid(rspamd_main->workers_uid) == -1) {
+ msg_err_main("cannot setuid to %d (%s), aborting",
+ (gint) rspamd_main->workers_uid,
+ strerror(errno));
+ exit(-errno);
+ }
+ }
+}
+
+static void
+rspamd_worker_set_limits(struct rspamd_main *rspamd_main,
+ struct rspamd_worker_conf *cf)
+{
+ struct rlimit rlmt;
+
+ if (cf->rlimit_nofile != 0) {
+ rlmt.rlim_cur = (rlim_t) cf->rlimit_nofile;
+ rlmt.rlim_max = (rlim_t) cf->rlimit_nofile;
+
+ if (setrlimit(RLIMIT_NOFILE, &rlmt) == -1) {
+ msg_warn_main("cannot set files rlimit: %L, %s",
+ cf->rlimit_nofile,
+ strerror(errno));
+ }
+
+ memset(&rlmt, 0, sizeof(rlmt));
+
+ if (getrlimit(RLIMIT_NOFILE, &rlmt) == -1) {
+ msg_warn_main("cannot get max files rlimit: %HL, %s",
+ cf->rlimit_maxcore,
+ strerror(errno));
+ }
+ else {
+ msg_info_main("set max file descriptors limit: %HL cur and %HL max",
+ (guint64) rlmt.rlim_cur,
+ (guint64) rlmt.rlim_max);
+ }
+ }
+ else {
+ /* Just report */
+ if (getrlimit(RLIMIT_NOFILE, &rlmt) == -1) {
+ msg_warn_main("cannot get max files rlimit: %HL, %s",
+ cf->rlimit_maxcore,
+ strerror(errno));
+ }
+ else {
+ msg_info_main("use system max file descriptors limit: %HL cur and %HL max",
+ (guint64) rlmt.rlim_cur,
+ (guint64) rlmt.rlim_max);
+ }
+ }
+
+ if (rspamd_main->cores_throttling) {
+ msg_info_main("disable core files for the new worker as limits are reached");
+ rlmt.rlim_cur = 0;
+ rlmt.rlim_max = 0;
+
+ if (setrlimit(RLIMIT_CORE, &rlmt) == -1) {
+ msg_warn_main("cannot disable core dumps: error when setting limits: %s",
+ strerror(errno));
+ }
+ }
+ else {
+ if (cf->rlimit_maxcore != 0) {
+ rlmt.rlim_cur = (rlim_t) cf->rlimit_maxcore;
+ rlmt.rlim_max = (rlim_t) cf->rlimit_maxcore;
+
+ if (setrlimit(RLIMIT_CORE, &rlmt) == -1) {
+ msg_warn_main("cannot set max core size limit: %HL, %s",
+ cf->rlimit_maxcore,
+ strerror(errno));
+ }
+
+ /* Ensure that we did it */
+ memset(&rlmt, 0, sizeof(rlmt));
+
+ if (getrlimit(RLIMIT_CORE, &rlmt) == -1) {
+ msg_warn_main("cannot get max core size rlimit: %HL, %s",
+ cf->rlimit_maxcore,
+ strerror(errno));
+ }
+ else {
+ if (rlmt.rlim_cur != cf->rlimit_maxcore ||
+ rlmt.rlim_max != cf->rlimit_maxcore) {
+ msg_warn_main("setting of core file limits was unsuccessful: "
+ "%HL was wanted, "
+ "but we have %HL cur and %HL max",
+ cf->rlimit_maxcore,
+ (guint64) rlmt.rlim_cur,
+ (guint64) rlmt.rlim_max);
+ }
+ else {
+ msg_info_main("set max core size limit: %HL cur and %HL max",
+ (guint64) rlmt.rlim_cur,
+ (guint64) rlmt.rlim_max);
+ }
+ }
+ }
+ else {
+ /* Just report */
+ if (getrlimit(RLIMIT_CORE, &rlmt) == -1) {
+ msg_warn_main("cannot get max core size limit: %HL, %s",
+ cf->rlimit_maxcore,
+ strerror(errno));
+ }
+ else {
+ msg_info_main("use system max core size limit: %HL cur and %HL max",
+ (guint64) rlmt.rlim_cur,
+ (guint64) rlmt.rlim_max);
+ }
+ }
+ }
+}
+
+static void
+rspamd_worker_on_term(EV_P_ ev_child *w, int revents)
+{
+ struct rspamd_worker *wrk = (struct rspamd_worker *) w->data;
+
+ if (wrk->ppid == getpid()) {
+ if (wrk->term_handler) {
+ wrk->term_handler(EV_A_ w, wrk->srv, wrk);
+ }
+ else {
+ rspamd_check_termination_clause(wrk->srv, wrk, w->rstatus);
+ }
+ }
+ else {
+ /* Ignore SIGCHLD for not our children... */
+ }
+}
+
+static void
+rspamd_worker_heartbeat_cb(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_worker *wrk = (struct rspamd_worker *) w->data;
+ struct rspamd_srv_command cmd;
+
+ memset(&cmd, 0, sizeof(cmd));
+ cmd.type = RSPAMD_SRV_HEARTBEAT;
+ rspamd_srv_send_command(wrk, EV_A, &cmd, -1, NULL, NULL);
+}
+
+static void
+rspamd_worker_heartbeat_start(struct rspamd_worker *wrk, struct ev_loop *event_loop)
+{
+ wrk->hb.heartbeat_ev.data = (void *) wrk;
+ ev_timer_init(&wrk->hb.heartbeat_ev, rspamd_worker_heartbeat_cb,
+ 0.0, wrk->srv->cfg->heartbeat_interval);
+ ev_timer_start(event_loop, &wrk->hb.heartbeat_ev);
+}
+
+static void
+rspamd_main_heartbeat_cb(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_worker *wrk = (struct rspamd_worker *) w->data;
+ gdouble time_from_last = ev_time();
+ struct rspamd_main *rspamd_main;
+ static struct rspamd_control_command cmd;
+ struct tm tm;
+ gchar timebuf[64];
+ gchar usec_buf[16];
+ gint r;
+
+ time_from_last -= wrk->hb.last_event;
+ rspamd_main = wrk->srv;
+
+ if (wrk->hb.last_event > 0 &&
+ time_from_last > 0 &&
+ time_from_last >= rspamd_main->cfg->heartbeat_interval * 2) {
+
+ rspamd_localtime(wrk->hb.last_event, &tm);
+ r = strftime(timebuf, sizeof(timebuf), "%F %H:%M:%S", &tm);
+ rspamd_snprintf(usec_buf, sizeof(usec_buf), "%.5f",
+ wrk->hb.last_event - (gdouble) (time_t) wrk->hb.last_event);
+ rspamd_snprintf(timebuf + r, sizeof(timebuf) - r,
+ "%s", usec_buf + 1);
+
+ if (wrk->hb.nbeats > 0) {
+ /* First time lost event */
+ cmd.type = RSPAMD_CONTROL_CHILD_CHANGE;
+ cmd.cmd.child_change.what = rspamd_child_offline;
+ cmd.cmd.child_change.pid = wrk->pid;
+ rspamd_control_broadcast_srv_cmd(rspamd_main, &cmd, wrk->pid);
+ msg_warn_main("lost heartbeat from worker type %s with pid %P, "
+ "last beat on: %s (%L beats received previously)",
+ g_quark_to_string(wrk->type), wrk->pid,
+ timebuf,
+ wrk->hb.nbeats);
+ wrk->hb.nbeats = -1;
+ /* TODO: send notify about worker problem */
+ }
+ else {
+ wrk->hb.nbeats--;
+ msg_warn_main("lost %L heartbeat from worker type %s with pid %P, "
+ "last beat on: %s",
+ -(wrk->hb.nbeats),
+ g_quark_to_string(wrk->type),
+ wrk->pid,
+ timebuf);
+
+ if (rspamd_main->cfg->heartbeats_loss_max > 0 &&
+ -(wrk->hb.nbeats) >= rspamd_main->cfg->heartbeats_loss_max) {
+
+
+ if (-(wrk->hb.nbeats) > rspamd_main->cfg->heartbeats_loss_max + 1) {
+ msg_err_main("force kill worker type %s with pid %P, "
+ "last beat on: %s; %L heartbeat lost",
+ g_quark_to_string(wrk->type),
+ wrk->pid,
+ timebuf,
+ -(wrk->hb.nbeats));
+ kill(wrk->pid, SIGKILL);
+ }
+ else {
+ msg_err_main("terminate worker type %s with pid %P, "
+ "last beat on: %s; %L heartbeat lost",
+ g_quark_to_string(wrk->type),
+ wrk->pid,
+ timebuf,
+ -(wrk->hb.nbeats));
+ kill(wrk->pid, SIGTERM);
+ }
+ }
+ }
+ }
+ else if (wrk->hb.nbeats < 0) {
+ rspamd_localtime(wrk->hb.last_event, &tm);
+ r = strftime(timebuf, sizeof(timebuf), "%F %H:%M:%S", &tm);
+ rspamd_snprintf(usec_buf, sizeof(usec_buf), "%.5f",
+ wrk->hb.last_event - (gdouble) (time_t) wrk->hb.last_event);
+ rspamd_snprintf(timebuf + r, sizeof(timebuf) - r,
+ "%s", usec_buf + 1);
+
+ cmd.type = RSPAMD_CONTROL_CHILD_CHANGE;
+ cmd.cmd.child_change.what = rspamd_child_online;
+ cmd.cmd.child_change.pid = wrk->pid;
+ rspamd_control_broadcast_srv_cmd(rspamd_main, &cmd, wrk->pid);
+ msg_info_main("received heartbeat from worker type %s with pid %P, "
+ "last beat on: %s (%L beats lost previously)",
+ g_quark_to_string(wrk->type), wrk->pid,
+ timebuf,
+ -(wrk->hb.nbeats));
+ wrk->hb.nbeats = 1;
+ /* TODO: send notify about worker restoration */
+ }
+}
+
+static void
+rspamd_main_heartbeat_start(struct rspamd_worker *wrk, struct ev_loop *event_loop)
+{
+ wrk->hb.heartbeat_ev.data = (void *) wrk;
+ ev_timer_init(&wrk->hb.heartbeat_ev, rspamd_main_heartbeat_cb,
+ 0.0, wrk->srv->cfg->heartbeat_interval * 2);
+ ev_timer_start(event_loop, &wrk->hb.heartbeat_ev);
+}
+
+static bool
+rspamd_maybe_reuseport_socket(struct rspamd_worker_listen_socket *ls)
+{
+ if (ls->is_systemd) {
+ /* No need to reuseport */
+ return true;
+ }
+
+ if (ls->fd != -1 && rspamd_inet_address_get_af(ls->addr) == AF_UNIX) {
+ /* Just try listen */
+
+ if (listen(ls->fd, -1) == -1) {
+ return false;
+ }
+
+ return true;
+ }
+
+#if defined(SO_REUSEPORT) && defined(SO_REUSEADDR) && defined(LINUX)
+ gint nfd = -1;
+
+ if (ls->type == RSPAMD_WORKER_SOCKET_UDP) {
+ nfd = rspamd_inet_address_listen(ls->addr,
+ (ls->type == RSPAMD_WORKER_SOCKET_UDP ? SOCK_DGRAM : SOCK_STREAM),
+ RSPAMD_INET_ADDRESS_LISTEN_ASYNC | RSPAMD_INET_ADDRESS_LISTEN_REUSEPORT,
+ -1);
+
+ if (nfd == -1) {
+ msg_warn("cannot create reuseport listen socket for %d: %s",
+ ls->fd, strerror(errno));
+ nfd = ls->fd;
+ }
+ else {
+ if (ls->fd != -1) {
+ close(ls->fd);
+ }
+ ls->fd = nfd;
+ nfd = -1;
+ }
+ }
+ else {
+ /*
+ * Reuseport is broken with the current architecture, so it is easier not
+ * to use it at all
+ */
+ nfd = ls->fd;
+ }
+#endif
+
+#if 0
+ /* This needed merely if we have reuseport for tcp, but for now it is disabled */
+ /* This means that we have an fd with no listening enabled */
+ if (nfd != -1) {
+ if (ls->type == RSPAMD_WORKER_SOCKET_TCP) {
+ if (listen (nfd, -1) == -1) {
+ return false;
+ }
+ }
+ }
+#endif
+
+ return true;
+}
+
+/**
+ * Handles worker after fork returned zero
+ * @param wrk
+ * @param rspamd_main
+ * @param cf
+ * @param listen_sockets
+ */
+static void __attribute__((noreturn))
+rspamd_handle_child_fork(struct rspamd_worker *wrk,
+ struct rspamd_main *rspamd_main,
+ struct rspamd_worker_conf *cf,
+ GHashTable *listen_sockets)
+{
+ gint rc;
+ struct rlimit rlim;
+
+ /* Update pid for logging */
+ rspamd_log_on_fork(cf->type, rspamd_main->cfg, rspamd_main->logger);
+ wrk->pid = getpid();
+
+ /* Init PRNG after fork */
+ rc = ottery_init(rspamd_main->cfg->libs_ctx->ottery_cfg);
+ if (rc != OTTERY_ERR_NONE) {
+ msg_err_main("cannot initialize PRNG: %d", rc);
+ abort();
+ }
+
+ rspamd_random_seed_fast();
+#ifdef HAVE_EVUTIL_RNG_INIT
+ evutil_secure_rng_init();
+#endif
+
+ /*
+ * Libev stores all signals in a global table, so
+ * previous handlers must be explicitly detached and forgotten
+ * before starting a new loop
+ */
+ ev_signal_stop(rspamd_main->event_loop, &rspamd_main->int_ev);
+ ev_signal_stop(rspamd_main->event_loop, &rspamd_main->term_ev);
+ ev_signal_stop(rspamd_main->event_loop, &rspamd_main->hup_ev);
+ ev_signal_stop(rspamd_main->event_loop, &rspamd_main->usr1_ev);
+ /* Remove the inherited event base */
+ ev_loop_destroy(rspamd_main->event_loop);
+ rspamd_main->event_loop = NULL;
+
+ /* Close unused sockets */
+ GHashTableIter it;
+ gpointer k, v;
+
+
+ g_hash_table_iter_init(&it, listen_sockets);
+
+ /*
+ * Close listen sockets of not our process (inherited from other forks)
+ */
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ GList *elt = (GList *) v;
+ GList *our = cf->listen_socks;
+
+ if (g_list_position(our, elt) == -1) {
+ GList *cur = elt;
+
+ while (cur) {
+ struct rspamd_worker_listen_socket *ls =
+ (struct rspamd_worker_listen_socket *) cur->data;
+
+ if (ls->fd != -1 && close(ls->fd) == -1) {
+ msg_err("cannot close fd %d (addr = %s): %s",
+ ls->fd,
+ rspamd_inet_address_to_string_pretty(ls->addr),
+ strerror(errno));
+ }
+
+ ls->fd = -1;
+
+ cur = g_list_next(cur);
+ }
+ }
+ }
+
+ /* Reuseport before dropping privs */
+ GList *cur = cf->listen_socks;
+
+ while (cur) {
+ struct rspamd_worker_listen_socket *ls =
+ (struct rspamd_worker_listen_socket *) cur->data;
+
+ if (!rspamd_maybe_reuseport_socket(ls)) {
+ msg_err("cannot listen on socket %s: %s",
+ rspamd_inet_address_to_string_pretty(ls->addr),
+ strerror(errno));
+ }
+
+ cur = g_list_next(cur);
+ }
+
+ /* Drop privileges */
+ rspamd_worker_drop_priv(rspamd_main);
+ /* Set limits */
+ rspamd_worker_set_limits(rspamd_main, cf);
+ /* Re-set stack limit */
+ getrlimit(RLIMIT_STACK, &rlim);
+ rlim.rlim_cur = 100 * 1024 * 1024;
+ rlim.rlim_max = rlim.rlim_cur;
+ setrlimit(RLIMIT_STACK, &rlim);
+
+ if (cf->bind_conf) {
+ rspamd_setproctitle("%s process (%s)", cf->worker->name,
+ cf->bind_conf->bind_line);
+ }
+ else {
+ rspamd_setproctitle("%s process", cf->worker->name);
+ }
+
+ if (rspamd_main->pfh) {
+ rspamd_pidfile_close(rspamd_main->pfh);
+ }
+
+ if (rspamd_main->cfg->log_silent_workers) {
+ rspamd_log_set_log_level(rspamd_main->logger, G_LOG_LEVEL_MESSAGE);
+ }
+
+ wrk->start_time = rspamd_get_calendar_ticks();
+
+ if (cf->bind_conf) {
+ GString *listen_conf_stringified = g_string_new(NULL);
+ struct rspamd_worker_bind_conf *cur_conf;
+
+ LL_FOREACH(cf->bind_conf, cur_conf)
+ {
+ if (cur_conf->next) {
+ rspamd_printf_gstring(listen_conf_stringified, "%s, ",
+ cur_conf->bind_line);
+ }
+ else {
+ rspamd_printf_gstring(listen_conf_stringified, "%s",
+ cur_conf->bind_line);
+ }
+ }
+
+ msg_info_main("starting %s process %P (%d); listen on: %v",
+ cf->worker->name,
+ getpid(), wrk->index, listen_conf_stringified);
+ g_string_free(listen_conf_stringified, TRUE);
+ }
+ else {
+ msg_info_main("starting %s process %P (%d); no listen",
+ cf->worker->name,
+ getpid(), wrk->index);
+ }
+ /* Close parent part of socketpair */
+ close(wrk->control_pipe[0]);
+ close(wrk->srv_pipe[0]);
+ /*
+ * Read comments in `rspamd_handle_main_fork` for details why these channel
+ * is blocking.
+ */
+ rspamd_socket_nonblocking(wrk->control_pipe[1]);
+#if 0
+ rspamd_socket_nonblocking (wrk->srv_pipe[1]);
+#endif
+ rspamd_main->cfg->cur_worker = wrk;
+ /* Execute worker (this function should not return normally!) */
+ cf->worker->worker_start_func(wrk);
+ /* To distinguish from normal termination */
+ exit(EXIT_FAILURE);
+}
+
+static void
+rspamd_handle_main_fork(struct rspamd_worker *wrk,
+ struct rspamd_main *rspamd_main,
+ struct rspamd_worker_conf *cf,
+ struct ev_loop *ev_base)
+{
+ /* Close worker part of socketpair */
+ close(wrk->control_pipe[1]);
+ close(wrk->srv_pipe[1]);
+
+ /*
+ * There are no reasons why control pipes are blocking: the messages
+ * there are rare and are strictly bounded by command sizes, so if we block
+ * on some pipe, it is ok, as we still poll that for all operations.
+ * It is also impossible to block on writing in normal conditions.
+ * And if the conditions are not normal, e.g. a worker is unresponsive, then
+ * we can safely think that the non-blocking behaviour as it is implemented
+ * currently will not make things better, as it would lead to incomplete
+ * reads/writes that are not handled anyhow and are totally broken from the
+ * beginning.
+ */
+#if 0
+ rspamd_socket_nonblocking (wrk->srv_pipe[0]);
+#endif
+ rspamd_socket_nonblocking(wrk->control_pipe[0]);
+
+ rspamd_srv_start_watching(rspamd_main, wrk, ev_base);
+ /* Child event */
+ wrk->cld_ev.data = wrk;
+ ev_child_init(&wrk->cld_ev, rspamd_worker_on_term, wrk->pid, 0);
+ ev_child_start(rspamd_main->event_loop, &wrk->cld_ev);
+ /* Heartbeats */
+ rspamd_main_heartbeat_start(wrk, rspamd_main->event_loop);
+ /* Insert worker into worker's table, pid is index */
+ g_hash_table_insert(rspamd_main->workers,
+ GSIZE_TO_POINTER(wrk->pid), wrk);
+
+#if defined(SO_REUSEPORT) && defined(SO_REUSEADDR) && defined(LINUX)
+ /*
+ * Close listen sockets in the main process once a child is handling them,
+ * if we have reuseport
+ */
+ GList *cur = cf->listen_socks;
+
+ while (cur) {
+ struct rspamd_worker_listen_socket *ls =
+ (struct rspamd_worker_listen_socket *) cur->data;
+
+ if (ls->fd != -1 && ls->type == RSPAMD_WORKER_SOCKET_UDP) {
+ close(ls->fd);
+ ls->fd = -1;
+ }
+
+ cur = g_list_next(cur);
+ }
+#endif
+}
+
+#ifndef SOCK_SEQPACKET
+#define SOCK_SEQPACKET SOCK_DGRAM
+#endif
+struct rspamd_worker *
+rspamd_fork_worker(struct rspamd_main *rspamd_main,
+ struct rspamd_worker_conf *cf,
+ guint index,
+ struct ev_loop *ev_base,
+ rspamd_worker_term_cb term_handler,
+ GHashTable *listen_sockets)
+{
+ struct rspamd_worker *wrk;
+
+ /* Starting worker process */
+ wrk = (struct rspamd_worker *) g_malloc0(sizeof(struct rspamd_worker));
+
+ if (!rspamd_socketpair(wrk->control_pipe, SOCK_SEQPACKET)) {
+ msg_err("socketpair failure: %s", strerror(errno));
+ rspamd_hard_terminate(rspamd_main);
+ }
+
+ if (!rspamd_socketpair(wrk->srv_pipe, SOCK_SEQPACKET)) {
+ msg_err("socketpair failure: %s", strerror(errno));
+ rspamd_hard_terminate(rspamd_main);
+ }
+
+ if (cf->bind_conf) {
+ msg_info_main("prepare to fork process %s (%d); listen on: %s",
+ cf->worker->name,
+ index, cf->bind_conf->name);
+ }
+ else {
+ msg_info_main("prepare to fork process %s (%d), no bind socket",
+ cf->worker->name,
+ index);
+ }
+
+ wrk->srv = rspamd_main;
+ wrk->type = cf->type;
+ wrk->cf = cf;
+ wrk->flags = cf->worker->flags;
+ REF_RETAIN(cf);
+ wrk->index = index;
+ wrk->ctx = cf->ctx;
+ wrk->ppid = getpid();
+ wrk->pid = fork();
+ wrk->cores_throttled = rspamd_main->cores_throttling;
+ wrk->term_handler = term_handler;
+ wrk->control_events_pending = g_hash_table_new_full(g_direct_hash, g_direct_equal,
+ NULL, rspamd_pending_control_free);
+
+ switch (wrk->pid) {
+ case 0:
+ rspamd_current_worker = wrk;
+ rspamd_handle_child_fork(wrk, rspamd_main, cf, listen_sockets);
+ break;
+ case -1:
+ msg_err_main("cannot fork main process: %s", strerror(errno));
+
+ if (rspamd_main->pfh) {
+ rspamd_pidfile_remove(rspamd_main->pfh);
+ }
+
+ rspamd_hard_terminate(rspamd_main);
+ break;
+ default:
+ rspamd_handle_main_fork(wrk, rspamd_main, cf, ev_base);
+ break;
+ }
+
+ return wrk;
+}
+
+void rspamd_worker_block_signals(void)
+{
+ sigset_t set;
+
+ sigemptyset(&set);
+ sigaddset(&set, SIGTERM);
+ sigaddset(&set, SIGINT);
+ sigaddset(&set, SIGHUP);
+ sigaddset(&set, SIGUSR1);
+ sigaddset(&set, SIGUSR2);
+ sigprocmask(SIG_BLOCK, &set, NULL);
+}
+
+void rspamd_worker_unblock_signals(void)
+{
+ sigset_t set;
+
+ sigemptyset(&set);
+ sigaddset(&set, SIGTERM);
+ sigaddset(&set, SIGINT);
+ sigaddset(&set, SIGHUP);
+ sigaddset(&set, SIGUSR1);
+ sigaddset(&set, SIGUSR2);
+ sigprocmask(SIG_UNBLOCK, &set, NULL);
+}
+
+void rspamd_hard_terminate(struct rspamd_main *rspamd_main)
+{
+ GHashTableIter it;
+ gpointer k, v;
+ struct rspamd_worker *w;
+ sigset_t set;
+
+ /* Block all signals */
+ sigemptyset(&set);
+ sigaddset(&set, SIGTERM);
+ sigaddset(&set, SIGINT);
+ sigaddset(&set, SIGHUP);
+ sigaddset(&set, SIGUSR1);
+ sigaddset(&set, SIGUSR2);
+ sigaddset(&set, SIGCHLD);
+ sigprocmask(SIG_BLOCK, &set, NULL);
+
+ /* We need to terminate all workers that might be already spawned */
+ rspamd_worker_block_signals();
+ g_hash_table_iter_init(&it, rspamd_main->workers);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ w = v;
+ msg_err_main("kill worker %P as Rspamd is terminating due to "
+ "an unrecoverable error",
+ w->pid);
+ kill(w->pid, SIGKILL);
+ }
+
+ msg_err_main("shutting down Rspamd due to fatal error");
+
+ rspamd_log_close(rspamd_main->logger);
+ exit(EXIT_FAILURE);
+}
+
+gboolean
+rspamd_worker_is_scanner(struct rspamd_worker *w)
+{
+
+ if (w) {
+ return !!(w->flags & RSPAMD_WORKER_SCANNER);
+ }
+
+ return FALSE;
+}
+
+gboolean
+rspamd_worker_is_primary_controller(struct rspamd_worker *w)
+{
+
+ if (w) {
+ return !!(w->flags & RSPAMD_WORKER_CONTROLLER) && w->index == 0;
+ }
+
+ return FALSE;
+}
+
+gboolean
+rspamd_worker_check_controller_presence(struct rspamd_worker *w)
+{
+ if (w->index == 0) {
+ GQuark our_type = w->type;
+ gboolean controller_seen = FALSE;
+ GList *cur;
+
+ enum {
+ low_priority_worker,
+ high_priority_worker
+ } our_priority;
+
+ if (our_type == g_quark_from_static_string("rspamd_proxy")) {
+ our_priority = low_priority_worker;
+ }
+ else if (our_type == g_quark_from_static_string("normal")) {
+ our_priority = high_priority_worker;
+ }
+ else {
+ msg_err("function is called for a wrong worker type: %s", g_quark_to_string(our_type));
+ return FALSE;
+ }
+
+ cur = w->srv->cfg->workers;
+
+ while (cur) {
+ struct rspamd_worker_conf *cf;
+
+ cf = (struct rspamd_worker_conf *) cur->data;
+
+ if (our_priority == low_priority_worker) {
+ if ((cf->type == g_quark_from_static_string("controller")) ||
+ (cf->type == g_quark_from_static_string("normal"))) {
+
+ if (cf->enabled && cf->count >= 0) {
+ controller_seen = TRUE;
+ break;
+ }
+ }
+ }
+ else {
+ if (cf->type == g_quark_from_static_string("controller")) {
+ if (cf->enabled && cf->count >= 0) {
+ controller_seen = TRUE;
+ break;
+ }
+ }
+ }
+
+ cur = g_list_next(cur);
+ }
+
+ if (!controller_seen) {
+ msg_info("no controller or normal workers defined, execute "
+ "controller periodics in this worker");
+ w->flags |= RSPAMD_WORKER_CONTROLLER;
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+struct rspamd_worker_session_elt {
+ void *ptr;
+ guint *pref;
+ const gchar *tag;
+ time_t when;
+};
+
+struct rspamd_worker_session_cache {
+ struct ev_loop *ev_base;
+ GHashTable *cache;
+ struct rspamd_config *cfg;
+ struct ev_timer periodic;
+};
+
+static gint
+rspamd_session_cache_sort_cmp(gconstpointer pa, gconstpointer pb)
+{
+ const struct rspamd_worker_session_elt
+ *e1 = *(const struct rspamd_worker_session_elt **) pa,
+ *e2 = *(const struct rspamd_worker_session_elt **) pb;
+
+ return e2->when < e1->when;
+}
+
+static void
+rspamd_sessions_cache_periodic(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_worker_session_cache *c =
+ (struct rspamd_worker_session_cache *) w->data;
+ GHashTableIter it;
+ gchar timebuf[32];
+ gpointer k, v;
+ struct rspamd_worker_session_elt *elt;
+ struct tm tms;
+ GPtrArray *res;
+ guint i;
+
+ if (g_hash_table_size(c->cache) > c->cfg->max_sessions_cache) {
+ res = g_ptr_array_sized_new(g_hash_table_size(c->cache));
+ g_hash_table_iter_init(&it, c->cache);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ g_ptr_array_add(res, v);
+ }
+
+ msg_err("sessions cache is overflowed %d elements where %d is limit",
+ (gint) res->len, (gint) c->cfg->max_sessions_cache);
+ g_ptr_array_sort(res, rspamd_session_cache_sort_cmp);
+
+ PTR_ARRAY_FOREACH(res, i, elt)
+ {
+ rspamd_localtime(elt->when, &tms);
+ strftime(timebuf, sizeof(timebuf), "%F %H:%M:%S", &tms);
+
+ msg_warn("redundant session; ptr: %p, "
+ "tag: %s, refcount: %d, time: %s",
+ elt->ptr, elt->tag ? elt->tag : "unknown",
+ elt->pref ? *elt->pref : 0,
+ timebuf);
+ }
+ }
+
+ ev_timer_again(EV_A_ w);
+}
+
+void *
+rspamd_worker_session_cache_new(struct rspamd_worker *w,
+ struct ev_loop *ev_base)
+{
+ struct rspamd_worker_session_cache *c;
+ static const gdouble periodic_interval = 60.0;
+
+ c = g_malloc0(sizeof(*c));
+ c->ev_base = ev_base;
+ c->cache = g_hash_table_new_full(g_direct_hash, g_direct_equal,
+ NULL, g_free);
+ c->cfg = w->srv->cfg;
+ c->periodic.data = c;
+ ev_timer_init(&c->periodic, rspamd_sessions_cache_periodic, periodic_interval,
+ periodic_interval);
+ ev_timer_start(ev_base, &c->periodic);
+
+ return c;
+}
+
+
+void rspamd_worker_session_cache_add(void *cache, const gchar *tag,
+ guint *pref, void *ptr)
+{
+ struct rspamd_worker_session_cache *c = cache;
+ struct rspamd_worker_session_elt *elt;
+
+ elt = g_malloc0(sizeof(*elt));
+ elt->pref = pref;
+ elt->ptr = ptr;
+ elt->tag = tag;
+ elt->when = time(NULL);
+
+ g_hash_table_insert(c->cache, elt->ptr, elt);
+}
+
+
+void rspamd_worker_session_cache_remove(void *cache, void *ptr)
+{
+ struct rspamd_worker_session_cache *c = cache;
+
+ g_hash_table_remove(c->cache, ptr);
+}
+
+static void
+rspamd_worker_monitored_on_change(struct rspamd_monitored_ctx *ctx,
+ struct rspamd_monitored *m, gboolean alive,
+ void *ud)
+{
+ struct rspamd_worker *worker = ud;
+ struct rspamd_config *cfg = worker->srv->cfg;
+ struct ev_loop *ev_base;
+ guchar tag[RSPAMD_MONITORED_TAG_LEN];
+ static struct rspamd_srv_command srv_cmd;
+
+ rspamd_monitored_get_tag(m, tag);
+ ev_base = rspamd_monitored_ctx_get_ev_base(ctx);
+ memset(&srv_cmd, 0, sizeof(srv_cmd));
+ srv_cmd.type = RSPAMD_SRV_MONITORED_CHANGE;
+ rspamd_strlcpy(srv_cmd.cmd.monitored_change.tag, tag,
+ sizeof(srv_cmd.cmd.monitored_change.tag));
+ srv_cmd.cmd.monitored_change.alive = alive;
+ srv_cmd.cmd.monitored_change.sender = getpid();
+ msg_info_config("broadcast monitored update for %s: %s",
+ srv_cmd.cmd.monitored_change.tag, alive ? "alive" : "dead");
+
+ rspamd_srv_send_command(worker, ev_base, &srv_cmd, -1, NULL, NULL);
+}
+
+void rspamd_worker_init_monitored(struct rspamd_worker *worker,
+ struct ev_loop *ev_base,
+ struct rspamd_dns_resolver *resolver)
+{
+ rspamd_monitored_ctx_config(worker->srv->cfg->monitored_ctx,
+ worker->srv->cfg, ev_base, resolver->r,
+ rspamd_worker_monitored_on_change, worker);
+}
+
+#ifdef HAVE_SA_SIGINFO
+
+static struct rspamd_main *saved_main = NULL;
+static gboolean
+rspamd_crash_propagate(gpointer key, gpointer value, gpointer unused)
+{
+ struct rspamd_worker *w = value;
+
+ /* Kill children softly */
+ kill(w->pid, SIGTERM);
+
+ return TRUE;
+}
+
+#ifdef BACKWARD_ENABLE
+/* See backtrace.cxx */
+extern void rspamd_print_crash(void);
+#endif
+
+static void
+rspamd_crash_sig_handler(int sig, siginfo_t *info, void *ctx)
+{
+ struct sigaction sa;
+ ucontext_t *uap = ctx;
+ pid_t pid;
+
+ pid = getpid();
+ msg_err("caught fatal signal %d(%s), "
+ "pid: %P, trace: ",
+ sig, strsignal(sig), pid);
+ (void) uap;
+#ifdef BACKWARD_ENABLE
+ rspamd_print_crash();
+#endif
+ msg_err("please see Rspamd FAQ to learn how to dump core files and how to "
+ "fill a bug report");
+
+ if (saved_main) {
+ if (pid == saved_main->pid) {
+ /*
+ * Main process has crashed, propagate crash further to trigger
+ * monitoring alerts and mass panic
+ */
+ g_hash_table_foreach_remove(saved_main->workers,
+ rspamd_crash_propagate, NULL);
+ }
+ }
+
+ /*
+ * Invoke signal with the default handler
+ */
+ sigemptyset(&sa.sa_mask);
+ sa.sa_handler = SIG_DFL;
+ sa.sa_flags = 0;
+ sigaction(sig, &sa, NULL);
+ kill(pid, sig);
+}
+#endif
+
+RSPAMD_NO_SANITIZE void
+rspamd_set_crash_handler(struct rspamd_main *rspamd_main)
+{
+#ifdef HAVE_SA_SIGINFO
+ struct sigaction sa;
+
+#ifdef HAVE_SIGALTSTACK
+ void *stack_mem;
+ stack_t ss;
+ memset(&ss, 0, sizeof ss);
+
+ ss.ss_size = MAX(SIGSTKSZ, 8192 * 4);
+ stack_mem = g_malloc0(ss.ss_size);
+ ss.ss_sp = stack_mem;
+ sigaltstack(&ss, NULL);
+#endif
+ saved_main = rspamd_main;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_sigaction = &rspamd_crash_sig_handler;
+ sa.sa_flags = SA_RESTART | SA_SIGINFO | SA_ONSTACK;
+ sigaction(SIGSEGV, &sa, NULL);
+ sigaction(SIGBUS, &sa, NULL);
+ sigaction(SIGABRT, &sa, NULL);
+ sigaction(SIGFPE, &sa, NULL);
+ sigaction(SIGSYS, &sa, NULL);
+#endif
+}
+
+RSPAMD_NO_SANITIZE void rspamd_unset_crash_handler(struct rspamd_main *unused_)
+{
+#ifdef HAVE_SIGALTSTACK
+ int ret;
+ stack_t ss;
+ ret = sigaltstack(NULL, &ss);
+
+ if (ret != -1) {
+ if (ss.ss_size > 0 && ss.ss_sp) {
+ g_free(ss.ss_sp);
+ }
+
+ ss.ss_size = 0;
+ ss.ss_sp = NULL;
+#ifdef SS_DISABLE
+ ss.ss_flags |= SS_DISABLE;
+#endif
+ sigaltstack(&ss, NULL);
+ }
+#endif
+}
+
+static void
+rspamd_enable_accept_event(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_worker_accept_event *ac_ev =
+ (struct rspamd_worker_accept_event *) w->data;
+
+ ev_timer_stop(EV_A_ w);
+ ev_io_start(EV_A_ & ac_ev->accept_ev);
+}
+
+void rspamd_worker_throttle_accept_events(gint sock, void *data)
+{
+ struct rspamd_worker_accept_event *head, *cur;
+ const gdouble throttling = 0.5;
+
+ head = (struct rspamd_worker_accept_event *) data;
+
+ DL_FOREACH(head, cur)
+ {
+
+ ev_io_stop(cur->event_loop, &cur->accept_ev);
+ cur->throttling_ev.data = cur;
+ ev_timer_init(&cur->throttling_ev, rspamd_enable_accept_event,
+ throttling, 0.0);
+ ev_timer_start(cur->event_loop, &cur->throttling_ev);
+ }
+}
+
+gboolean
+rspamd_check_termination_clause(struct rspamd_main *rspamd_main,
+ struct rspamd_worker *wrk,
+ int res)
+{
+ gboolean need_refork = TRUE;
+
+ if (wrk->state != rspamd_worker_state_running || rspamd_main->wanna_die ||
+ (wrk->flags & RSPAMD_WORKER_OLD_CONFIG)) {
+ /* Do not refork workers that are intended to be terminated */
+ need_refork = FALSE;
+ }
+
+ if (WIFEXITED(res) && WEXITSTATUS(res) == 0) {
+ /* Normal worker termination, do not fork one more */
+
+ if (wrk->flags & RSPAMD_WORKER_OLD_CONFIG) {
+ /* Never re-fork old workers */
+ msg_info_main("%s process %P terminated normally",
+ g_quark_to_string(wrk->type),
+ wrk->pid);
+ need_refork = FALSE;
+ }
+ else {
+ if (wrk->hb.nbeats < 0 && rspamd_main->cfg->heartbeats_loss_max > 0 &&
+ -(wrk->hb.nbeats) >= rspamd_main->cfg->heartbeats_loss_max) {
+ msg_info_main("%s process %P terminated normally, but lost %L "
+ "heartbeats, refork it",
+ g_quark_to_string(wrk->type),
+ wrk->pid,
+ -(wrk->hb.nbeats));
+ need_refork = TRUE;
+ }
+ else {
+ msg_info_main("%s process %P terminated normally",
+ g_quark_to_string(wrk->type),
+ wrk->pid);
+ need_refork = FALSE;
+ }
+ }
+ }
+ else {
+ if (WIFSIGNALED(res)) {
+#ifdef WCOREDUMP
+ if (WCOREDUMP(res)) {
+ msg_warn_main(
+ "%s process %P terminated abnormally by signal: %s"
+ " and created core file; please see Rspamd FAQ "
+ "to learn how to extract data from core file and "
+ "fill a bug report",
+ g_quark_to_string(wrk->type),
+ wrk->pid,
+ g_strsignal(WTERMSIG(res)));
+ }
+ else {
+#ifdef HAVE_SYS_RESOURCE_H
+ struct rlimit rlmt;
+ (void) getrlimit(RLIMIT_CORE, &rlmt);
+
+ msg_warn_main(
+ "%s process %P terminated abnormally with exit code %d by "
+ "signal: %s"
+ " but NOT created core file (throttled=%s); "
+ "core file limits: %L current, %L max",
+ g_quark_to_string(wrk->type),
+ wrk->pid,
+ WEXITSTATUS(res),
+ g_strsignal(WTERMSIG(res)),
+ wrk->cores_throttled ? "yes" : "no",
+ (gint64) rlmt.rlim_cur,
+ (gint64) rlmt.rlim_max);
+#else
+ msg_warn_main(
+ "%s process %P terminated abnormally with exit code %d by signal: %s"
+ " but NOT created core file (throttled=%s); ",
+ g_quark_to_string(wrk->type),
+ wrk->pid, WEXITSTATUS(res),
+ g_strsignal(WTERMSIG(res)),
+ wrk->cores_throttled ? "yes" : "no");
+#endif
+ }
+#else
+ msg_warn_main(
+ "%s process %P terminated abnormally with exit code %d by signal: %s",
+ g_quark_to_string(wrk->type),
+ wrk->pid, WEXITSTATUS(res),
+ g_strsignal(WTERMSIG(res)));
+#endif
+ if (WTERMSIG(res) == SIGUSR2) {
+ /*
+ * It is actually race condition when not started process
+ * has been requested to be reloaded.
+ *
+ * We shouldn't refork on this
+ */
+ need_refork = FALSE;
+ }
+ }
+ else {
+ msg_warn_main("%s process %P terminated abnormally "
+ "(but it was not killed by a signal) "
+ "with exit code %d",
+ g_quark_to_string(wrk->type),
+ wrk->pid,
+ WEXITSTATUS(res));
+ }
+ }
+
+ return need_refork;
+}
+
+#ifdef WITH_HYPERSCAN
+gboolean
+rspamd_worker_hyperscan_ready(struct rspamd_main *rspamd_main,
+ struct rspamd_worker *worker, gint fd,
+ gint attached_fd,
+ struct rspamd_control_command *cmd,
+ gpointer ud)
+{
+ struct rspamd_control_reply rep;
+ struct rspamd_re_cache *cache = worker->srv->cfg->re_cache;
+
+ memset(&rep, 0, sizeof(rep));
+ rep.type = RSPAMD_CONTROL_HYPERSCAN_LOADED;
+
+ if (rspamd_re_cache_is_hs_loaded(cache) != RSPAMD_HYPERSCAN_LOADED_FULL ||
+ cmd->cmd.hs_loaded.forced) {
+
+ msg_info("loading hyperscan expressions after receiving compilation "
+ "notice: %s",
+ (rspamd_re_cache_is_hs_loaded(cache) != RSPAMD_HYPERSCAN_LOADED_FULL) ? "new db" : "forced update");
+ rep.reply.hs_loaded.status = rspamd_re_cache_load_hyperscan(
+ worker->srv->cfg->re_cache, cmd->cmd.hs_loaded.cache_dir, false);
+ }
+
+ if (write(fd, &rep, sizeof(rep)) != sizeof(rep)) {
+ msg_err("cannot write reply to the control socket: %s",
+ strerror(errno));
+ }
+
+ return TRUE;
+}
+#endif /* With Hyperscan */
+
+gboolean
+rspamd_worker_check_context(gpointer ctx, guint64 magic)
+{
+ struct rspamd_abstract_worker_ctx *actx = (struct rspamd_abstract_worker_ctx *) ctx;
+
+ return actx->magic == magic;
+}
+
+static gboolean
+rspamd_worker_log_pipe_handler(struct rspamd_main *rspamd_main,
+ struct rspamd_worker *worker, gint fd,
+ gint attached_fd,
+ struct rspamd_control_command *cmd,
+ gpointer ud)
+{
+ struct rspamd_config *cfg = ud;
+ struct rspamd_worker_log_pipe *lp;
+ struct rspamd_control_reply rep;
+
+ memset(&rep, 0, sizeof(rep));
+ rep.type = RSPAMD_CONTROL_LOG_PIPE;
+
+ if (attached_fd != -1) {
+ lp = g_malloc0(sizeof(*lp));
+ lp->fd = attached_fd;
+ lp->type = cmd->cmd.log_pipe.type;
+
+ DL_APPEND(cfg->log_pipes, lp);
+ msg_info("added new log pipe");
+ }
+ else {
+ rep.reply.log_pipe.status = ENOENT;
+ msg_err("cannot attach log pipe: invalid fd");
+ }
+
+ if (write(fd, &rep, sizeof(rep)) != sizeof(rep)) {
+ msg_err("cannot write reply to the control socket: %s",
+ strerror(errno));
+ }
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_worker_monitored_handler(struct rspamd_main *rspamd_main,
+ struct rspamd_worker *worker, gint fd,
+ gint attached_fd,
+ struct rspamd_control_command *cmd,
+ gpointer ud)
+{
+ struct rspamd_control_reply rep;
+ struct rspamd_monitored *m;
+ struct rspamd_monitored_ctx *mctx = worker->srv->cfg->monitored_ctx;
+ struct rspamd_config *cfg = ud;
+
+ memset(&rep, 0, sizeof(rep));
+ rep.type = RSPAMD_CONTROL_MONITORED_CHANGE;
+
+ if (cmd->cmd.monitored_change.sender != getpid()) {
+ m = rspamd_monitored_by_tag(mctx, cmd->cmd.monitored_change.tag);
+
+ if (m != NULL) {
+ rspamd_monitored_set_alive(m, cmd->cmd.monitored_change.alive);
+ rep.reply.monitored_change.status = 1;
+ msg_info_config("updated monitored status for %s: %s",
+ cmd->cmd.monitored_change.tag,
+ cmd->cmd.monitored_change.alive ? "alive" : "dead");
+ }
+ else {
+ msg_err("cannot find monitored by tag: %*s", 32,
+ cmd->cmd.monitored_change.tag);
+ rep.reply.monitored_change.status = 0;
+ }
+ }
+
+ if (write(fd, &rep, sizeof(rep)) != sizeof(rep)) {
+ msg_err("cannot write reply to the control socket: %s",
+ strerror(errno));
+ }
+
+ return TRUE;
+}
+
+void rspamd_worker_init_scanner(struct rspamd_worker *worker,
+ struct ev_loop *ev_base,
+ struct rspamd_dns_resolver *resolver,
+ struct rspamd_lang_detector **plang_det)
+{
+ rspamd_stat_init(worker->srv->cfg, ev_base);
+#ifdef WITH_HYPERSCAN
+ rspamd_control_worker_add_cmd_handler(worker,
+ RSPAMD_CONTROL_HYPERSCAN_LOADED,
+ rspamd_worker_hyperscan_ready,
+ NULL);
+#endif
+ rspamd_control_worker_add_cmd_handler(worker,
+ RSPAMD_CONTROL_LOG_PIPE,
+ rspamd_worker_log_pipe_handler,
+ worker->srv->cfg);
+ rspamd_control_worker_add_cmd_handler(worker,
+ RSPAMD_CONTROL_MONITORED_CHANGE,
+ rspamd_worker_monitored_handler,
+ worker->srv->cfg);
+
+ *plang_det = worker->srv->cfg->lang_det;
+}
+
+void rspamd_controller_store_saved_stats(struct rspamd_main *rspamd_main,
+ struct rspamd_config *cfg)
+{
+ struct rspamd_stat *stat;
+ ucl_object_t *top, *sub;
+ struct ucl_emitter_functions *efuncs;
+ gint i, fd;
+ FILE *fp;
+ gchar fpath[PATH_MAX];
+
+ if (cfg->stats_file == NULL) {
+ return;
+ }
+
+ rspamd_snprintf(fpath, sizeof(fpath), "%s.XXXXXXXX", cfg->stats_file);
+ fd = g_mkstemp_full(fpath, O_WRONLY | O_TRUNC, 00644);
+
+ if (fd == -1) {
+ msg_err_config("cannot open for writing controller stats from %s: %s",
+ fpath, strerror(errno));
+ return;
+ }
+
+ fp = fdopen(fd, "w");
+ stat = rspamd_main->stat;
+
+ top = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(top, ucl_object_fromint(stat->messages_scanned), "scanned", 0, false);
+ ucl_object_insert_key(top, ucl_object_fromint(stat->messages_learned), "learned", 0, false);
+
+ if (stat->messages_scanned > 0) {
+ sub = ucl_object_typed_new(UCL_OBJECT);
+ for (i = METRIC_ACTION_REJECT; i <= METRIC_ACTION_NOACTION; i++) {
+ ucl_object_insert_key(sub,
+ ucl_object_fromint(stat->actions_stat[i]),
+ rspamd_action_to_str(i), 0, false);
+ }
+ ucl_object_insert_key(top, sub, "actions", 0, false);
+ }
+
+ ucl_object_insert_key(top,
+ ucl_object_fromint(stat->connections_count),
+ "connections", 0, false);
+ ucl_object_insert_key(top,
+ ucl_object_fromint(stat->control_connections_count),
+ "control_connections", 0, false);
+
+ efuncs = ucl_object_emit_file_funcs(fp);
+ if (!ucl_object_emit_full(top, UCL_EMIT_JSON_COMPACT,
+ efuncs, NULL)) {
+ msg_err_config("cannot write stats to %s: %s",
+ fpath, strerror(errno));
+
+ unlink(fpath);
+ }
+ else {
+ if (rename(fpath, cfg->stats_file) == -1) {
+ msg_err_config("cannot rename stats from %s to %s: %s",
+ fpath, cfg->stats_file, strerror(errno));
+ }
+ }
+
+ ucl_object_unref(top);
+ fclose(fp);
+ ucl_object_emit_funcs_free(efuncs);
+}
+
+static ev_timer rrd_timer;
+
+void rspamd_controller_on_terminate(struct rspamd_worker *worker,
+ struct rspamd_rrd_file *rrd)
+{
+ struct rspamd_abstract_worker_ctx *ctx;
+
+ ctx = (struct rspamd_abstract_worker_ctx *) worker->ctx;
+ rspamd_controller_store_saved_stats(worker->srv, worker->srv->cfg);
+
+ if (rrd) {
+ ev_timer_stop(ctx->event_loop, &rrd_timer);
+ msg_info("closing rrd file: %s", rrd->filename);
+ rspamd_rrd_close(rrd);
+ }
+}
+
+static void
+rspamd_controller_load_saved_stats(struct rspamd_main *rspamd_main,
+ struct rspamd_config *cfg)
+{
+ struct ucl_parser *parser;
+ ucl_object_t *obj;
+ const ucl_object_t *elt, *subelt;
+ struct rspamd_stat *stat, stat_copy;
+ gint i;
+
+ if (cfg->stats_file == NULL) {
+ return;
+ }
+
+ if (access(cfg->stats_file, R_OK) == -1) {
+ msg_err_config("cannot load controller stats from %s: %s",
+ cfg->stats_file, strerror(errno));
+ return;
+ }
+
+ parser = ucl_parser_new(0);
+
+ if (!ucl_parser_add_file(parser, cfg->stats_file)) {
+ msg_err_config("cannot parse controller stats from %s: %s",
+ cfg->stats_file, ucl_parser_get_error(parser));
+ ucl_parser_free(parser);
+
+ return;
+ }
+
+ obj = ucl_parser_get_object(parser);
+ ucl_parser_free(parser);
+
+ stat = rspamd_main->stat;
+ memcpy(&stat_copy, stat, sizeof(stat_copy));
+
+ elt = ucl_object_lookup(obj, "scanned");
+
+ if (elt != NULL && ucl_object_type(elt) == UCL_INT) {
+ stat_copy.messages_scanned = ucl_object_toint(elt);
+ }
+
+ elt = ucl_object_lookup(obj, "learned");
+
+ if (elt != NULL && ucl_object_type(elt) == UCL_INT) {
+ stat_copy.messages_learned = ucl_object_toint(elt);
+ }
+
+ elt = ucl_object_lookup(obj, "actions");
+
+ if (elt != NULL) {
+ for (i = METRIC_ACTION_REJECT; i <= METRIC_ACTION_NOACTION; i++) {
+ subelt = ucl_object_lookup(elt, rspamd_action_to_str(i));
+
+ if (subelt && ucl_object_type(subelt) == UCL_INT) {
+ stat_copy.actions_stat[i] = ucl_object_toint(subelt);
+ }
+ }
+ }
+
+ elt = ucl_object_lookup(obj, "connections_count");
+
+ if (elt != NULL && ucl_object_type(elt) == UCL_INT) {
+ stat_copy.connections_count = ucl_object_toint(elt);
+ }
+
+ elt = ucl_object_lookup(obj, "control_connections_count");
+
+ if (elt != NULL && ucl_object_type(elt) == UCL_INT) {
+ stat_copy.control_connections_count = ucl_object_toint(elt);
+ }
+
+ ucl_object_unref(obj);
+ memcpy(stat, &stat_copy, sizeof(stat_copy));
+}
+
+struct rspamd_controller_periodics_cbdata {
+ struct rspamd_worker *worker;
+ struct rspamd_rrd_file *rrd;
+ struct rspamd_stat *stat;
+ ev_timer save_stats_event;
+};
+
+static void
+rspamd_controller_rrd_update(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_controller_periodics_cbdata *cbd =
+ (struct rspamd_controller_periodics_cbdata *) w->data;
+ struct rspamd_stat *stat;
+ GArray ar;
+ gdouble points[METRIC_ACTION_MAX];
+ GError *err = NULL;
+ guint i;
+
+ g_assert(cbd->rrd != NULL);
+ stat = cbd->stat;
+
+ for (i = METRIC_ACTION_REJECT; i < METRIC_ACTION_MAX; i++) {
+ points[i] = stat->actions_stat[i];
+ }
+
+ ar.data = (gchar *) points;
+ ar.len = sizeof(points);
+
+ if (!rspamd_rrd_add_record(cbd->rrd, &ar, rspamd_get_calendar_ticks(),
+ &err)) {
+ msg_err("cannot update rrd file: %e", err);
+ g_error_free(err);
+ }
+
+ /* Plan new event */
+ ev_timer_again(EV_A_ w);
+}
+
+static void
+rspamd_controller_stats_save_periodic(EV_P_ ev_timer *w, int revents)
+{
+ struct rspamd_controller_periodics_cbdata *cbd =
+ (struct rspamd_controller_periodics_cbdata *) w->data;
+
+ rspamd_controller_store_saved_stats(cbd->worker->srv, cbd->worker->srv->cfg);
+ ev_timer_again(EV_A_ w);
+}
+
+void rspamd_worker_init_controller(struct rspamd_worker *worker,
+ struct rspamd_rrd_file **prrd)
+{
+ struct rspamd_abstract_worker_ctx *ctx;
+ static const ev_tstamp rrd_update_time = 1.0;
+
+ ctx = (struct rspamd_abstract_worker_ctx *) worker->ctx;
+ rspamd_controller_load_saved_stats(worker->srv, worker->srv->cfg);
+
+ if (worker->index == 0) {
+ /* Enable periodics and other stuff */
+ static struct rspamd_controller_periodics_cbdata cbd;
+ const ev_tstamp save_stats_interval = 60; /* 1 minute */
+
+ memset(&cbd, 0, sizeof(cbd));
+ cbd.save_stats_event.data = &cbd;
+ cbd.worker = worker;
+ cbd.stat = worker->srv->stat;
+
+ ev_timer_init(&cbd.save_stats_event,
+ rspamd_controller_stats_save_periodic,
+ save_stats_interval, save_stats_interval);
+ ev_timer_start(ctx->event_loop, &cbd.save_stats_event);
+
+ rspamd_map_watch(worker->srv->cfg, ctx->event_loop,
+ ctx->resolver, worker,
+ RSPAMD_MAP_WATCH_PRIMARY_CONTROLLER);
+
+ if (prrd != NULL) {
+ if (ctx->cfg->rrd_file && worker->index == 0) {
+ GError *rrd_err = NULL;
+
+ *prrd = rspamd_rrd_file_default(ctx->cfg->rrd_file, &rrd_err);
+
+ if (*prrd) {
+ cbd.rrd = *prrd;
+ rrd_timer.data = &cbd;
+ ev_timer_init(&rrd_timer, rspamd_controller_rrd_update,
+ rrd_update_time, rrd_update_time);
+ ev_timer_start(ctx->event_loop, &rrd_timer);
+ }
+ else if (rrd_err) {
+ msg_err("cannot load rrd from %s: %e", ctx->cfg->rrd_file,
+ rrd_err);
+ g_error_free(rrd_err);
+ }
+ else {
+ msg_err("cannot load rrd from %s: unknown error",
+ ctx->cfg->rrd_file);
+ }
+ }
+ else {
+ *prrd = NULL;
+ }
+ }
+
+ if (!ctx->cfg->disable_monitored) {
+ rspamd_worker_init_monitored(worker,
+ ctx->event_loop, ctx->resolver);
+ }
+ }
+ else {
+ rspamd_map_watch(worker->srv->cfg, ctx->event_loop,
+ ctx->resolver, worker, RSPAMD_MAP_WATCH_SCANNER);
+ }
+}
+
+gdouble
+rspamd_worker_check_and_adjust_timeout(struct rspamd_config *cfg, gdouble timeout)
+{
+ if (isnan(timeout)) {
+ /* Use implicit timeout from cfg->task_timeout */
+ timeout = cfg->task_timeout;
+ }
+
+ if (isnan(timeout)) {
+ return timeout;
+ }
+
+ struct rspamd_symcache_timeout_result *tres = rspamd_symcache_get_max_timeout(cfg->cache);
+ g_assert(tres != 0);
+
+ if (tres->max_timeout > timeout) {
+ msg_info_config("configured task_timeout %.2f is less than maximum symbols cache timeout %.2f; "
+ "some symbols can be terminated before checks",
+ timeout, tres->max_timeout);
+ GString *buf = g_string_sized_new(512);
+ static const int max_displayed_items = 12;
+
+ for (int i = 0; i < MIN(tres->nitems, max_displayed_items); i++) {
+ if (i == 0) {
+ rspamd_printf_gstring(buf, "%s(%.2f)",
+ rspamd_symcache_item_name((struct rspamd_symcache_item *) tres->items[i].item),
+ tres->items[i].timeout);
+ }
+ else {
+ rspamd_printf_gstring(buf, "; %s(%.2f)",
+ rspamd_symcache_item_name((struct rspamd_symcache_item *) tres->items[i].item),
+ tres->items[i].timeout);
+ }
+ }
+ msg_info_config("list of top %d symbols by execution time: %v",
+ (int) MIN(tres->nitems, max_displayed_items),
+ buf);
+
+ g_string_free(buf, TRUE);
+ }
+
+ rspamd_symcache_timeout_result_free(tres);
+
+ /* TODO: maybe adjust timeout */
+ return timeout;
+}
diff --git a/src/libserver/worker_util.h b/src/libserver/worker_util.h
new file mode 100644
index 0000000..ef48188
--- /dev/null
+++ b/src/libserver/worker_util.h
@@ -0,0 +1,334 @@
+/*-
+ * 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 WORKER_UTIL_H_
+#define WORKER_UTIL_H_
+
+#include "config.h"
+#include "util.h"
+#include "libserver/http/http_connection.h"
+#include "rspamd.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef HAVE_SA_SIGINFO
+typedef void (*rspamd_sig_handler_t)(gint);
+#else
+
+typedef void (*rspamd_sig_handler_t)(gint, siginfo_t *, void *);
+
+#endif
+
+struct rspamd_worker;
+struct rspamd_worker_signal_handler;
+
+extern struct rspamd_worker *rspamd_current_worker;
+
+/**
+ * Init basic signals for a worker
+ * @param worker
+ * @param event_loop
+ */
+void rspamd_worker_init_signals(struct rspamd_worker *worker, struct ev_loop *event_loop);
+
+typedef void (*rspamd_accept_handler)(struct ev_loop *loop, ev_io *w, int revents);
+
+/**
+ * Prepare worker's startup
+ * @param worker worker structure
+ * @param name name of the worker
+ * @param sig_handler handler of main signals
+ * @param accept_handler handler of accept event for listen sockets
+ * @return event base suitable for a worker
+ */
+struct ev_loop *
+rspamd_prepare_worker(struct rspamd_worker *worker, const char *name,
+ rspamd_accept_handler hdl);
+
+/**
+ * Should be used to validate context for a worker as in assert like invocation
+ * @param ctx
+ * @param magic
+ * @return
+ */
+gboolean rspamd_worker_check_context(gpointer ctx, guint64 magic);
+
+/**
+ * Set special signal handler for a worker
+ */
+void rspamd_worker_set_signal_handler(int signo,
+ struct rspamd_worker *worker,
+ struct ev_loop *event_loop,
+ rspamd_worker_signal_cb_t handler,
+ void *handler_data);
+
+/**
+ * Stop accepting new connections for a worker
+ * @param worker
+ */
+void rspamd_worker_stop_accept(struct rspamd_worker *worker);
+
+typedef gint (*rspamd_controller_func_t)(
+ struct rspamd_http_connection_entry *conn_ent,
+ struct rspamd_http_message *msg,
+ struct module_ctx *ctx);
+
+struct rspamd_custom_controller_command {
+ const gchar *command;
+ struct module_ctx *ctx;
+ gboolean privileged;
+ gboolean require_message;
+ rspamd_controller_func_t handler;
+};
+
+struct rspamd_controller_worker_ctx;
+struct rspamd_lang_detector;
+
+struct rspamd_controller_session {
+ struct rspamd_controller_worker_ctx *ctx;
+ struct rspamd_worker *wrk;
+ rspamd_mempool_t *pool;
+ struct rspamd_task *task;
+ gchar *classifier;
+ rspamd_inet_addr_t *from_addr;
+ struct rspamd_config *cfg;
+ struct rspamd_lang_detector *lang_det;
+ gboolean is_spam;
+ gboolean is_read_only;
+};
+
+/**
+ * Send error using HTTP and JSON output
+ * @param entry router entry
+ * @param code error code
+ * @param error_msg error message
+ */
+void rspamd_controller_send_error(struct rspamd_http_connection_entry *entry,
+ gint code, const gchar *error_msg, ...);
+
+/**
+ * Send openmetrics-formatted strings using HTTP
+ * @param entry router entry
+ * @param str rspamd fstring buffer, ownership is transferred
+ */
+void rspamd_controller_send_openmetrics(struct rspamd_http_connection_entry *entry,
+ rspamd_fstring_t *str);
+
+/**
+ * Send a custom string using HTTP
+ * @param entry router entry
+ * @param str string to send
+ */
+void rspamd_controller_send_string(struct rspamd_http_connection_entry *entry,
+ const gchar *str);
+
+/**
+ * Send UCL using HTTP and JSON serialization
+ * @param entry router entry
+ * @param obj object to send
+ */
+void rspamd_controller_send_ucl(struct rspamd_http_connection_entry *entry,
+ ucl_object_t *obj);
+
+/**
+ * Return worker's control structure by its type
+ * @param type
+ * @return worker's control structure or NULL
+ */
+worker_t *rspamd_get_worker_by_type(struct rspamd_config *cfg, GQuark type);
+
+/**
+ * Block signals before terminations
+ */
+void rspamd_worker_block_signals(void);
+
+/**
+ * Unblock signals
+ */
+void rspamd_worker_unblock_signals(void);
+
+/**
+ * Kill rspamd main and all workers
+ * @param rspamd_main
+ */
+void rspamd_hard_terminate(struct rspamd_main *rspamd_main) G_GNUC_NORETURN;
+
+/**
+ * Returns TRUE if a specific worker is a scanner worker
+ * @param w
+ * @return
+ */
+gboolean rspamd_worker_is_scanner(struct rspamd_worker *w);
+
+/**
+ * Checks
+ * @param cfg
+ * @param timeout
+ * @return
+ */
+gdouble rspamd_worker_check_and_adjust_timeout(struct rspamd_config *cfg,
+ gdouble timeout);
+
+/**
+ * Returns TRUE if a specific worker is a primary controller
+ * @param w
+ * @return
+ */
+gboolean rspamd_worker_is_primary_controller(struct rspamd_worker *w);
+
+/**
+ * Returns TRUE if a specific worker should take a role of a controller
+ */
+gboolean rspamd_worker_check_controller_presence(struct rspamd_worker *w);
+
+/**
+ * Creates new session cache
+ * @param w
+ * @return
+ */
+void *rspamd_worker_session_cache_new(struct rspamd_worker *w,
+ struct ev_loop *ev_base);
+
+/**
+ * Adds a new session identified by pointer
+ * @param cache
+ * @param tag
+ * @param pref
+ * @param ptr
+ */
+void rspamd_worker_session_cache_add(void *cache, const gchar *tag,
+ guint *pref, void *ptr);
+
+/**
+ * Removes session from cache
+ * @param cache
+ * @param ptr
+ */
+void rspamd_worker_session_cache_remove(void *cache, void *ptr);
+
+/**
+ * Fork new worker with the specified configuration
+ */
+struct rspamd_worker *rspamd_fork_worker(struct rspamd_main *,
+ struct rspamd_worker_conf *, guint idx,
+ struct ev_loop *ev_base,
+ rspamd_worker_term_cb term_handler,
+ GHashTable *listen_sockets);
+
+/**
+ * Sets crash signals handlers if compiled with libunwind
+ */
+RSPAMD_NO_SANITIZE void rspamd_set_crash_handler(struct rspamd_main *);
+
+/**
+ * Restore memory for crash signals
+ */
+RSPAMD_NO_SANITIZE void rspamd_unset_crash_handler(struct rspamd_main *);
+
+/**
+ * Initialise the main monitoring worker
+ * @param worker
+ * @param ev_base
+ * @param resolver
+ */
+void rspamd_worker_init_monitored(struct rspamd_worker *worker,
+ struct ev_loop *ev_base,
+ struct rspamd_dns_resolver *resolver);
+
+/**
+ * Performs throttling for accept events
+ * @param sock
+ * @param data struct rspamd_worker_accept_event * list
+ */
+void rspamd_worker_throttle_accept_events(gint sock, void *data);
+
+/**
+ * Checks (and logs) the worker's termination status. Returns TRUE if a worker
+ * should be restarted.
+ * @param rspamd_main
+ * @param wrk
+ * @param status waitpid res
+ * @return TRUE if refork is desired
+ */
+gboolean rspamd_check_termination_clause(struct rspamd_main *rspamd_main,
+ struct rspamd_worker *wrk, int status);
+
+/**
+ * Call for final scripts for a worker
+ * @param worker
+ * @return
+ */
+gboolean rspamd_worker_call_finish_handlers(struct rspamd_worker *worker);
+
+struct rspamd_rrd_file;
+/**
+ * Terminate controller worker
+ * @param worker
+ */
+void rspamd_controller_on_terminate(struct rspamd_worker *worker,
+ struct rspamd_rrd_file *rrd);
+
+/**
+ * Inits controller worker
+ * @param worker
+ * @param ev_base
+ * @param prrd
+ */
+void rspamd_worker_init_controller(struct rspamd_worker *worker,
+ struct rspamd_rrd_file **prrd);
+
+/**
+ * Saves stats
+ * @param rspamd_main
+ * @param cfg
+ */
+void rspamd_controller_store_saved_stats(struct rspamd_main *rspamd_main,
+ struct rspamd_config *cfg);
+
+#ifdef WITH_HYPERSCAN
+struct rspamd_control_command;
+
+gboolean rspamd_worker_hyperscan_ready(struct rspamd_main *rspamd_main,
+ struct rspamd_worker *worker, gint fd,
+ gint attached_fd,
+ struct rspamd_control_command *cmd,
+ gpointer ud);
+
+#endif
+
+#define msg_err_main(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
+ rspamd_main->server_pool->tag.tagname, rspamd_main->server_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_warn_main(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \
+ rspamd_main->server_pool->tag.tagname, rspamd_main->server_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_notice_main(...) rspamd_default_log_function(G_LOG_LEVEL_MESSAGE, \
+ rspamd_main->server_pool->tag.tagname, rspamd_main->server_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+#define msg_info_main(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \
+ rspamd_main->server_pool->tag.tagname, rspamd_main->server_pool->tag.uid, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* WORKER_UTIL_H_ */