diff options
author | Vsevolod Stakhov <vsevolod@highsecure.ru> | 2014-04-21 16:25:51 +0100 |
---|---|---|
committer | Vsevolod Stakhov <vsevolod@highsecure.ru> | 2014-04-21 16:25:51 +0100 |
commit | 61555065f3d1c8badcc9573691232f1b6e42988c (patch) | |
tree | 563d5b7cb8c468530f7e79c4da0a75267b1184e1 /src/libserver/spf.c | |
parent | ad5bf825b7f33bc10311673991f0cc888e69c0b1 (diff) | |
download | rspamd-61555065f3d1c8badcc9573691232f1b6e42988c.tar.gz rspamd-61555065f3d1c8badcc9573691232f1b6e42988c.zip |
Rework project structure, remove trash files.
Diffstat (limited to 'src/libserver/spf.c')
-rw-r--r-- | src/libserver/spf.c | 1465 |
1 files changed, 1465 insertions, 0 deletions
diff --git a/src/libserver/spf.c b/src/libserver/spf.c new file mode 100644 index 000000000..12f1513d4 --- /dev/null +++ b/src/libserver/spf.c @@ -0,0 +1,1465 @@ +/* + * Copyright (c) 2009-2012, Vsevolod Stakhov + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "dns.h" +#include "spf.h" +#include "main.h" +#include "message.h" +#include "filter.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_IP6 "ip6" +#define SPF_PTR "ptr" +#define SPF_MX "mx" +#define SPF_EXISTS "exists" +#define SPF_INCLUDE "include" +#define SPF_REDIRECT "redirect" +#define SPF_EXP "exp" + +/** SPF limits for avoiding abuse **/ +#define SPF_MAX_NESTING 10 +#define SPF_MAX_DNS_REQUESTS 30 + +/** + * State machine 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 + +struct spf_dns_cb { + struct spf_record *rec; + struct spf_addr *addr; + spf_action_t cur_action; + gboolean in_include; +}; + +#define CHECK_REC(rec) \ +do { \ + if ((rec)->nested > SPF_MAX_NESTING || \ + (rec)->dns_requests > SPF_MAX_DNS_REQUESTS) { \ + msg_info ("<%s> spf recursion limit %d is reached, domain: %s", \ + (rec)->task->message_id, (rec)->dns_requests, \ + (rec)->sender_domain); \ + return FALSE; \ + } \ +} while (0) \ + +static gboolean parse_spf_record (struct rspamd_task *task, struct spf_record *rec); +static void start_spf_parse (struct spf_record *rec, gchar *begin, guint ttl); + +/* 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; + } +} + +/* Debugging function that dumps spf record in log */ +static void +dump_spf_record (GList *addrs) +{ + struct spf_addr *addr; + GList *cur; + gint r = 0; + gchar logbuf[BUFSIZ], c; +#ifdef HAVE_INET_PTON + gchar ipbuf[INET6_ADDRSTRLEN]; +#else + struct in_addr ina; +#endif + + cur = addrs; + + while (cur) { + addr = cur->data; + if (!addr->is_list) { + switch (addr->mech) { + case SPF_FAIL: + c = '-'; + break; + case SPF_SOFT_FAIL: + case SPF_NEUTRAL: + c = '~'; + break; + case SPF_PASS: + c = '+'; + break; + } +#ifdef HAVE_INET_PTON + if (addr->data.normal.ipv6) { + inet_ntop (AF_INET6, &addr->data.normal.d.in6, ipbuf, sizeof (ipbuf)); + + } + else { + inet_ntop (AF_INET, &addr->data.normal.d.in4, ipbuf, sizeof (ipbuf)); + } + r += snprintf (logbuf + r, sizeof (logbuf) - r, "%c%s/%d; ", c, ipbuf, addr->data.normal.mask); +#else + ina.s_addr = addr->data.normal.d.in4.s_addr; + r += snprintf (logbuf + r, sizeof (logbuf) - r, "%c%s/%d; ", c, inet_ntoa (ina), addr->data.normal.mask); +#endif + } + else { + r += snprintf (logbuf + r, sizeof (logbuf) - r, "%s; ", addr->spf_string); + dump_spf_record (addr->data.list); + } + cur = g_list_next (cur); + } + msg_info ("spf record: %s", logbuf); +} + +/* Find position of address inside addrs list */ +static GList * +spf_addr_find (GList *addrs, gpointer to_find) +{ + struct spf_addr *addr; + GList *cur, *res = NULL; + + cur = addrs; + while (cur) { + addr = cur->data; + if (addr->is_list) { + if ((res = spf_addr_find (addr->data.list, to_find)) != NULL) { + return cur; + } + } + else { + if (cur->data == to_find) { + return cur; + } + } + cur = g_list_next (cur); + } + + return res; +} + +/* + * Destructor for spf record + */ +static void +spf_record_destructor (gpointer r) +{ + struct spf_record *rec = r; + GList *cur; + struct spf_addr *addr; + + if (rec->addrs) { + cur = rec->addrs; + while (cur) { + addr = cur->data; + if (addr->is_list && addr->data.list != NULL) { + g_list_free (addr->data.list); + } + cur = g_list_next (cur); + } + g_list_free (rec->addrs); + } +} + +static gboolean +parse_spf_ipmask (const gchar *begin, struct spf_addr *addr, struct spf_record *rec) +{ + const gchar *pos; + gchar mask_buf[5] = {'\0'}, *p; + gint state = 0, dots = 0; +#ifdef HAVE_INET_PTON + gchar ip_buf[INET6_ADDRSTRLEN]; +#else + gchar ip_buf[INET_ADDRSTRLEN]; +#endif + + bzero (ip_buf, sizeof (ip_buf)); + bzero (mask_buf, sizeof (mask_buf)); + pos = begin; + p = ip_buf; + + while (*pos) { + switch (state) { + case 0: + /* Require ':' */ + if (*pos != ':') { + msg_info ("<%s>: spf error for domain %s: semicolon missing", + rec->task->message_id, rec->sender_domain); + return FALSE; + } + state = 1; + pos ++; + p = ip_buf; + dots = 0; + break; + case 1: +#ifdef HAVE_INET_PTON + if (p - ip_buf >= (gint)sizeof (ip_buf)) { + return FALSE; + } + if (g_ascii_isxdigit (*pos)) { + *p ++ = *pos ++; + } + else if (*pos == '.' || *pos == ':') { + *p ++ = *pos ++; + dots ++; + } +#else + /* Begin parse ip */ + if (p - ip_buf >= (gint)sizeof (ip_buf) || dots > 3) { + return FALSE; + } + if (g_ascii_isdigit (*pos)) { + *p ++ = *pos ++; + } + else if (*pos == '.') { + *p ++ = *pos ++; + dots ++; + } +#endif + else if (*pos == '/') { + pos ++; + p = mask_buf; + state = 2; + } + else { + /* Invalid character */ + msg_info ("<%s>: spf error for domain %s: invalid ip address", + rec->task->message_id, rec->sender_domain); + return FALSE; + } + break; + case 2: + /* Parse mask */ + if (p - mask_buf >= (gint)sizeof (mask_buf)) { + msg_info ("<%s>: spf error for domain %s: too long mask", + rec->task->message_id, rec->sender_domain); + return FALSE; + } + if (g_ascii_isdigit (*pos)) { + *p ++ = *pos ++; + } + else { + return FALSE; + } + break; + } + } + +#ifdef HAVE_INET_PTON + if (inet_pton (AF_INET, ip_buf, &addr->data.normal.d.in4) != 1) { + if (inet_pton (AF_INET6, ip_buf, &addr->data.normal.d.in6) == 1) { + addr->data.normal.ipv6 = TRUE; + } + else { + msg_info ("<%s>: spf error for domain %s: invalid ip address", + rec->task->message_id, rec->sender_domain); + return FALSE; + } + } + else { + addr->data.normal.ipv6 = FALSE; + } +#else + if (!inet_aton (ip_buf, &addr->data.normal.d.in4)) { + return FALSE; + } +#endif + if (state == 2) { + /* Also parse mask */ + if (!addr->data.normal.ipv6) { + addr->data.normal.mask = strtoul (mask_buf, NULL, 10); + if (addr->data.normal.mask > 32) { + msg_info ("<%s>: spf error for domain %s: bad ipmask value: '%s'", + rec->task->message_id, rec->sender_domain, begin); + return FALSE; + } + } + else { + addr->data.normal.mask = strtoul (mask_buf, NULL, 10); + if (addr->data.normal.mask > 128) { + msg_info ("<%s>: spf error for domain %s: bad ipmask value: '%s'", + rec->task->message_id, rec->sender_domain, begin); + return FALSE; + } + } + } + else { + addr->data.normal.mask = addr->data.normal.ipv6 ? 128 : 32; + } + addr->data.normal.parsed = TRUE; + return TRUE; + +} + +static gchar * +parse_spf_hostmask (struct rspamd_task *task, const gchar *begin, struct spf_addr *addr, struct spf_record *rec) +{ + gchar *host = NULL, *p, mask_buf[3]; + gint hostlen; + + bzero (mask_buf, sizeof (mask_buf)); + if (*begin == '\0' || *begin == '/') { + /* Assume host as host to resolve from record */ + host = rec->cur_domain; + } + p = strchr (begin, '/'); + if (p != NULL) { + /* Extract mask */ + rspamd_strlcpy (mask_buf, p + 1, sizeof (mask_buf)); + addr->data.normal.mask = strtoul (mask_buf, NULL, 10); + if (addr->data.normal.mask > 32) { + msg_info ("<%s>: spf error for domain %s: too long mask", + rec->task->message_id, rec->sender_domain); + return FALSE; + } + if (host == NULL) { + hostlen = p - begin; + host = rspamd_mempool_alloc (task->task_pool, hostlen); + rspamd_strlcpy (host, begin, hostlen); + } + } + else { + addr->data.normal.mask = 32; + if (host == NULL) { + host = rspamd_mempool_strdup (task->task_pool, begin); + } + } + + return host; +} + +static void +spf_record_dns_callback (struct rdns_reply *reply, gpointer arg) +{ + struct spf_dns_cb *cb = arg; + gchar *begin; + struct rdns_reply_entry *elt_data; + GList *tmp = NULL; + struct rspamd_task *task; + struct spf_addr *new_addr; + + task = cb->rec->task; + + cb->rec->requests_inflight --; + + if (reply->code == RDNS_RC_NOERROR) { + /* Add all logic for all DNS states here */ + LL_FOREACH (reply->entries, elt_data) { + switch (cb->cur_action) { + case SPF_RESOLVE_MX: + if (elt_data->type == RDNS_REQUEST_MX) { + /* Now resolve A record for this MX */ + if (make_dns_request (task->resolver, task->s, task->task_pool, + spf_record_dns_callback, (void *)cb, RDNS_REQUEST_A, elt_data->content.mx.name)) { + task->dns_requests ++; + cb->rec->requests_inflight ++; + } + } + else if (elt_data->type == RDNS_REQUEST_A) { + if (!cb->addr->data.normal.parsed) { + cb->addr->data.normal.d.in4.s_addr = elt_data->content.a.addr.s_addr; + cb->addr->data.normal.mask = 32; + cb->addr->data.normal.parsed = TRUE; + } + else { + /* Insert one more address */ + tmp = spf_addr_find (cb->rec->addrs, cb->addr); + if (tmp) { + new_addr = rspamd_mempool_alloc (task->task_pool, sizeof (struct spf_addr)); + memcpy (new_addr, cb->addr, sizeof (struct spf_addr)); + new_addr->data.normal.d.in4.s_addr = elt_data->content.a.addr.s_addr; + new_addr->data.normal.parsed = TRUE; + cb->rec->addrs = g_list_insert_before (cb->rec->addrs, tmp, new_addr); + } + else { + msg_info ("<%s>: spf error for domain %s: addresses mismatch", + task->message_id, cb->rec->sender_domain); + } + } + + } +#ifdef HAVE_INET_PTON + else if (elt_data->type == RDNS_REQUEST_AAAA) { + if (!cb->addr->data.normal.parsed) { + memcpy (&cb->addr->data.normal.d.in6, &elt_data->content.aaa.addr, sizeof (struct in6_addr)); + cb->addr->data.normal.mask = 32; + cb->addr->data.normal.parsed = TRUE; + cb->addr->data.normal.ipv6 = TRUE; + } + else { + /* Insert one more address */ + tmp = spf_addr_find (cb->rec->addrs, cb->addr); + if (tmp) { + new_addr = rspamd_mempool_alloc (task->task_pool, sizeof (struct spf_addr)); + memcpy (new_addr, cb->addr, sizeof (struct spf_addr)); + memcpy (&new_addr->data.normal.d.in6, &elt_data->content.aaa.addr, sizeof (struct in6_addr)); + new_addr->data.normal.parsed = TRUE; + new_addr->data.normal.ipv6 = TRUE; + cb->rec->addrs = g_list_insert_before (cb->rec->addrs, tmp, new_addr); + } + else { + msg_info ("<%s>: spf error for domain %s: addresses mismatch", + task->message_id, cb->rec->sender_domain); + } + } + + } +#endif + break; + case SPF_RESOLVE_A: + if (elt_data->type == RDNS_REQUEST_A) { + /* XXX: process only one record */ + cb->addr->data.normal.d.in4.s_addr = elt_data->content.a.addr.s_addr; + cb->addr->data.normal.mask = 32; + cb->addr->data.normal.parsed = TRUE; + } +#ifdef HAVE_INET_PTON + else if (elt_data->type == RDNS_REQUEST_AAAA) { + memcpy (&cb->addr->data.normal.d.in6, &elt_data->content.aaa.addr, sizeof (struct in6_addr)); + cb->addr->data.normal.mask = 32; + cb->addr->data.normal.parsed = TRUE; + cb->addr->data.normal.ipv6 = TRUE; + } +#endif + break; +#ifdef HAVE_INET_PTON + case SPF_RESOLVE_AAA: + if (elt_data->type == RDNS_REQUEST_A) { + /* XXX: process only one record */ + cb->addr->data.normal.d.in4.s_addr = elt_data->content.a.addr.s_addr; + cb->addr->data.normal.mask = 32; + cb->addr->data.normal.parsed = TRUE; + } + else if (elt_data->type == RDNS_REQUEST_AAAA) { + memcpy (&cb->addr->data.normal.d.in6, &elt_data->content.aaa.addr, sizeof (struct in6_addr)); + cb->addr->data.normal.mask = 32; + cb->addr->data.normal.parsed = TRUE; + cb->addr->data.normal.ipv6 = TRUE; + } +#endif + break; + case SPF_RESOLVE_PTR: + break; + case SPF_RESOLVE_REDIRECT: + if (elt_data->type == RDNS_REQUEST_TXT) { + begin = elt_data->content.txt.data; + + if (!cb->in_include && cb->rec->addrs) { + g_list_free (cb->rec->addrs); + cb->rec->addrs = NULL; + } + start_spf_parse (cb->rec, begin, elt_data->ttl); + + } + break; + case SPF_RESOLVE_INCLUDE: + if (elt_data->type == RDNS_REQUEST_TXT) { + begin = elt_data->content.txt.data; +#ifdef SPF_DEBUG + msg_info ("before include"); + dump_spf_record (cb->rec->addrs); +#endif + tmp = cb->rec->addrs; + cb->rec->addrs = NULL; + cb->rec->in_include = TRUE; + start_spf_parse (cb->rec, begin, 0); + cb->rec->in_include = FALSE; + +#ifdef SPF_DEBUG + msg_info ("after include"); + dump_spf_record (cb->rec->addrs); +#endif + /* Insert new list */ + cb->addr->is_list = TRUE; + cb->addr->data.list = cb->rec->addrs; + cb->rec->addrs = tmp; + } + break; + case SPF_RESOLVE_EXP: + break; + case SPF_RESOLVE_EXISTS: + if (elt_data->type == RDNS_REQUEST_A) { + /* If specified address resolves, we can accept connection from every IP */ + cb->addr->data.normal.d.in4.s_addr = INADDR_NONE; + cb->addr->data.normal.mask = 0; + } + break; + } + } + } + else if (reply->code == RDNS_RC_NXDOMAIN) { + switch (cb->cur_action) { + case SPF_RESOLVE_MX: + if (rdns_request_has_type (reply->request, RDNS_REQUEST_MX)) { + msg_info ("<%s>: spf error for domain %s: cannot find MX record for %s", + task->message_id, cb->rec->sender_domain, cb->rec->cur_domain); + cb->addr->data.normal.d.in4.s_addr = INADDR_NONE; + cb->addr->data.normal.mask = 32; + } + else { + msg_info ("<%s>: spf error for domain %s: cannot resolve MX record for %s", + task->message_id, cb->rec->sender_domain, cb->rec->cur_domain); + cb->addr->data.normal.d.in4.s_addr = INADDR_NONE; + cb->addr->data.normal.mask = 32; + } + break; + case SPF_RESOLVE_A: + if (rdns_request_has_type (reply->request, RDNS_REQUEST_A)) { + cb->addr->data.normal.d.in4.s_addr = INADDR_NONE; + cb->addr->data.normal.mask = 32; + } + break; +#ifdef HAVE_INET_PTON + case SPF_RESOLVE_AAA: + if (rdns_request_has_type (reply->request, RDNS_REQUEST_AAAA)) { + memset (&cb->addr->data.normal.d.in6, 0xff, sizeof (struct in6_addr)); + cb->addr->data.normal.mask = 32; + } + break; +#endif + case SPF_RESOLVE_PTR: + break; + case SPF_RESOLVE_REDIRECT: + msg_info ("<%s>: spf error for domain %s: cannot resolve TXT record for %s", + task->message_id, cb->rec->sender_domain, cb->rec->cur_domain); + break; + case SPF_RESOLVE_INCLUDE: + msg_info ("<%s>: spf error for domain %s: cannot resolve TXT record for %s", + task->message_id, cb->rec->sender_domain, cb->rec->cur_domain); + break; + case SPF_RESOLVE_EXP: + break; + case SPF_RESOLVE_EXISTS: + cb->addr->data.normal.d.in4.s_addr = INADDR_NONE; + cb->addr->data.normal.mask = 32; + break; + } + } + + if (cb->rec->requests_inflight == 0) { + cb->rec->callback (cb->rec, cb->rec->task); + } +} + +static gboolean +parse_spf_a (struct rspamd_task *task, const gchar *begin, struct spf_record *rec, struct spf_addr *addr) +{ + struct spf_dns_cb *cb; + gchar *host; + + CHECK_REC (rec); + + if (begin == NULL || *begin != ':') { + return FALSE; + } + begin ++; + + host = parse_spf_hostmask (task, begin, addr, rec); + + if (!host) { + 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_A; + cb->in_include = rec->in_include; + if (make_dns_request (task->resolver, task->s, task->task_pool, + spf_record_dns_callback, (void *)cb, RDNS_REQUEST_A, host)) { + task->dns_requests ++; + rec->requests_inflight ++; + return TRUE; + } + + return FALSE; + +} + +static gboolean +parse_spf_ptr (struct rspamd_task *task, const gchar *begin, struct spf_record *rec, struct spf_addr *addr) +{ + CHECK_REC (rec); + + msg_info ("<%s>: spf error for domain %s: ptr elements are not implemented", + rec->task->message_id, rec->sender_domain); + return FALSE; +} + +static gboolean +parse_spf_mx (struct rspamd_task *task, const gchar *begin, struct spf_record *rec, struct spf_addr *addr) +{ + struct spf_dns_cb *cb; + gchar *host; + + CHECK_REC (rec); + + if (begin == NULL) { + return FALSE; + } + if (*begin == ':') { + begin ++; + } + + host = parse_spf_hostmask (task, begin, addr, rec); + + if (!host) { + return FALSE; + } + rec->dns_requests ++; + cb = rspamd_mempool_alloc (task->task_pool, sizeof (struct spf_dns_cb)); + cb->rec = rec; + cb->addr = addr; + memset (&addr->data.normal, 0, sizeof (addr->data.normal)); + cb->cur_action = SPF_RESOLVE_MX; + cb->in_include = rec->in_include; + if (make_dns_request (task->resolver, task->s, task->task_pool, + spf_record_dns_callback, (void *)cb, RDNS_REQUEST_MX, host)) { + task->dns_requests ++; + rec->requests_inflight ++; + + return TRUE; + } + + return FALSE; +} + +static gboolean +parse_spf_all (struct rspamd_task *task, const gchar *begin, struct spf_record *rec, struct spf_addr *addr) +{ + /* All is 0/0 */ + memset (&addr->data.normal.d, 0, sizeof (addr->data.normal.d)); + if (rec->in_include) { + /* Ignore all record in include */ + addr->data.normal.mask = 32; + } + else { + addr->data.normal.mask = 0; + addr->data.normal.addr_any = TRUE; + } + + return TRUE; +} + +static gboolean +parse_spf_ip4 (struct rspamd_task *task, const gchar *begin, struct spf_record *rec, struct spf_addr *addr) +{ + /* ip4:addr[/mask] */ + + CHECK_REC (rec); + return parse_spf_ipmask (begin, addr, rec); +} + +#ifdef HAVE_INET_PTON +static gboolean +parse_spf_ip6 (struct rspamd_task *task, const gchar *begin, struct spf_record *rec, struct spf_addr *addr) +{ + /* ip6:addr[/mask] */ + + CHECK_REC (rec); + return parse_spf_ipmask (begin, addr, rec); +} +#endif + +static gboolean +parse_spf_include (struct rspamd_task *task, const gchar *begin, struct spf_record *rec, struct spf_addr *addr) +{ + struct spf_dns_cb *cb; + gchar *domain; + + CHECK_REC (rec); + + if (begin == NULL || *begin != ':') { + return FALSE; + } + begin ++; + 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; + cb->in_include = rec->in_include; + addr->is_list = TRUE; + addr->data.list = NULL; + domain = rspamd_mempool_strdup (task->task_pool, begin); + if (make_dns_request (task->resolver, task->s, task->task_pool, + spf_record_dns_callback, (void *)cb, RDNS_REQUEST_TXT, domain)) { + task->dns_requests ++; + rec->requests_inflight ++; + + return TRUE; + } + + + return FALSE; +} + +static gboolean +parse_spf_exp (struct rspamd_task *task, const gchar *begin, struct spf_record *rec, struct spf_addr *addr) +{ + CHECK_REC (rec); + + msg_info ("exp record is ignored"); + return TRUE; +} + +static gboolean +parse_spf_redirect (struct rspamd_task *task, const gchar *begin, struct spf_record *rec, struct spf_addr *addr) +{ + struct spf_dns_cb *cb; + gchar *domain; + + CHECK_REC (rec); + + if (begin == NULL || *begin != '=') { + return FALSE; + } + begin ++; + 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_REDIRECT; + cb->in_include = rec->in_include; + domain = rspamd_mempool_strdup (task->task_pool, begin); + if (make_dns_request (task->resolver, task->s, task->task_pool, + spf_record_dns_callback, (void *)cb, RDNS_REQUEST_TXT, domain)) { + task->dns_requests ++; + rec->requests_inflight ++; + + return TRUE; + } + + return FALSE; +} + +static gboolean +parse_spf_exists (struct rspamd_task *task, const gchar *begin, struct spf_record *rec, struct spf_addr *addr) +{ + struct spf_dns_cb *cb; + gchar *host; + + CHECK_REC (rec); + + if (begin == NULL || *begin != ':') { + return FALSE; + } + begin ++; + rec->dns_requests ++; + + addr->data.normal.mask = 32; + 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->in_include = rec->in_include; + host = rspamd_mempool_strdup (task->task_pool, begin); + + if (make_dns_request (task->resolver, task->s, task->task_pool, + spf_record_dns_callback, (void *)cb, RDNS_REQUEST_A, host)) { + task->dns_requests ++; + rec->requests_inflight ++; + + return TRUE; + } + + return FALSE; +} + +static void +reverse_spf_ip (gchar *ip, gint len) +{ + gchar ipbuf[sizeof("255.255.255.255") - 1], *p, *c; + gint t = 0, l = len; + + if (len > (gint)sizeof (ipbuf)) { + msg_info ("cannot reverse string of length %d", len); + return; + } + + p = ipbuf + len; + c = ip; + while (-- l) { + if (*c == '.') { + memcpy (p, c - t, t); + *--p = '.'; + c ++; + t = 0; + continue; + } + + t ++; + c ++; + p --; + } + + memcpy (p - 1, c - t, t + 1); + + memcpy (ip, ipbuf, len); +} + +static gchar * +expand_spf_macro (struct rspamd_task *task, struct spf_record *rec, gchar *begin) +{ + gchar *p, *c, *new, *tmp; + gint len = 0, slen = 0, state = 0; +#ifdef HAVE_INET_PTON + gchar ip_buf[INET6_ADDRSTRLEN]; +#endif + gboolean need_expand = FALSE; + + p = begin; + /* Calculate length */ + while (*p) { + switch (state) { + case 0: + /* Skip any character and wait for % in input */ + if (*p == '%') { + state = 1; + } + else { + len ++; + } + + slen ++; + 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 ++; + } + else if (*p == '_') { + /* %20 */ + len += sizeof ("%20") - 1; + } + else if (*p == '{') { + state = 2; + } + else { + /* Something unknown */ + msg_info ("<%s>: spf error for domain %s: unknown spf element", + task->message_id, rec->sender_domain); + return begin; + } + p ++; + slen ++; + break; + case 2: + /* Read macro name */ + switch (g_ascii_tolower (*p)) { + case 'i': +#ifdef HAVE_INET_PTON + len += sizeof (INET6_ADDRSTRLEN) - 1; +#else + len += sizeof (INET_ADDRSTRLEN) - 1; +#endif + break; + case 's': + len += strlen (rec->sender); + break; + case 'l': + len += strlen (rec->local_part); + break; + case 'o': + len += strlen (rec->sender_domain); + break; + case 'd': + len += strlen (rec->cur_domain); + break; + case 'v': + len += sizeof ("in-addr") - 1; + break; + case 'h': + if (task->helo) { + len += strlen (task->helo); + } + break; + default: + msg_info ("<%s>: spf error for domain %s: unknown or unsupported spf macro %c in %s", + task->message_id, rec->sender_domain, *p, begin); + return begin; + } + p ++; + slen ++; + state = 3; + break; + case 3: + /* Read modifier */ + if (*p == '}') { + state = 0; + need_expand = TRUE; + } + else if (*p != 'r' && !g_ascii_isdigit (*p)) { + msg_info ("<%s>: spf error for domain %s: unknown or unsupported spf modifier %c in %s", + task->message_id, rec->sender_domain, *p, begin); + return begin; + } + p ++; + slen ++; + break; + } + } + + if (!need_expand) { + /* No expansion needed */ + return begin; + } + + new = rspamd_mempool_alloc (task->task_pool, len + 1); + + 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++ = '%'; + } + else if (*p == '-') { + *c++ = ' '; + } + else if (*p == '_') { + /* %20 */ + *c++ = '%'; + *c++ = '2'; + *c++ = '0'; + } + else if (*p == '{') { + state = 2; + } + else { + /* Something unknown */ + msg_info ("<%s>: spf error for domain %s: unknown spf element", + task->message_id, rec->sender_domain); + return begin; + } + p ++; + break; + case 2: + /* Read macro name */ + switch (g_ascii_tolower (*p)) { + case 'i': +#ifdef HAVE_INET_PTON + len = rspamd_strlcpy (ip_buf, + rspamd_inet_address_to_string (&task->from_addr), + sizeof (ip_buf)); + memcpy (c, ip_buf, len); +#else + tmp = inet_ntoa (task->from_addr); + len = strlen (tmp); + memcpy (c, tmp, len); +#endif + c += len; + break; + case 's': + len = strlen (rec->sender); + memcpy (c, rec->sender, len); + c += len; + break; + case 'l': + len = strlen (rec->local_part); + memcpy (c, rec->local_part, len); + c += len; + break; + case 'o': + len = strlen (rec->sender_domain); + memcpy (c, rec->sender_domain, len); + c += len; + break; + case 'd': + len = strlen (rec->cur_domain); + memcpy (c, rec->cur_domain, len); + c += len; + break; + case 'v': + len = sizeof ("in-addr") - 1; + memcpy (c, "in-addr", len); + c += len; + break; + case 'h': + if (task->helo) { + tmp = strchr (task->helo, '@'); + if (tmp) { + len = strlen (tmp + 1); + memcpy (c, tmp + 1, len); + c += len; + } + } + break; + default: + msg_info ("<%s>: spf error for domain %s: unknown or unsupported spf macro %c in %s", + task->message_id, rec->sender_domain, *p, begin); + return begin; + } + p ++; + state = 3; + break; + case 3: + /* Read modifier */ + if (*p == '}') { + state = 0; + } + else if (*p == 'r' && len != 0) { + reverse_spf_ip (c - len, len); + len = 0; + } + else if (g_ascii_isdigit (*p)) { + /*XXX: try to implement domain strimming */ + } + else { + msg_info ("<%s>: spf error for domain %s: unknown or unsupported spf macro %c in %s", + task->message_id, rec->sender_domain, *p, begin); + return begin; + } + p ++; + break; + } + } + /* Null terminate */ + *c = '\0'; + + return new; + +} + +#define NEW_ADDR(x) do { \ + (x) = rspamd_mempool_alloc (task->task_pool, sizeof (struct spf_addr)); \ + (x)->mech = check_spf_mech (rec->cur_elt, &need_shift); \ + (x)->spf_string = rspamd_mempool_strdup (task->task_pool, begin); \ + memset (&(x)->data.normal, 0, sizeof ((x)->data.normal)); \ + (x)->data.normal.mask = 32; \ + (x)->is_list = FALSE; \ +} while (0); + +/* Read current element and try to parse record */ +static gboolean +parse_spf_record (struct rspamd_task *task, struct spf_record *rec) +{ + struct spf_addr *new = NULL; + gboolean need_shift, res = FALSE; + gchar *begin; + + rec->cur_elt = rec->elts[rec->elt_num]; + if (rec->cur_elt == NULL) { + return FALSE; + } + else if (*rec->cur_elt == '\0') { + /* Silently skip empty elements */ + rec->elt_num ++; + return TRUE; + } + else { + begin = expand_spf_macro (task, rec, rec->cur_elt); + if (*begin == '?' || *begin == '+' || *begin == '-' || *begin == '~') { + begin ++; + } + + + /* Now check what we have */ + switch (g_ascii_tolower (*begin)) { + case 'a': + /* all or a */ + if (g_ascii_strncasecmp (begin, SPF_ALL, sizeof (SPF_ALL) - 1) == 0) { + NEW_ADDR (new); + begin += sizeof (SPF_ALL) - 1; + res = parse_spf_all (task, begin, rec, new); + } + else if (g_ascii_strncasecmp (begin, SPF_A, sizeof (SPF_A) - 1) == 0) { + NEW_ADDR (new); + begin += sizeof (SPF_A) - 1; + res = parse_spf_a (task, begin, rec, new); + } + else { + msg_info ("<%s>: spf error for domain %s: bad spf command %s", + task->message_id, rec->sender_domain, begin); + } + break; + case 'i': + /* include or ip4 */ + if (g_ascii_strncasecmp (begin, SPF_IP4, sizeof (SPF_IP4) - 1) == 0) { + NEW_ADDR (new); + begin += sizeof (SPF_IP4) - 1; + res = parse_spf_ip4 (task, begin, rec, new); + } + else if (g_ascii_strncasecmp (begin, SPF_INCLUDE, sizeof (SPF_INCLUDE) - 1) == 0) { + NEW_ADDR (new); + begin += sizeof (SPF_INCLUDE) - 1; + res = parse_spf_include (task, begin, rec, new); + } + else if (g_ascii_strncasecmp (begin, SPF_IP6, sizeof (SPF_IP6) - 1) == 0) { +#ifdef HAVE_INET_PTON + NEW_ADDR (new); + begin += sizeof (SPF_IP6) - 1; + res = parse_spf_ip6 (task, begin, rec, new); +#else + msg_info ("ignoring ip6 spf command as IPv6 is not supported: %s", begin); + new = NULL; + res = TRUE; + begin += sizeof (SPF_IP6) - 1; +#endif + } + else { + msg_info ("<%s>: spf error for domain %s: bad spf command %s", + task->message_id, rec->sender_domain, begin); + } + break; + case 'm': + /* mx */ + if (g_ascii_strncasecmp (begin, SPF_MX, sizeof (SPF_MX) - 1) == 0) { + NEW_ADDR (new); + begin += sizeof (SPF_MX) - 1; + res = parse_spf_mx (task, begin, rec, new); + } + else { + msg_info ("<%s>: spf error for domain %s: bad spf command %s", + task->message_id, rec->sender_domain, begin); + } + break; + case 'p': + /* ptr */ + if (g_ascii_strncasecmp (begin, SPF_PTR, sizeof (SPF_PTR) - 1) == 0) { + NEW_ADDR (new); + begin += sizeof (SPF_PTR) - 1; + res = parse_spf_ptr (task, begin, rec, new); + } + else { + msg_info ("<%s>: spf error for domain %s: bad spf command %s", + task->message_id, rec->sender_domain, begin); + } + break; + case 'e': + /* exp or exists */ + if (g_ascii_strncasecmp (begin, SPF_EXP, sizeof (SPF_EXP) - 1) == 0) { + begin += sizeof (SPF_EXP) - 1; + res = parse_spf_exp (task, begin, rec, NULL); + } + else if (g_ascii_strncasecmp (begin, SPF_EXISTS, sizeof (SPF_EXISTS) - 1) == 0) { + NEW_ADDR (new); + begin += sizeof (SPF_EXISTS) - 1; + res = parse_spf_exists (task, begin, rec, new); + } + else { + msg_info ("<%s>: spf error for domain %s: bad spf command %s", + task->message_id, rec->sender_domain, begin); + } + break; + case 'r': + /* redirect */ + if (g_ascii_strncasecmp (begin, SPF_REDIRECT, sizeof (SPF_REDIRECT) - 1) == 0) { + begin += sizeof (SPF_REDIRECT) - 1; + res = parse_spf_redirect (task, begin, rec, NULL); + } + else { + msg_info ("<%s>: spf error for domain %s: bad spf command %s", + task->message_id, 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_info ("<%s>: spf error for domain %s: bad spf command %s", + task->message_id, rec->sender_domain, begin); + break; + } + if (res) { + if (new != NULL) { + rec->addrs = g_list_prepend (rec->addrs, new); + } + rec->elt_num ++; + } + } + + return res; +} +#undef NEW_ADDR + +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 standart spf1 check */ + *begin += sizeof (SPF_SCOPE_MFROM) - 1; + continue; + } + else if (**begin != ',') { + break; + } + (*begin) ++; + } +} + +static void +start_spf_parse (struct spf_record *rec, gchar *begin, guint ttl) +{ + /* Skip spaces */ + while (g_ascii_isspace (*begin)) { + 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 ++; + } + rec->elts = g_strsplit_set (begin, " ", 0); + rec->elt_num = 0; + if (rec->elts) { + rspamd_mempool_add_destructor (rec->task->task_pool, (rspamd_mempool_destruct_t)g_strfreev, rec->elts); + rec->cur_elt = rec->elts[0]; + while (parse_spf_record (rec->task, rec)); + if (ttl != 0) { + rec->ttl = ttl; + } + } + } + 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_info ("<%s>: spf error for domain %s: sender id is invalid", + rec->task->message_id, rec->sender_domain); + } + else { + begin ++; + parse_spf_scopes (rec, &begin); + } + /* Now common spf record */ + while (g_ascii_isspace (*begin) && *begin) { + begin ++; + } + rec->elts = g_strsplit_set (begin, " ", 0); + rec->elt_num = 0; + if (rec->elts) { + rspamd_mempool_add_destructor (rec->task->task_pool, (rspamd_mempool_destruct_t)g_strfreev, rec->elts); + rec->cur_elt = rec->elts[0]; + while (parse_spf_record (rec->task, rec)); + if (ttl != 0) { + rec->ttl = ttl; + } + } + } + else { + msg_debug ("<%s>: spf error for domain %s: bad spf record version: %*s", + rec->task->message_id, rec->sender_domain, sizeof (SPF_VER1_STR) - 1, begin); + } +} + +static void +spf_dns_callback (struct rdns_reply *reply, gpointer arg) +{ + struct spf_record *rec = arg; + struct rdns_reply_entry *elt; + + rec->requests_inflight --; + if (reply->code == RDNS_RC_NOERROR) { + LL_FOREACH (reply->entries, elt) { + start_spf_parse (rec, elt->content.txt.data, elt->ttl); + } + } + + if (rec->requests_inflight == 0) { + rec->callback (rec, rec->task); + } +} + +gchar * +get_spf_domain (struct rspamd_task *task) +{ + gchar *domain, *res = NULL; + GList *domains; + + if (task->from && (domain = strchr (task->from, '@')) != NULL && *domain == '@') { + res = rspamd_mempool_strdup (task->task_pool, domain + 1); + if ((domain = strchr (res, '>')) != NULL) { + *domain = '\0'; + } + } + else { + /* Extract from header */ + domains = message_get_header (task->task_pool, task->message, "From", FALSE); + + if (domains != NULL) { + res = rspamd_mempool_strdup (task->task_pool, domains->data); + + if ((domain = strrchr (res, '@')) == NULL) { + g_list_free (domains); + return NULL; + } + res = rspamd_mempool_strdup (task->task_pool, domain + 1); + g_list_free (domains); + + if ((domain = strchr (res, '>')) != NULL) { + *domain = '\0'; + } + } + } + + return res; +} + +gboolean +resolve_spf (struct rspamd_task *task, spf_cb_t callback) +{ + struct spf_record *rec; + gchar *domain; + GList *domains; + + rec = rspamd_mempool_alloc0 (task->task_pool, sizeof (struct spf_record)); + rec->task = task; + rec->callback = callback; + /* Add destructor */ + rspamd_mempool_add_destructor (task->task_pool, (rspamd_mempool_destruct_t)spf_record_destructor, rec); + + /* Extract from data */ + if (task->from && (domain = strchr (task->from, '@')) != NULL && *domain == '@') { + rec->sender = task->from; + + rec->local_part = rspamd_mempool_strdup (task->task_pool, task->from); + *(rec->local_part + (domain - task->from)) = '\0'; + if (*rec->local_part == '<') { + memmove (rec->local_part, rec->local_part + 1, strlen (rec->local_part)); + } + rec->cur_domain = rspamd_mempool_strdup (task->task_pool, domain + 1); + if ((domain = strchr (rec->cur_domain, '>')) != NULL) { + *domain = '\0'; + } + rec->sender_domain = rec->cur_domain; + + if (make_dns_request (task->resolver, task->s, task->task_pool, spf_dns_callback, + (void *)rec, RDNS_REQUEST_TXT, rec->cur_domain)) { + task->dns_requests ++; + rec->requests_inflight ++; + return TRUE; + } + } + else { + /* Extract from header */ + domains = message_get_header (task->task_pool, task->message, "From", FALSE); + + if (domains != NULL) { + rec->cur_domain = rspamd_mempool_strdup (task->task_pool, domains->data); + g_list_free (domains); + + if ((domain = strrchr (rec->cur_domain, '@')) == NULL) { + return FALSE; + } + rec->sender = rspamd_mempool_strdup (task->task_pool, rec->cur_domain); + rec->local_part = rec->cur_domain; + *domain = '\0'; + rec->cur_domain = domain + 1; + + if ((domain = strchr (rec->local_part, '<')) != NULL) { + memmove (rec->local_part, domain + 1, strlen (domain)); + } + + if ((domain = strchr (rec->cur_domain, '>')) != NULL) { + *domain = '\0'; + } + rec->sender_domain = rec->cur_domain; + if (make_dns_request (task->resolver, task->s, task->task_pool, + spf_dns_callback, (void *)rec, RDNS_REQUEST_TXT, rec->cur_domain)) { + task->dns_requests ++; + rec->requests_inflight ++; + return TRUE; + } + } + } + + return FALSE; +} + +/* + * vi:ts=4 + */ |