diff --git a/spf.py b/spf.py index 26cd662748f9ea5e97f1fd910f250b0dd13eafd7..20d87ae39982de28f25916888516b117ec408de7 100755 --- a/spf.py +++ b/spf.py @@ -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>. # # $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 # Remove incomplete saving of result. Was messing up bmsmilter. Would # be useful if done consistently - and disabled when passing spf= to check(). @@ -187,11 +202,11 @@ if not hasattr(DNS.Type, 'SPF'): DNS.Type.typemap[99] = 'SPF' DNS.Lib.RRunpacker.getSPFdata = DNS.Lib.RRunpacker.getTXTdata -def DNSLookup(name, qtype): +def DNSLookup(name, qtype, strict=True): try: req = DNS.DnsRequest(name, qtype=qtype) resp = req.req() - #resp.show() + #resp.show() # key k: ('wayforward.net', 'A'), value v # FIXME: pydns returns AAAA RR as 16 byte binary string, but # A RR as dotted quad. For consistency, this driver should @@ -202,15 +217,14 @@ def DNSLookup(name, qtype): except DNS.DNSError, x: raise TempError, 'DNS ' + str(x) -def isSPF(txt): - "Return True if txt has SPF record signature." - return txt.startswith('v=spf1 ') or txt == 'v=spf1' +RE_SPF = re.compile(r'^v=spf1$|^v=spf1 ',re.IGNORECASE) # Regular expression to look for modifiers RE_MODIFIER = re.compile(r'^([a-z][a-z0-9_\-\.]*)=', re.IGNORECASE) # 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 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) RE_IP4 = re.compile(PAT_IP4+'$') 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$' '|::(?:%(hex4)s:){5}%(ls32)s$' @@ -720,10 +735,10 @@ class query(object): # validate domain-spec if m in ('a', 'mx', 'ptr', 'exists', 'include'): - arg = self.expand(arg) # any trailing dot was removed by expand() if RE_TOPLAB.split(arg)[-1]: raise PermError('Invalid domain found (use FQDN)', arg) + arg = self.expand(arg) if m == 'include': if arg == self.d: if mech != 'include': @@ -760,11 +775,12 @@ class query(object): spf = spf.split() # Catch case where SPF record has no spaces. # 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" # in relaxed mode. - if spf[0] != 'v=spf1': - raise PermError('Invalid SPF record in', self.d) + if spf[0].lower() != 'v=spf1': + assert strict > 1 + raise AmbiguityWarning('Invalid SPF record in', self.d) spf = spf[1:] # copy of explanations to be modified by exp= @@ -1037,15 +1053,20 @@ class query(object): name. Returns None if not found, or if more than one record 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 - 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: raise PermError('Two or more type TXT spf records found.') if len(a) == 1 and self.strict < 2: return a[0] # check official SPF type first when it becomes more popular 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: # some braindead DNS servers hang on type 99 query if self.strict > 1: raise TempError(x) @@ -1064,7 +1085,7 @@ class query(object): if DELEGATE: # use local record if neither found a = [t for t in self.dns_txt(domain+'._spf.'+DELEGATE) - if isSPF(t) + if RE_SPF.match(t) ] if len(a) == 1: return a[0] return None @@ -1159,7 +1180,7 @@ class query(object): result = self.cache.get( (name, qtype) ) cname = None if not result: - for k, v in DNSLookup(name, qtype): + for k, v in DNSLookup(name, qtype, self.strict): if k == (name, 'CNAME'): cname = v self.cache.setdefault(k, []).append(v)