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

More fixes from pyspf

parent 3a90a35c
Branches
No related tags found
No related merge requests found
...@@ -2,7 +2,8 @@ ...@@ -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, ...@@ -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 ...@@ -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 ...@@ -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): ...@@ -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): ...@@ -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?[^\}]*\}))') ...@@ -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: ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment