/* $Id: spf.C,v 1.11 2006/04/03 18:21:47 dm Exp $ */
/*
*
* Copyright (C) 2004 David Mazieres (dm@uun.org)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA
*
*/
#include "asmtpd.h"
spfhosts_map smap;
#define SPF_VERS "v=spf1"
static int spftrace (getenv ("SPF_TRACE") ? atoi (getenv ("SPF_TRACE")) : 0);
static rxx spaceplus (" +");
static rxx domcidr ("^([\\w_\\-.]+)?(/(/)?(\\d{1,3}))?$");
static rxx ip4cidr ("^(\\d{1,3}(\\.\\d{1,3}){3})(/(\\d{1,3}))?$");
rxx &
spfhosts_map::linerx ()
{
static rxx spfrx ("^\\s*([^\\x00-\\x20\\x7f:]+)\\s*:\\s*(|\\S.*)$");
return spfrx;
}
bool
spfhosts_map::lookup (str *spfrecp, str domain)
{
if (!load ())
return false;
domain = mytolower (domain);
if (str *sp = table[domain]) {
*spfrecp = *sp;
return true;
}
const char *p = domain;
while ((p = strchr (p + 1, '.'))) {
if (str *sp = table[domain]) {
*spfrecp = *sp;
return true;
}
}
return false;
}
spf_t::spf_t (in_addr a, str f, ptr<bhash<str> > lc, int rd)
: addr (a), sender (f), fallback (NULL), tracedepth (0), recdepth (rd),
loopcheck (lc ? lc : ptr<bhash<str> > (New refcounted<bhash<str> >)),
ptr_cache_err (false), dnsrq (NULL), recurse (NULL)
{
}
spf_t::~spf_t ()
{
if (dnsrq)
dnsreq_cancel (dnsrq);
if (recurse)
delete recurse;
}
void
spf_t::getexp (cbv cb, ptr<txtlist> t, int err)
{
dnsrq = NULL;
if (t) {
strbuf sb;
for (u_int i = 0; i < t->t_ntxt; i++)
sb << t->t_txts[i];
explain = sb;
}
(*cb) ();
}
void
spf_t::finish (spf_result stat)
{
if (stat != SPF_PASS && !explain && expdn && expdn.len ()) {
if (!macro_subst (&expdn, expdn)) {
getptr (wrap (this, &spf_t::finish, stat));
return;
}
str n = expdn;
expdn = NULL;
if (dnsreq_t *d
= dns_txtbyname (n, wrap (this, &spf_t::getexp,
wrap (this, &spf_t::finish, stat))))
dnsrq = d;
return;
}
if (stat != SPF_PASS && explain && explain.len ()) {
if (!macro_subst (&explain, explain, true)) {
getptr (wrap (this, &spf_t::finish, stat));
return;
}
}
if (spftrace > !!tracedepth) {
warn ("SPF_TRACE: %*sRETURN ", tracedepth, "")
<< inet_ntoa (addr) << " / "
<< domain << " -> " << spf_print (stat) << "\n";
if (explain)
warn ("SPF_TRACE: %*sEXPLANATION ", tracedepth, "") << explain << "\n";
}
result = stat;
cb_t c (cb);
cb = NULL;
domain = NULL;
(*c) (this);
if (cb)
init ();
else
delete this;
}
bool
spf_t::suffix_check (const char *targ, str suffix)
{
u_int n = strlen (targ);
if (n < suffix.len ())
return false;
if (n == suffix.len ())
return (!strcasecmp (targ, suffix));
const char *p = targ + n - suffix.len ();
if (p[-1] != '.')
return false;
return !strcasecmp (p, suffix);
}
void
spf_t::getptr (cbv cb)
{
if (dnsreq_t *d = dns_hostbyaddr (addr, wrap (this, &spf_t::getptr_2, cb)))
dnsrq = d;
}
void
spf_t::getptr_2 (cbv cb, ptr<hostent> h, int err)
{
dnsrq = NULL;
if (spftrace >= 4)
printaddrs (sender, h, err);
if (dns_tmperr (err)) {
finish (SPF_ERROR);
return;
}
ptr_cache = h;
ptr_cache_err = err;
(*cb) ();
}
void
spf_t::init ()
{
redirect = NULL;
curmech = NULL;
if (!domain)
if (!(domain = extract_domain (sender)))
domain = sender;
if (!validate_domain (domain, true))
finish (SPF_UNKNOWN);
else if (spfrec)
parse_spf ();
else if (!loopcheck->insert (mytolower (domain)) || recdepth > 20) {
if (spftrace >= 2)
warn ("SPF_TRACE: %*sLOOP ", tracedepth, "") << domain << "\n";
finish (SPF_UNKNOWN);
}
else if (dnsreq_t *d = dns_txtbyname (domain, wrap (this, &spf_t::gettxt)))
dnsrq = d;
}
void
spf_t::gettxt (ptr<txtlist> t, int err)
{
dnsrq = NULL;
if (spftrace >= 4)
printtxtlist (sender, t, err);
if (err) {
if (dns_tmperr (err))
finish (SPF_ERROR);
else if (fallback && fallback->lookup (&spfrec, domain))
parse_spf ();
else
finish (SPF_NONE);
return;
}
for (u_int i = 0; i < t->t_ntxt; i++)
if (!strncmp (t->t_txts[i], SPF_VERS, sizeof (SPF_VERS) - 1)
&& (t->t_txts[i][sizeof (SPF_VERS) - 1] == ' '
|| t->t_txts[i][sizeof (SPF_VERS) - 1] == '\0')) {
if (spfrec) {
finish (SPF_UNKNOWN);
return;
}
else
spfrec = t->t_txts[i] + sizeof (SPF_VERS) - 1;
}
if (spfrec)
parse_spf ();
else
finish (SPF_NONE);
}
void
spf_t::parse_spf ()
{
vec<str> dm;
split (&dm, spaceplus, spfrec);
if (spftrace > !!recdepth) {
warn ("SPF_TRACE: %*sCHECK ", tracedepth, "")
<< inet_ntoa (addr) << " / " << domain << "\n";
if (spftrace >= 2) {
warn ("SPF_TRACE: %*sRULES", tracedepth, "");
for (str *rp = dm.base (); rp < dm.lim (); rp++)
warnx << " " << *rp;
warnx << "\n";
}
}
for (str *dmp = dm.base (); dmp < dm.lim (); dmp++) {
if (!dmp->len ())
continue;
const char *eq = strchr (*dmp, '=');
if (!strchr (*dmp, '='))
mechv.push_back (*dmp);
else {
const char *co = strchr (*dmp, ':');
if (co && co < eq)
mechv.push_back (*dmp);
else if (!strncasecmp (*dmp, "redirect=", 9))
redirect = dmp->cstr () + 9;
else if (!strncasecmp (*dmp, "exp=", 4)) {
expdn = dmp->cstr () + 4;
explain = NULL;
}
}
}
mech_start ();
}
/*
reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
"$" | ","
unreserved = alphanum | mark
mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
delims = "<" | ">" | "#" | "%" | <">
unwise = "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"
*/
inline bool
needsescape (u_char c)
{
if (isalnum (c))
return false;
if (c == '-' || c == '_' || c == '.' || c == '!' || c == '~'
|| c == '*' || c == '\'' || c == '(' || c == ')')
return false;
return true;
}
static str
urlescape (str in)
{
strbuf sb;
for (const char *p = in; *p; p++)
if (needsescape (*p))
sb.fmt ("%%%02x", (u_char) *p);
else
sb.fmt ("%c", *p);
return sb;
}
bool
spf_t::macro_subst_inner (const strbuf &out, str in, bool exp)
{
if (!in.len ())
return true;
str text;
bool escape = isupper (in[0]);
switch (tolower (in[0])) {
case 'l':
if (!(text = extract_local (sender)))
text = "postmaster";
break;
case 's':
if (extract_local (sender))
text = sender;
else
text = strbuf ("postmaster@%s", sender.cstr ());
break;
case 'o':
if (!(text = extract_domain (sender)))
text = sender;
break;
case 'd':
text = domain;
break;
case 'i':
text = inet_ntoa (addr);
break;
case 'p':
if (!ptrok ())
return false;
else if (ptr_cache)
text = ptr_cache->h_name;
else
text = "unknown";
break;
case 'v':
text = "in-addr";
break;
case 'h':
text = helo ? helo : str ("unknown");
break;
case 'r':
text = opt->hostname;
break;
case 'c':
if (exp) {
text = inet_ntoa (addr);
break;
}
/* cascade */
case 't':
if (exp) {
text = strbuf ("%lu", (u_long) time (NULL));
break;
}
/* cascade */
default:
out << "%{" << in << "}";
return true;
}
static rxx td ("^.(\\d+)?(r)?([\\.\\-\\+,\\/_=]+)?");
if (!td.match (in)) {
out << "%{" << in << "}";
return true;
}
str spn = td[3];
if (!spn)
spn = ".";
vec<str> parts;
const char *p = text;
while (*p) {
int n = strcspn (p, spn);
parts.push_back (str (p, n));
p += n;
if (*p)
p++;
}
if (td[2]) {
str *sp = parts.base (), *ep = parts.lim ();
while (sp + 1 < ep) {
str tmp = *--ep;
*ep = *sp;
*sp++ = tmp;
}
}
if (str ns = td[1]) {
u_int n = atoi (ns);
if (n < parts.size ())
parts.popn_front (parts.size () - n );
}
for (u_int i = 0; i < parts.size (); i++)
if (i)
out << "." << (escape ? urlescape (parts[i]) : parts[i]);
else
out << (escape ? urlescape (parts[i]) : parts[i]);
return true;
}
bool
spf_t::macro_subst (str *outp, str in, bool exp)
{
strbuf out;
for (const char *p = in; *p; p++) {
if (*p != '%') {
out.fmt ("%c", *p);
continue;
}
switch (*++p) {
case '%':
out.fmt ("%%");
break;
case '_':
out.fmt (" ");
break;
case '-':
out.fmt ("%%20");
break;
case '{':
{
const char *r = strchr (p, '}');
if (!r) {
out << "%" << p;
if (spftrace)
warn << "malformed macro " << in << "\n";
*outp = out;
return true;
}
if (!macro_subst_inner (out, str (p + 1, r - p - 1), exp))
return false;
p = r;
break;
}
case '\0':
out.fmt ("%%");
if (spftrace >= 3)
warn ("SPF_TRACE: %*sMACRO ", tracedepth, "") << in << " -> ";
*outp = out;
if (spftrace >= 3)
warnx << *outp << "\n";
return true;
default:
out.fmt ("%%%c", *p);
break;
}
}
if (spftrace >= 3)
warn ("SPF_TRACE: %*sMACRO ", tracedepth, "") << in << " -> ";
*outp = out;
if (spftrace >= 3)
warnx << *outp << "\n";
return true;
}
bool
spf_t::addr_check (int cidrlen, ref<hostent> h)
{
if (spftrace >= 5)
printaddrs ("addr_check", h);
u_int32_t mask = ip4_mask (cidrlen);
u_int32_t targ = addr.s_addr & mask;
for (char **ap = h->h_addr_list; *ap; ap++)
if (targ == (((in_addr *) *ap)->s_addr & mask))
return true;
return false;
}
void
spf_t::mech_start ()
{
if (mechv.empty ()) {
if (redirect) {
curmech = (strbuf () << "redirect=" << redirect);
if (!macro_subst (&redirect, redirect)) {
getptr (wrap (this, &spf_t::mech_start));
return;
}
spfrec = NULL;
recdepth++;
if (spftrace >= 2)
warn ("SPF_TRACE: %*sREDIRECT ", tracedepth, "")
<< domain << " -> " << redirect << "\n";
domain = redirect;
init ();
}
else {
curmech = NULL;
finish (SPF_NEUTRAL);
}
return;
}
curmech = mechv.pop_front ();
const char *mp = curmech;
switch (*mp) {
case '+':
case '-':
case '~':
case '?':
prefix = *mp++;
break;
default:
prefix = '+';
break;
}
char *cp = strchr (mp, ':');
if (!cp)
cp = strchr (mp, '/');
str arg;
if (cp)
arg = *cp == ':' ? cp + 1 : cp;
str mech = mytolower (cp ? str (mp, cp - mp) : str (mp));
if (mech == "all")
mech_end (SPF_PASS);
else if (mech == "include")
mech_include (arg);
else if (mech == "a")
mech_a (arg);
else if (mech == "mx")
mech_mx (arg);
else if (mech == "ptr")
mech_ptr (arg);
else if (mech == "ip4")
mech_ip4 (arg);
else if (mech == "ip6")
mech_ip6 (arg);
else if (mech == "exists")
mech_exists (arg);
else
finish (SPF_UNKNOWN);
}
void
spf_t::mech_end (spf_result res)
{
if (spftrace >= 2) {
warn ("SPF_TRACE: %*sTEST ", tracedepth, "");
if (curmech)
warnx << curmech << " -> ";
warnx << spf_print (res) << "\n";
}
switch (res) {
case SPF_PASS:
switch (prefix) {
case '+':
finish (SPF_PASS);
break;
case '-':
finish (SPF_FAIL);
break;
case '~':
finish (SPF_SOFTFAIL);
break;
case '?':
finish (SPF_NEUTRAL);
break;
}
break;
case SPF_FAIL:
case SPF_SOFTFAIL:
case SPF_NEUTRAL:
mech_start ();
break;
case SPF_NONE:
case SPF_ERROR:
case SPF_UNKNOWN:
finish (res);
break;
default:
panic ("unknown SPF state %d\n", res);
break;
}
}
void
spf_t::mech_include (str targ)
{
if (!targ) {
mech_end (SPF_UNKNOWN);
return;
}
if (!macro_subst (&targ, targ)) {
getptr (wrap (this, &spf_t::mech_include, targ));
return;
}
if (spftrace >= 2)
warn ("SPF_TRACE: %*sINCLUDE ", tracedepth, "") << targ << " ...\n";
recurse = New spf_t (addr, sender, loopcheck, recdepth + 1);
recurse->cb = wrap (this, &spf_t::mech_include_2);
recurse->domain = targ;
recurse->helo = helo;
recurse->ptr_cache = ptr_cache;
recurse->tracedepth = tracedepth + 1;
recurse->init ();
}
void
spf_t::mech_include_2 (spf_t *)
{
spf_result res = recurse->result;
bool error = (res == SPF_NONE || res == SPF_ERROR || res == SPF_UNKNOWN);
if (error || res == SPF_PASS || spftrace) {
strbuf sb;
sb << curmech << " (";
if (recurse->curmech)
sb << recurse->curmech << " -> ";
sb << spf_print (res) << ")";
curmech = sb;
}
recurse = NULL;
if (res == SPF_NONE)
res = SPF_UNKNOWN;
mech_end (res);
}
void
spf_t::mech_a (str targ)
{
int cidrlen = 32;
if (!targ)
targ = domain;
else {
if (!macro_subst (&targ, targ)) {
getptr (wrap (this, &spf_t::mech_a, targ));
return;
}
if (!domcidr.match (targ) || domcidr[3]) {
mech_end (SPF_FAIL);
return;
}
if (str cl = domcidr[4])
cidrlen = atoi (domcidr[4]);
if (str d = domcidr[1])
targ = domcidr[1];
else
targ = domain;
}
if (dnsreq_t *d
= dns_hostbyname (targ, wrap (this, &spf_t::mech_a_2, cidrlen),
false, false))
dnsrq = d;
}
void
spf_t::mech_a_2 (int cidrlen, ptr<hostent> h, int err)
{
dnsrq = NULL;
if (err)
mech_end (dns_tmperr (err) ? SPF_ERROR : SPF_FAIL);
else if (addr_check (cidrlen, h))
mech_end (SPF_PASS);
else
mech_end (SPF_FAIL);
}
void
spf_t::mech_mx (str targ)
{
int cidrlen = 32;
if (!targ)
targ = domain;
else {
if (!macro_subst (&targ, targ)) {
getptr (wrap (this, &spf_t::mech_a, targ));
return;
}
if (!domcidr.match (targ) || domcidr[3]) {
mech_end (SPF_FAIL);
return;
}
if (str cl = domcidr[4])
cidrlen = atoi (domcidr[4]);
if (str d = domcidr[1])
targ = domcidr[1];
else
targ = domain;
}
if (dnsreq_t *d
= dns_mxbyname (targ, wrap (this, &spf_t::mech_mx_2, cidrlen)))
dnsrq = d;
}
void
spf_t::mech_mx_2 (int cidrlen, ptr<mxlist> mxl, int err)
{
dnsrq = NULL;
if (spftrace >= 5)
printmxlist ("mech_mx_2", mxl, err);
if (err)
mech_end (dns_tmperr (err) ? SPF_ERROR : SPF_FAIL);
else if (dnsreq_t *d
= dns_hostbyname (mxl->m_mxes[0].name,
wrap (this, &spf_t::mech_mx_3, cidrlen, mxl, 1),
false, false))
dnsrq = d;
}
void
spf_t::mech_mx_3 (int cidrlen, ptr<mxlist> mxl, int n, ptr<hostent> h, int err)
{
dnsrq = NULL;
if (h && addr_check (cidrlen, h))
mech_end (SPF_PASS);
else if (err && dns_tmperr (err))
mech_end (SPF_ERROR);
else if (n >= mxl->m_nmx)
mech_end (SPF_FAIL);
else if (dnsreq_t *d
= dns_hostbyname (mxl->m_mxes[n].name,
wrap (this, &spf_t::mech_mx_3, cidrlen, mxl, n+1),
false, false))
dnsrq = d;
}
void
spf_t::mech_ptr (str targ)
{
if (!ptrok ()) {
getptr (wrap (this, &spf_t::mech_ptr, targ));
return;
}
if (!targ)
targ = domain;
macro_subst (&targ, targ);
if (!ptr_cache) {
mech_end (dns_tmperr (ptr_cache_err) ? SPF_ERROR : SPF_FAIL);
return;
}
if (suffix_check (ptr_cache->h_name, targ)) {
mech_end (SPF_PASS);
return;
}
for (char **np = ptr_cache->h_aliases; *np; np++)
if (suffix_check (targ, *np)) {
mech_end (SPF_PASS);
return;
}
mech_end (SPF_FAIL);
}
void
spf_t::mech_ip4 (str targ)
{
in_addr a;
if (!targ || !ip4cidr.match (targ) || !inet_aton (ip4cidr[1], &a)) {
mech_end (SPF_UNKNOWN);
return;
}
u_int32_t mask;
if (str cl = ip4cidr[4])
mask = ip4_mask (atoi (cl));
else
mask = 0xffffffff;
if ((addr.s_addr & mask) == (a.s_addr & mask))
mech_end (SPF_PASS);
else
mech_end (SPF_FAIL);
}
void
spf_t::mech_ip6 (str targ)
{
/* Never matches, no IPv6 support */
mech_end (SPF_FAIL);
}
void
spf_t::mech_exists (str targ)
{
if (!targ)
targ = domain;
else if (!macro_subst (&targ, targ)) {
getptr (wrap (this, &spf_t::mech_exists, targ));
return;
}
if (dnsreq_t *d = dns_hostbyname (targ,
wrap (this, &spf_t::mech_exists_2, targ),
false, false))
dnsrq = d;
}
void
spf_t::mech_exists_2 (str targ, ptr<hostent> h, int err)
{
dnsrq = NULL;
if (spftrace >= 4)
printaddrs (targ, h, err);
if (h)
mech_end (SPF_PASS);
else if (dns_tmperr (err))
mech_end (SPF_ERROR);
else
mech_end (SPF_FAIL);
}
static void
spf_check_3 (spfckcb_t cb, spf_result override, str omech, spf_t *spf)
{
spf_result res (spf->result);
if (override != SPF_INVALID) {
if (res == SPF_NEUTRAL || res == SPF_NONE || res == SPF_UNKNOWN) {
res = override;
(*cb) (res, spf->explain, omech);
}
else
(*cb) (res, spf->explain, spf->curmech);
return;
}
switch (res) {
case SPF_FAIL:
if (opt->spf_fail) {
spf->spfrec = opt->spf_fail;
spf->cb = wrap (spf_check_3, cb, res, spf->curmech);
spf->domain = NULL;
spf->explain = opt->spf_exp;
return;
}
break;
case SPF_NONE:
if (opt->spf_none) {
spf->spfrec = opt->spf_none;
spf->cb = wrap (spf_check_3, cb, res, spf->curmech);
spf->domain = NULL;
spf->explain = opt->spf_exp;
return;
}
break;
default:
break;
}
(*cb) (res, spf->explain, spf->curmech);
}
static void
spf_check_2 (spfckcb_t cb, spf_t *spf)
{
spf_result res (spf->result);
switch (res) {
case SPF_PASS:
case SPF_FAIL:
case SPF_SOFTFAIL:
case SPF_ERROR:
(*cb) (res, spf->explain, NULL);
return;
default:
break;
}
spf->spfrec = NULL;
spf->explain = opt->spf_exp;
spf->cb = wrap (spf_check_3, cb, SPF_INVALID, str (NULL));
}
void
spf_check (in_addr a, str from, spfckcb_t cb,
str helo, ptr<const hostent> ptrc)
{
spf_t *spf = New spf_t (a, from);
spf->helo = helo;
spf->ptr_cache = ptrc;
spf->spfrec = opt->spf_local;
spf->explain = opt->spf_exp;
spf->fallback = &smap;
if (opt->spf_local)
spf->cb = wrap (spf_check_2, cb);
else
spf->cb = wrap (spf_check_3, cb, SPF_INVALID, str (NULL));
spf->init ();
}
const char *
spf_print (spf_result res)
{
switch (res) {
case SPF_PASS:
return "pass";
case SPF_FAIL:
return "fail";
case SPF_SOFTFAIL:
return "softfail";
case SPF_NEUTRAL:
return "neutral";
case SPF_NONE:
return "none";
case SPF_ERROR:
return "error";
case SPF_UNKNOWN:
return "unknown";
default:
return "bad SPF result code";
}
}
const char *
spf1_print (spf_result res)
{
switch (res) {
case SPF_PASS:
return "Pass";
case SPF_FAIL:
return "Fail";
case SPF_SOFTFAIL:
return "SoftFail";
case SPF_NEUTRAL:
return "Neutral";
case SPF_NONE:
return "None";
case SPF_ERROR:
return "TempError";
case SPF_UNKNOWN:
return "PermError";
default:
return "bad SPF result code";
}
}
syntax highlighted by Code2HTML, v. 0.9.1