More fixes from pyspf
This commit is contained in:
@@ -2,7 +2,8 @@
|
|||||||
"""SPF (Sender Policy Framework) implementation.
|
"""SPF (Sender Policy Framework) implementation.
|
||||||
|
|
||||||
Copyright (c) 2003, Terence Way
|
Copyright (c) 2003, Terence Way
|
||||||
Portions Copyright (c) 2004,2005 Stuart Gathman <stuart@bmsi.com>
|
Portions Copyright (c) 2004,2005,2006 Stuart Gathman <stuart@bmsi.com>
|
||||||
|
Portions Copyright (c) 2005,2006 Scott Kitterman <scott@kitterman.com>
|
||||||
This module is free software, and you may redistribute it and/or modify
|
This module is free software, and you may redistribute it and/or modify
|
||||||
it under the same terms as Python itself, so long as this copyright message
|
it under the same terms as Python itself, so long as this copyright message
|
||||||
and disclaimer are retained in their original form.
|
and disclaimer are retained in their original form.
|
||||||
@@ -19,7 +20,7 @@ AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
|
|||||||
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
||||||
|
|
||||||
For more information about SPF, a tool against email forgery, see
|
For more information about SPF, a tool against email forgery, see
|
||||||
http://openspf.org/
|
http://www.openspf.org/
|
||||||
|
|
||||||
For news, bugfixes, etc. visit the home page for this implementation at
|
For news, bugfixes, etc. visit the home page for this implementation at
|
||||||
http://www.wayforward.net/spf/
|
http://www.wayforward.net/spf/
|
||||||
@@ -47,6 +48,9 @@ For news, bugfixes, etc. visit the home page for this implementation at
|
|||||||
# Terrence is not responding to email.
|
# Terrence is not responding to email.
|
||||||
#
|
#
|
||||||
# $Log$
|
# $Log$
|
||||||
|
# Revision 1.22 2006/06/21 21:13:07 customdesigned
|
||||||
|
# initialize perm_error
|
||||||
|
#
|
||||||
# Revision 1.21 2006/05/12 16:15:20 customdesigned
|
# Revision 1.21 2006/05/12 16:15:20 customdesigned
|
||||||
# a:1.2.3.4 -> ip4:1.2.3.4 'lax' heuristic.
|
# a:1.2.3.4 -> ip4:1.2.3.4 'lax' heuristic.
|
||||||
#
|
#
|
||||||
@@ -273,7 +277,7 @@ For news, bugfixes, etc. visit the home page for this implementation at
|
|||||||
|
|
||||||
__author__ = "Terence Way"
|
__author__ = "Terence Way"
|
||||||
__email__ = "terry@wayforward.net"
|
__email__ = "terry@wayforward.net"
|
||||||
__version__ = "1.6: December 18, 2003"
|
__version__ = "1.7: July 26, 2006"
|
||||||
MODULE = 'spf'
|
MODULE = 'spf'
|
||||||
|
|
||||||
USAGE = """To check an incoming mail request:
|
USAGE = """To check an incoming mail request:
|
||||||
@@ -311,6 +315,8 @@ def DNSLookup(name,qtype):
|
|||||||
#resp.show()
|
#resp.show()
|
||||||
# key k: ('wayforward.net', 'A'), value v
|
# key k: ('wayforward.net', 'A'), value v
|
||||||
return [((a['name'], a['typename']), a['data']) for a in resp.answers]
|
return [((a['name'], a['typename']), a['data']) for a in resp.answers]
|
||||||
|
except IOError,x:
|
||||||
|
raise TempError,'DNS ' + str(x)
|
||||||
except DNS.DNSError,x:
|
except DNS.DNSError,x:
|
||||||
raise TempError,'DNS ' + str(x)
|
raise TempError,'DNS ' + str(x)
|
||||||
|
|
||||||
@@ -322,7 +328,7 @@ def isSPF(txt):
|
|||||||
MASK = 0xFFFFFFFFL
|
MASK = 0xFFFFFFFFL
|
||||||
|
|
||||||
# Regular expression to look for modifiers
|
# Regular expression to look for modifiers
|
||||||
RE_MODIFIER = re.compile(r'^([a-zA-Z]+)=')
|
RE_MODIFIER = re.compile(r'^([a-zA-Z0-9_\-\.]+)=')
|
||||||
|
|
||||||
# Regular expression to find macro expansions
|
# Regular expression to find macro expansions
|
||||||
RE_CHAR = re.compile(r'%(%|_|-|(\{[a-zA-Z][0-9]*r?[^\}]*\}))')
|
RE_CHAR = re.compile(r'%(%|_|-|(\{[a-zA-Z][0-9]*r?[^\}]*\}))')
|
||||||
@@ -330,7 +336,7 @@ RE_CHAR = re.compile(r'%(%|_|-|(\{[a-zA-Z][0-9]*r?[^\}]*\}))')
|
|||||||
# 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]*)')
|
||||||
|
|
||||||
RE_CIDR = re.compile(r'/([1-9]|1[0-9]*|2[0-9]*|3[0-2]*)$')
|
RE_CIDR = re.compile(r'/([1-9]|1[0-9]|2[0-9]|3[0-2])$')
|
||||||
|
|
||||||
RE_IP4 = re.compile(r'\.'.join(
|
RE_IP4 = re.compile(r'\.'.join(
|
||||||
[r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)+'$')
|
[r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)+'$')
|
||||||
@@ -370,9 +376,9 @@ except NameError:
|
|||||||
DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr'
|
DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr'
|
||||||
|
|
||||||
# maximum DNS lookups allowed
|
# maximum DNS lookups allowed
|
||||||
MAX_LOOKUP = 10 #draft-schlitt-spf-classic-02 Para 10.1
|
MAX_LOOKUP = 10 #RFC 4408 Para 10.1
|
||||||
MAX_MX = 10 #draft-schlitt-spf-classic-02 Para 10.1
|
MAX_MX = 10 #RFC 4408 Para 10.1
|
||||||
MAX_PTR = 10 #draft-schlitt-spf-classic-02 Para 10.1
|
MAX_PTR = 10 #RFC 4408 Para 10.1
|
||||||
MAX_CNAME = 10 # analogous interpretation to MAX_PTR
|
MAX_CNAME = 10 # analogous interpretation to MAX_PTR
|
||||||
MAX_RECURSION = 20
|
MAX_RECURSION = 20
|
||||||
ALL_MECHANISMS = ('a', 'mx', 'ptr', 'exists', 'include', 'ip4', 'ip6', 'all')
|
ALL_MECHANISMS = ('a', 'mx', 'ptr', 'exists', 'include', 'ip4', 'ip6', 'all')
|
||||||
@@ -480,7 +486,7 @@ class query(object):
|
|||||||
('unknown', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
|
('unknown', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
|
||||||
|
|
||||||
>>> q.check(spf='v=spf1 =a ?all moo')
|
>>> q.check(spf='v=spf1 =a ?all moo')
|
||||||
('unknown', 550, 'SPF Permanent Error: Unknown qualifier, IETF draft para 4.6.1, found in: =a')
|
('unknown', 550, 'SPF Permanent Error: Unknown qualifier, RFC 4408 para 4.6.1, found in: =a')
|
||||||
|
|
||||||
>>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ~all')
|
>>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ~all')
|
||||||
('pass', 250, 'sender SPF verified')
|
('pass', 250, 'sender SPF verified')
|
||||||
@@ -575,6 +581,11 @@ class query(object):
|
|||||||
Returns mech,m,arg,cidrlength,result
|
Returns mech,m,arg,cidrlength,result
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
>>> q = query(s='strong-bad@email.example.com.',
|
||||||
|
... h='mx.example.org', i='192.0.2.3')
|
||||||
|
>>> q.validate_mechanism('A')
|
||||||
|
('A', 'a', 'email.example.com', 32, 'pass')
|
||||||
|
|
||||||
>>> q = query(s='strong-bad@email.example.com',
|
>>> q = query(s='strong-bad@email.example.com',
|
||||||
... h='mx.example.org', i='192.0.2.3')
|
... h='mx.example.org', i='192.0.2.3')
|
||||||
>>> q.validate_mechanism('A')
|
>>> q.validate_mechanism('A')
|
||||||
@@ -583,8 +594,24 @@ class query(object):
|
|||||||
>>> q.validate_mechanism('?mx:%{d}/27')
|
>>> q.validate_mechanism('?mx:%{d}/27')
|
||||||
('?mx:%{d}/27', 'mx', 'email.example.com', 27, 'neutral')
|
('?mx:%{d}/27', 'mx', 'email.example.com', 27, 'neutral')
|
||||||
|
|
||||||
>>> q.validate_mechanism('-mx::%%%_/.Clara.de/27')
|
>>> try: q.validate_mechanism('ip4:1.2.3.4/247')
|
||||||
('-mx::%%%_/.Clara.de/27', 'mx', ':% /.Clara.de', 27, 'fail')
|
... except PermError,x: print x
|
||||||
|
Invalid IP4 address: ip4:1.2.3.4/247
|
||||||
|
|
||||||
|
>>> try: q.validate_mechanism('a:example.com:8080')
|
||||||
|
... except PermError,x: print x
|
||||||
|
Too many :. Not allowed in domain name.: a:example.com:8080
|
||||||
|
|
||||||
|
>>> try: q.validate_mechanism('ip4:1.2.3.444/24')
|
||||||
|
... except PermError,x: print x
|
||||||
|
Invalid IP4 address: ip4:1.2.3.444/24
|
||||||
|
|
||||||
|
>>> try: q.validate_mechanism('-all:3030')
|
||||||
|
... except PermError,x: print x
|
||||||
|
Invalid all mechanism format - only qualifier allowed with all: -all:3030
|
||||||
|
|
||||||
|
>>> q.validate_mechanism('-mx:%%%_/.Clara.de/27')
|
||||||
|
('-mx:%%%_/.Clara.de/27', 'mx', '% /.Clara.de', 27, 'fail')
|
||||||
|
|
||||||
>>> q.validate_mechanism('~exists:%{i}.%{s1}.100/86400.rate.%{d}')
|
>>> q.validate_mechanism('~exists:%{i}.%{s1}.100/86400.rate.%{d}')
|
||||||
('~exists:%{i}.%{s1}.100/86400.rate.%{d}', 'exists', '192.0.2.3.com.100/86400.rate.email.example.com', 32, 'softfail')
|
('~exists:%{i}.%{s1}.100/86400.rate.%{d}', 'exists', '192.0.2.3.com.100/86400.rate.email.example.com', 32, 'softfail')
|
||||||
@@ -608,7 +635,9 @@ class query(object):
|
|||||||
x = self.note_error(
|
x = self.note_error(
|
||||||
'Use the ip4 mechanism for ip4 addresses',mech)
|
'Use the ip4 mechanism for ip4 addresses',mech)
|
||||||
m = 'ip4'
|
m = 'ip4'
|
||||||
|
# Check for : within the arguement
|
||||||
|
if arg.count(':') > 0:
|
||||||
|
raise PermError('Too many :. Not allowed in domain name.',mech)
|
||||||
if m in ('a', 'mx', 'ptr', 'exists', 'include'):
|
if m in ('a', 'mx', 'ptr', 'exists', 'include'):
|
||||||
arg = self.expand(arg)
|
arg = self.expand(arg)
|
||||||
# FQDN must contain at least one '.'
|
# FQDN must contain at least one '.'
|
||||||
@@ -631,11 +660,18 @@ class query(object):
|
|||||||
return mech,m,arg,cidrlength,result
|
return mech,m,arg,cidrlength,result
|
||||||
if m == 'ip4' and not RE_IP4.match(arg):
|
if m == 'ip4' and not RE_IP4.match(arg):
|
||||||
raise PermError('Invalid IP4 address',mech)
|
raise PermError('Invalid IP4 address',mech)
|
||||||
|
#validate 'all' mechanism per RFC 4408 ABNF
|
||||||
|
if m == 'all' and \
|
||||||
|
(arg != self.d or mech.count(':') or mech.count('/')):
|
||||||
|
print '|'+ arg + '|', mech, self.d,
|
||||||
|
self.note_error(
|
||||||
|
'Invalid all mechanism format - only qualifier allowed with all'
|
||||||
|
,mech)
|
||||||
if m in ALL_MECHANISMS:
|
if m in ALL_MECHANISMS:
|
||||||
return mech,m,arg,cidrlength,result
|
return mech,m,arg,cidrlength,result
|
||||||
if m[1:] in ALL_MECHANISMS:
|
if m[1:] in ALL_MECHANISMS:
|
||||||
x = self.note_error(
|
x = self.note_error(
|
||||||
'Unknown qualifier, IETF draft para 4.6.1, found in', mech)
|
'Unknown qualifier, RFC 4408 para 4.6.1, found in', mech)
|
||||||
else:
|
else:
|
||||||
x = self.note_error('Unknown mechanism found',mech)
|
x = self.note_error('Unknown mechanism found',mech)
|
||||||
return mech,m,arg,cidrlength,x
|
return mech,m,arg,cidrlength,x
|
||||||
@@ -770,11 +806,12 @@ class query(object):
|
|||||||
def get_explanation(self, spec):
|
def get_explanation(self, spec):
|
||||||
"""Expand an explanation."""
|
"""Expand an explanation."""
|
||||||
if spec:
|
if spec:
|
||||||
return self.expand(''.join(self.dns_txt(self.expand(spec))))
|
txt = ''.join(self.dns_txt(self.expand(spec)))
|
||||||
|
return self.expand(txt,stripdot=False)
|
||||||
else:
|
else:
|
||||||
return 'explanation : Required option is missing'
|
return 'explanation : Required option is missing'
|
||||||
|
|
||||||
def expand(self, str):
|
def expand(self, str, stripdot=True): # macros='slodipvh'
|
||||||
"""Do SPF RFC macro expansion.
|
"""Do SPF RFC macro expansion.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -842,6 +879,9 @@ class query(object):
|
|||||||
>>> q.expand('%{p2}.trusted-domains.example.net')
|
>>> q.expand('%{p2}.trusted-domains.example.net')
|
||||||
'example.org.trusted-domains.example.net'
|
'example.org.trusted-domains.example.net'
|
||||||
|
|
||||||
|
>>> q.expand('%{p2}.trusted-domains.example.net.')
|
||||||
|
'example.org.trusted-domains.example.net'
|
||||||
|
|
||||||
"""
|
"""
|
||||||
end = 0
|
end = 0
|
||||||
result = ''
|
result = ''
|
||||||
@@ -867,7 +907,10 @@ class query(object):
|
|||||||
JOINERS.get(letter))
|
JOINERS.get(letter))
|
||||||
|
|
||||||
end = i.end()
|
end = i.end()
|
||||||
return result + str[end:]
|
result += str[end:]
|
||||||
|
if stripdot and result.endswith('.'):
|
||||||
|
return result[:-1]
|
||||||
|
return result
|
||||||
|
|
||||||
def dns_spf(self, domain):
|
def dns_spf(self, domain):
|
||||||
"""Get the SPF record recorded in DNS for a specific domain
|
"""Get the SPF record recorded in DNS for a specific domain
|
||||||
@@ -919,7 +962,7 @@ class query(object):
|
|||||||
"""Get a list of IP addresses for all MX exchanges for a
|
"""Get a list of IP addresses for all MX exchanges for a
|
||||||
domain name.
|
domain name.
|
||||||
"""
|
"""
|
||||||
# draft-schlitt-spf-classic-02 section 5.4 "mx"
|
# RFC 4408 section 5.4 "mx"
|
||||||
# To prevent DoS attacks, more than 10 MX names MUST NOT be looked up
|
# To prevent DoS attacks, more than 10 MX names MUST NOT be looked up
|
||||||
if self.strict:
|
if self.strict:
|
||||||
max = MAX_MX
|
max = MAX_MX
|
||||||
@@ -1072,8 +1115,8 @@ def parse_mechanism(str, d):
|
|||||||
>>> parse_mechanism('-exists:%{i}.%{s1}.100/86400.rate.%{d}','foo.com')
|
>>> parse_mechanism('-exists:%{i}.%{s1}.100/86400.rate.%{d}','foo.com')
|
||||||
('-exists', '%{i}.%{s1}.100/86400.rate.%{d}', 32)
|
('-exists', '%{i}.%{s1}.100/86400.rate.%{d}', 32)
|
||||||
|
|
||||||
>>> parse_mechanism('mx::%%%_/.Claranet.de/27','foo.com')
|
>>> parse_mechanism('mx:%%%_/.Claranet.de/27','foo.com')
|
||||||
('mx', ':%%%_/.Claranet.de', 27)
|
('mx', '%%%_/.Claranet.de', 27)
|
||||||
|
|
||||||
>>> parse_mechanism('mx:%{d}/27','foo.com')
|
>>> parse_mechanism('mx:%{d}/27','foo.com')
|
||||||
('mx', '%{d}', 27)
|
('mx', '%{d}', 27)
|
||||||
|
|||||||
Reference in New Issue
Block a user