/* * 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 "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, unsigned int how) { struct rspamd_milter_outbuf *obuf, *obuf_tmp; struct rspamd_milter_private *priv = session->priv; struct rspamd_email_address *cur; unsigned int 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", (int) 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"); char *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) { char *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 char 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) { int 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 int rspamd_milter_http_on_url(http_parser *parser, const char *at, size_t length) { GString *url = (GString *) parser->data; g_string_append_len(url, at, length); return 0; } static void rspamd_milter_io_handler(int 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 unsigned char *pos, *end, *zero; unsigned int cmdlen; uint32_t 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(uint16_t) + 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 { unsigned char proto; uint16_t port; char 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 unsigned char *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; int res; k = kh_get(milter_headers_hash_t, priv->headers, (char *) 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(int)); 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; char *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(uint32_t) * 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; char *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(unsigned char 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 unsigned char *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", (int) (p - (const unsigned char *) 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 unsigned char *) 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 unsigned char *) 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 unsigned char *) 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(int 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; int 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 { \ uint32_t _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) = (unsigned char *) (reply)->str + sizeof(_len) + 1; \ } while (0) gboolean rspamd_milter_send_action(struct rspamd_milter_session *session, enum rspamd_milter_reply act, ...) { uint32_t ver, actions, protocol, idx; va_list ap; unsigned char cmd, *pos; rspamd_fstring_t *reply = NULL; gsize len; GString *name, *value; const char *body_str; struct rspamd_milter_outbuf *obuf; struct rspamd_milter_private *priv = session->priv; const rspamd_fstring_t *reason; 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 rspamd_fstring_t *); if (reason != NULL) { len = reason->len; msg_debug_milter("send quarantine action %*s", (int) len, reason->str); SET_COMMAND(cmd, len + 1, reply, pos); memcpy(pos, reason->str, len); pos[len] = '\0'; } else { msg_debug_milter("send quarantine action with no specific string"); SET_COMMAND(cmd, 1, reply, pos); *pos = '\0'; } 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, uint32_t); 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(uint32_t), 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, uint32_t); actions = va_arg(ap, uint32_t); protocol = va_arg(ap, uint32_t); 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(uint32_t) * 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; uint32_t idx = 1; value.str = (char *) ""; 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; } /* * When we get a queue-id we try to pass it to the backend, where possible * We also need that for logging consistency */ IF_MACRO("{i}") { rspamd_http_message_add_header_len(msg, QUEUE_ID_HEADER, found->begin, found->len); rspamd_http_message_add_header_len(msg, LOG_TAG_HEADER, found->begin, found->len); } else { IF_MACRO("i") { rspamd_http_message_add_header_len(msg, QUEUE_ID_HEADER, found->begin, found->len); rspamd_http_message_add_header_len(msg, LOG_TAG_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; unsigned int 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 char *key, int nhdr) { int 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 char *hdr, const ucl_object_t *obj) { GString *hname, *hvalue; struct rspamd_milter_private *priv = session->priv; int 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, (uint32_t) 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, (uint32_t) 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 char *new_body, gsize bodylen) { const ucl_object_t *elt; struct rspamd_milter_private *priv = session->priv; const char *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 char *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) { if (!reply) { if (milter_ctx->quarantine_message == NULL) { reply = rspamd_fstring_new_init( RSPAMD_MILTER_QUARANTINE_MESSAGE, sizeof(RSPAMD_MILTER_QUARANTINE_MESSAGE) - 1); } else { reply = rspamd_fstring_new_init(milter_ctx->quarantine_message, strlen(milter_ctx->quarantine_message)); } rspamd_milter_send_action(session, RSPAMD_MILTER_QUARANTINE, reply); } /* 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) { if (milter_ctx->tempfail_message == NULL) { reply = rspamd_fstring_new_init( RSPAMD_MILTER_TEMPFAIL_MESSAGE, sizeof(RSPAMD_MILTER_TEMPFAIL_MESSAGE) - 1); } else { reply = rspamd_fstring_new_init(milter_ctx->tempfail_message, strlen(milter_ctx->tempfail_message)); } } 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, (uint32_t) 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, (uint32_t) 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: if (!reply) { if (milter_ctx->quarantine_message == NULL) { reply = rspamd_fstring_new_init( RSPAMD_MILTER_QUARANTINE_MESSAGE, sizeof(RSPAMD_MILTER_QUARANTINE_MESSAGE) - 1); } else { reply = rspamd_fstring_new_init(milter_ctx->quarantine_message, strlen(milter_ctx->quarantine_message)); } rspamd_milter_send_action(session, RSPAMD_MILTER_QUARANTINE, reply); } /* 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; }