Skip to content
Snippets Groups Projects
Commit a7e98f41 authored by Stuart Gathman's avatar Stuart Gathman
Browse files

More SPF fixes and tests from pyspf.

parent ea76acdd
No related branches found
No related tags found
No related merge requests found
...@@ -47,6 +47,21 @@ For news, bugfixes, etc. visit the home page for this implementation at ...@@ -47,6 +47,21 @@ For news, bugfixes, etc. visit the home page for this implementation at
# Development taken over by Stuart Gathman <stuart@bmsi.com>. # Development taken over by Stuart Gathman <stuart@bmsi.com>.
# #
# $Log$ # $Log$
# Revision 1.105 2006/10/07 22:06:28 kitterma
# Pass strict status to DNSLookup - will be needed for TCP failover.
#
# Revision 1.104 2006/10/07 21:59:37 customdesigned
# long/empty label tests and fix.
#
# Revision 1.103 2006/10/07 18:16:20 customdesigned
# Add tests for and fix RE_TOPLAB.
#
# Revision 1.102 2006/10/05 13:57:15 customdesigned
# Remove isSPF and make missing space after version tag a warning.
#
# Revision 1.101 2006/10/05 13:39:11 customdesigned
# SPF version tag is case insensitive.
#
# Revision 1.100 2006/10/04 02:14:04 customdesigned # Revision 1.100 2006/10/04 02:14:04 customdesigned
# Remove incomplete saving of result. Was messing up bmsmilter. Would # Remove incomplete saving of result. Was messing up bmsmilter. Would
# be useful if done consistently - and disabled when passing spf= to check(). # be useful if done consistently - and disabled when passing spf= to check().
...@@ -187,7 +202,7 @@ if not hasattr(DNS.Type, 'SPF'): ...@@ -187,7 +202,7 @@ if not hasattr(DNS.Type, 'SPF'):
DNS.Type.typemap[99] = 'SPF' DNS.Type.typemap[99] = 'SPF'
DNS.Lib.RRunpacker.getSPFdata = DNS.Lib.RRunpacker.getTXTdata DNS.Lib.RRunpacker.getSPFdata = DNS.Lib.RRunpacker.getTXTdata
def DNSLookup(name, qtype): def DNSLookup(name, qtype, strict=True):
try: try:
req = DNS.DnsRequest(name, qtype=qtype) req = DNS.DnsRequest(name, qtype=qtype)
resp = req.req() resp = req.req()
...@@ -202,15 +217,14 @@ def DNSLookup(name, qtype): ...@@ -202,15 +217,14 @@ def DNSLookup(name, qtype):
except DNS.DNSError, x: except DNS.DNSError, x:
raise TempError, 'DNS ' + str(x) raise TempError, 'DNS ' + str(x)
def isSPF(txt): RE_SPF = re.compile(r'^v=spf1$|^v=spf1 ',re.IGNORECASE)
"Return True if txt has SPF record signature."
return txt.startswith('v=spf1 ') or txt == 'v=spf1'
# Regular expression to look for modifiers # Regular expression to look for modifiers
RE_MODIFIER = re.compile(r'^([a-z][a-z0-9_\-\.]*)=', re.IGNORECASE) RE_MODIFIER = re.compile(r'^([a-z][a-z0-9_\-\.]*)=', re.IGNORECASE)
# Regular expression to find macro expansions # Regular expression to find macro expansions
RE_CHAR = re.compile(r'%(%|_|-|(\{[^\}]*\}))') PAT_CHAR = r'%(%|_|-|(\{[^\}]*\}))'
RE_CHAR = re.compile(PAT_CHAR)
# Regular expression to break up a macro expansion # Regular expression to break up a macro expansion
RE_ARGS = re.compile(r'([0-9]*)(r?)([^0-9a-zA-Z]*)') RE_ARGS = re.compile(r'([0-9]*)(r?)([^0-9a-zA-Z]*)')
...@@ -222,7 +236,8 @@ PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4) ...@@ -222,7 +236,8 @@ PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
RE_IP4 = re.compile(PAT_IP4+'$') RE_IP4 = re.compile(PAT_IP4+'$')
RE_TOPLAB = re.compile( RE_TOPLAB = re.compile(
r'\.[0-9a-z]*[a-z][0-9a-z]*|[0-9a-z]+-[0-9a-z-]*[0-9a-z]$', re.IGNORECASE) r'\.(?:[0-9a-z]*[a-z][0-9a-z]*|[0-9a-z]+-[0-9a-z-]*[0-9a-z])\.?$|%s'
% PAT_CHAR, re.IGNORECASE)
RE_IP6 = re.compile( '(?:%(hex4)s:){6}%(ls32)s$' RE_IP6 = re.compile( '(?:%(hex4)s:){6}%(ls32)s$'
'|::(?:%(hex4)s:){5}%(ls32)s$' '|::(?:%(hex4)s:){5}%(ls32)s$'
...@@ -720,10 +735,10 @@ class query(object): ...@@ -720,10 +735,10 @@ class query(object):
# validate domain-spec # validate domain-spec
if m in ('a', 'mx', 'ptr', 'exists', 'include'): if m in ('a', 'mx', 'ptr', 'exists', 'include'):
arg = self.expand(arg)
# any trailing dot was removed by expand() # any trailing dot was removed by expand()
if RE_TOPLAB.split(arg)[-1]: if RE_TOPLAB.split(arg)[-1]:
raise PermError('Invalid domain found (use FQDN)', arg) raise PermError('Invalid domain found (use FQDN)', arg)
arg = self.expand(arg)
if m == 'include': if m == 'include':
if arg == self.d: if arg == self.d:
if mech != 'include': if mech != 'include':
...@@ -760,11 +775,12 @@ class query(object): ...@@ -760,11 +775,12 @@ class query(object):
spf = spf.split() spf = spf.split()
# Catch case where SPF record has no spaces. # Catch case where SPF record has no spaces.
# Can never happen with conforming dns_spf(), however # Can never happen with conforming dns_spf(), however
# in the future we might want to give permerror # in the future we might want to give warnings
# for common mistakes like IN TXT "v=spf1" "mx" "-all" # for common mistakes like IN TXT "v=spf1" "mx" "-all"
# in relaxed mode. # in relaxed mode.
if spf[0] != 'v=spf1': if spf[0].lower() != 'v=spf1':
raise PermError('Invalid SPF record in', self.d) assert strict > 1
raise AmbiguityWarning('Invalid SPF record in', self.d)
spf = spf[1:] spf = spf[1:]
# copy of explanations to be modified by exp= # copy of explanations to be modified by exp=
...@@ -1037,15 +1053,20 @@ class query(object): ...@@ -1037,15 +1053,20 @@ class query(object):
name. Returns None if not found, or if more than one record name. Returns None if not found, or if more than one record
is found. is found.
""" """
# Per RFC 4.3/1, check for malformed domain. This produces
# no results as a special case.
for label in domain.split('.'):
if not label or len(label) > 63:
return None
# for performance, check for most common case of TXT first # for performance, check for most common case of TXT first
a = [t for t in self.dns_txt(domain) if isSPF(t)] a = [t for t in self.dns_txt(domain) if RE_SPF.match(t)]
if len(a) > 1: if len(a) > 1:
raise PermError('Two or more type TXT spf records found.') raise PermError('Two or more type TXT spf records found.')
if len(a) == 1 and self.strict < 2: if len(a) == 1 and self.strict < 2:
return a[0] return a[0]
# check official SPF type first when it becomes more popular # check official SPF type first when it becomes more popular
try: try:
b = [t for t in self.dns_99(domain) if isSPF(t)] b = [t for t in self.dns_99(domain) if RE_SPF.match(t)]
except TempError,x: except TempError,x:
# some braindead DNS servers hang on type 99 query # some braindead DNS servers hang on type 99 query
if self.strict > 1: raise TempError(x) if self.strict > 1: raise TempError(x)
...@@ -1064,7 +1085,7 @@ class query(object): ...@@ -1064,7 +1085,7 @@ class query(object):
if DELEGATE: # use local record if neither found if DELEGATE: # use local record if neither found
a = [t a = [t
for t in self.dns_txt(domain+'._spf.'+DELEGATE) for t in self.dns_txt(domain+'._spf.'+DELEGATE)
if isSPF(t) if RE_SPF.match(t)
] ]
if len(a) == 1: return a[0] if len(a) == 1: return a[0]
return None return None
...@@ -1159,7 +1180,7 @@ class query(object): ...@@ -1159,7 +1180,7 @@ class query(object):
result = self.cache.get( (name, qtype) ) result = self.cache.get( (name, qtype) )
cname = None cname = None
if not result: if not result:
for k, v in DNSLookup(name, qtype): for k, v in DNSLookup(name, qtype, self.strict):
if k == (name, 'CNAME'): if k == (name, 'CNAME'):
cname = v cname = v
self.cache.setdefault(k, []).append(v) self.cache.setdefault(k, []).append(v)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment