391b5352f3
This covers conversion of the whole project to python3, *except* for the strings/bytes distinction in __init__.py, which i'm leaving for a second commit. The changes in this commit are intended to be relatively uncontroversial, so that the following commit contains the tricky bits.
169 lines
5.9 KiB
Python
169 lines
5.9 KiB
Python
## @package dnsplug
|
|
# Provide a higher level interface to pydns or dnspython (or other provider).
|
|
# NOT RELEASED: this is a proposed API and implementation.
|
|
# Goals - work with both pydns and dnspython (and possibly other libraries)
|
|
# at a simplied level.
|
|
# TODO:
|
|
# 1. map exceptions to common dnsplug.DNSError exception (with
|
|
# original exception saved as a member).
|
|
# 2. include dict based implementation (handy for test suites)
|
|
# 3. move implementations to subpackages to enable autoselect on first call.
|
|
|
|
## Maximum number of CNAME records to follow
|
|
MAX_CNAME = 10
|
|
|
|
## Lookup DNS records by label and RR type.
|
|
# The response can include records of other types that the DNS
|
|
# server thinks we might need. FIXME: empty result
|
|
# could mean NXDOMAIN or NOANSWER.
|
|
# @param name the DNS label to lookup
|
|
# @param qtype the name of the DNS RR type to lookup
|
|
# @param tcpfallback if False, raise exception instead of TCP fallback
|
|
# @return a list of ((name,type),data) tuples
|
|
def DNSLookup(name, qtype, tcpfallback=True, timeout=30):
|
|
raise NotImplementedError('No supported dns library found')
|
|
|
|
class Session(object):
|
|
"""A Session object has a simple cache with no TTL that is valid
|
|
for a single "session", for example an SMTP conversation."""
|
|
def __init__(self):
|
|
self.cache = {}
|
|
|
|
## Additional DNS RRs we can safely cache.
|
|
# We have to be careful which additional DNS RRs we cache. For
|
|
# instance, PTR records are controlled by the connecting IP, and they
|
|
# could poison our local cache with bogus A and MX records.
|
|
# Each entry is a tuple of (query_type,rr_type). So for instance,
|
|
# the entry ('MX','A') says it is safe (for milter purposes) to cache
|
|
# any 'A' RRs found in an 'MX' query.
|
|
SAFE2CACHE = frozenset((
|
|
('MX','MX'), ('MX','A'),
|
|
('CNAME','CNAME'), ('CNAME','A'),
|
|
('A','A'),
|
|
('AAAA','AAAA'),
|
|
('PTR','PTR'),
|
|
('NS','NS'), ('NS','A'),
|
|
('TXT','TXT'),
|
|
('SPF','SPF')
|
|
))
|
|
|
|
## Cached DNS lookup.
|
|
# @param name the DNS label to query
|
|
# @param qtype the query type, e.g. 'A'
|
|
# @param cnames tracks CNAMES already followed in recursive calls
|
|
def dns(self, name, qtype, cnames=None):
|
|
"""DNS query.
|
|
|
|
If the result is in cache, return that. Otherwise pull the
|
|
result from DNS, and cache ALL answers, so additional info
|
|
is available for further queries later.
|
|
|
|
CNAMEs are followed.
|
|
|
|
If there is no data, [] is returned.
|
|
|
|
pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
|
|
post: isinstance(__return__, types.ListType)
|
|
"""
|
|
result = self.cache.get( (name, qtype) )
|
|
cname = None
|
|
|
|
if not result:
|
|
safe2cache = Session.SAFE2CACHE
|
|
for k, v in DNSLookup(name, qtype):
|
|
if k == (name, 'CNAME'):
|
|
cname = v
|
|
if (qtype,k[1]) in safe2cache:
|
|
self.cache.setdefault(k, []).append(v)
|
|
result = self.cache.get( (name, qtype), [])
|
|
if not result and cname:
|
|
if not cnames:
|
|
cnames = {}
|
|
elif len(cnames) >= MAX_CNAME:
|
|
#return result # if too many == NX_DOMAIN
|
|
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
|
|
cnames[name] = cname
|
|
if cname in cnames:
|
|
raise DNSError('CNAME loop')
|
|
result = self.dns(cname, qtype, cnames=cnames)
|
|
return result
|
|
|
|
def DNSLookup_pydns(name, qtype, tcpfallback=True, timeout=30):
|
|
try:
|
|
# FIXME: To be thread safe, we create a fresh DnsRequest with
|
|
# each call. It would be more efficient to reuse
|
|
# a req object stored in a Session.
|
|
req = DNS.DnsRequest(name, qtype=qtype, timeout=timeout)
|
|
resp = req.req()
|
|
#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
|
|
# return both as binary string.
|
|
#
|
|
if resp.header['tc'] == True:
|
|
if not tcpfallback:
|
|
raise DNS.DNSError('DNS: Truncated UDP Reply, SPF records should fit in a UDP packet')
|
|
try:
|
|
req = DNS.DnsRequest(name, qtype=qtype, protocol='tcp',
|
|
timeout=timeout)
|
|
resp = req.req()
|
|
except DNS.DNSError as x:
|
|
raise DNS.DNSError('TCP Fallback error: ' + str(x))
|
|
return [((a['name'], a['typename']), a['data']) for a in resp.answers]
|
|
except IOError as x:
|
|
raise DNS.DNSError('DNS: ' + str(x))
|
|
|
|
def DNSLookup_dnspython(name,qtype,tcpfallback=True,timeout=30):
|
|
retVal = []
|
|
try:
|
|
# FIXME: how to disable TCP fallback in dnspython if not tcpfallback?
|
|
answers = dns.resolver.query(name, qtype)
|
|
for rdata in answers:
|
|
if qtype == 'A' or qtype == 'AAAA':
|
|
retVal.append(((name, qtype), rdata.address))
|
|
elif qtype == 'MX':
|
|
retVal.append(((name, qtype), (rdata.preference, rdata.exchange)))
|
|
elif qtype == 'PTR':
|
|
retVal.append(((name, qtype), rdata.target.to_text(True)))
|
|
elif qtype == 'TXT' or qtype == 'SPF':
|
|
retVal.append(((name, qtype), rdata.strings))
|
|
except dns.resolver.NoAnswer:
|
|
pass
|
|
except dns.resolver.NXDOMAIN:
|
|
pass
|
|
return retVal
|
|
|
|
try:
|
|
# prefer dnspython (the more complete library)
|
|
import dns
|
|
import dns.resolver # http://www.dnspython.org
|
|
import dns.exception
|
|
|
|
if not hasattr(dns.rdatatype,'SPF'):
|
|
# patch in type99 support
|
|
dns.rdatatype.SPF = 99
|
|
dns.rdatatype._by_text['SPF'] = dns.rdatatype.SPF
|
|
|
|
DNSLookup = DNSLookup_dnspython
|
|
except:
|
|
import DNS # http://pydns.sourceforge.net
|
|
|
|
if not hasattr(DNS.Type, 'SPF'):
|
|
# patch in type99 support
|
|
DNS.Type.SPF = 99
|
|
DNS.Type.typemap[99] = 'SPF'
|
|
DNS.Lib.RRunpacker.getSPFdata = DNS.Lib.RRunpacker.getTXTdata
|
|
|
|
# Fails on Mac OS X? Add domain to /etc/resolv.conf
|
|
DNS.DiscoverNameServers()
|
|
|
|
DNSLookup = DNSLookup_pydns
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
s = Session()
|
|
for n,t in zip(*[iter(sys.argv[1:])]*2):
|
|
print(n,t)
|
|
print(s.dns(n,t))
|