2010-03-05 03:45:40 +03:00
# Copyright (C) 2003-2007, 2009, 2010 Nominum, Inc.
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose with or without fee is hereby granted,
# provided that the above copyright notice and this permission notice
# appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
""" DNS stub resolver.
@var default_resolver : The default resolver object
@type default_resolver : dns . resolver . Resolver object """
import socket
import sys
import time
import dns . exception
import dns . message
import dns . name
import dns . query
import dns . rcode
import dns . rdataclass
import dns . rdatatype
if sys . platform == ' win32 ' :
import _winreg
class NXDOMAIN ( dns . exception . DNSException ) :
""" The query name does not exist. """
pass
# The definition of the Timeout exception has moved from here to the
# dns.exception module. We keep dns.resolver.Timeout defined for
# backwards compatibility.
Timeout = dns . exception . Timeout
class NoAnswer ( dns . exception . DNSException ) :
""" The response did not contain an answer to the question. """
pass
class NoNameservers ( dns . exception . DNSException ) :
""" No non-broken nameservers are available to answer the query. """
pass
class NotAbsolute ( dns . exception . DNSException ) :
""" Raised if an absolute domain name is required but a relative name
was provided . """
pass
class NoRootSOA ( dns . exception . DNSException ) :
""" Raised if for some reason there is no SOA at the root name.
This should never happen ! """
pass
2010-12-20 04:31:40 +03:00
class NoMetaqueries ( dns . exception . DNSException ) :
""" Metaqueries are not allowed. """
pass
2010-03-05 03:45:40 +03:00
class Answer ( object ) :
""" DNS stub resolver answer
Instances of this class bundle up the result of a successful DNS
resolution .
For convenience , the answer object implements much of the sequence
protocol , forwarding to its rrset . E . g . " for a in answer " is
equivalent to " for a in answer.rrset " , " answer[i] " is equivalent
to " answer.rrset[i] " , and " answer[i:j] " is equivalent to
" answer.rrset[i:j] " .
Note that CNAMEs or DNAMEs in the response may mean that answer
node ' s name might not be the query name.
@ivar qname : The query name
@type qname : dns . name . Name object
@ivar rdtype : The query type
@type rdtype : int
@ivar rdclass : The query class
@type rdclass : int
@ivar response : The response message
@type response : dns . message . Message object
@ivar rrset : The answer
@type rrset : dns . rrset . RRset object
@ivar expiration : The time when the answer expires
@type expiration : float ( seconds since the epoch )
"""
def __init__ ( self , qname , rdtype , rdclass , response ) :
self . qname = qname
self . rdtype = rdtype
self . rdclass = rdclass
self . response = response
min_ttl = - 1
rrset = None
for count in xrange ( 0 , 15 ) :
try :
rrset = response . find_rrset ( response . answer , qname ,
rdclass , rdtype )
if min_ttl == - 1 or rrset . ttl < min_ttl :
min_ttl = rrset . ttl
break
except KeyError :
if rdtype != dns . rdatatype . CNAME :
try :
crrset = response . find_rrset ( response . answer ,
qname ,
rdclass ,
dns . rdatatype . CNAME )
if min_ttl == - 1 or crrset . ttl < min_ttl :
min_ttl = crrset . ttl
for rd in crrset :
qname = rd . target
break
continue
except KeyError :
raise NoAnswer
raise NoAnswer
if rrset is None :
raise NoAnswer
self . rrset = rrset
self . expiration = time . time ( ) + min_ttl
def __getattr__ ( self , attr ) :
if attr == ' name ' :
return self . rrset . name
elif attr == ' ttl ' :
return self . rrset . ttl
elif attr == ' covers ' :
return self . rrset . covers
elif attr == ' rdclass ' :
return self . rrset . rdclass
elif attr == ' rdtype ' :
return self . rrset . rdtype
else :
raise AttributeError ( attr )
def __len__ ( self ) :
return len ( self . rrset )
def __iter__ ( self ) :
return iter ( self . rrset )
def __getitem__ ( self , i ) :
return self . rrset [ i ]
def __delitem__ ( self , i ) :
del self . rrset [ i ]
def __getslice__ ( self , i , j ) :
return self . rrset [ i : j ]
def __delslice__ ( self , i , j ) :
del self . rrset [ i : j ]
class Cache ( object ) :
""" Simple DNS answer cache.
@ivar data : A dictionary of cached data
@type data : dict
@ivar cleaning_interval : The number of seconds between cleanings . The
default is 300 ( 5 minutes ) .
@type cleaning_interval : float
@ivar next_cleaning : The time the cache should next be cleaned ( in seconds
since the epoch . )
@type next_cleaning : float
"""
def __init__ ( self , cleaning_interval = 300.0 ) :
""" Initialize a DNS cache.
@param cleaning_interval : the number of seconds between periodic
cleanings . The default is 300.0
@type cleaning_interval : float .
"""
self . data = { }
self . cleaning_interval = cleaning_interval
self . next_cleaning = time . time ( ) + self . cleaning_interval
def maybe_clean ( self ) :
""" Clean the cache if it ' s time to do so. """
now = time . time ( )
if self . next_cleaning < = now :
keys_to_delete = [ ]
for ( k , v ) in self . data . iteritems ( ) :
if v . expiration < = now :
keys_to_delete . append ( k )
for k in keys_to_delete :
del self . data [ k ]
now = time . time ( )
self . next_cleaning = now + self . cleaning_interval
def get ( self , key ) :
""" Get the answer associated with I {key} . Returns None if
no answer is cached for the key .
@param key : the key
@type key : ( dns . name . Name , int , int ) tuple whose values are the
query name , rdtype , and rdclass .
@rtype : dns . resolver . Answer object or None
"""
self . maybe_clean ( )
v = self . data . get ( key )
if v is None or v . expiration < = time . time ( ) :
return None
return v
def put ( self , key , value ) :
""" Associate key and value in the cache.
@param key : the key
@type key : ( dns . name . Name , int , int ) tuple whose values are the
query name , rdtype , and rdclass .
@param value : The answer being cached
@type value : dns . resolver . Answer object
"""
self . maybe_clean ( )
self . data [ key ] = value
def flush ( self , key = None ) :
""" Flush the cache.
If I { key } is specified , only that item is flushed . Otherwise
the entire cache is flushed .
@param key : the key to flush
@type key : ( dns . name . Name , int , int ) tuple or None
"""
if not key is None :
if self . data . has_key ( key ) :
del self . data [ key ]
else :
self . data = { }
self . next_cleaning = time . time ( ) + self . cleaning_interval
class Resolver ( object ) :
""" DNS stub resolver
@ivar domain : The domain of this host
@type domain : dns . name . Name object
@ivar nameservers : A list of nameservers to query . Each nameserver is
a string which contains the IP address of a nameserver .
@type nameservers : list of strings
@ivar search : The search list . If the query name is a relative name ,
the resolver will construct an absolute query name by appending the search
names one by one to the query name .
@type search : list of dns . name . Name objects
@ivar port : The port to which to send queries . The default is 53.
@type port : int
@ivar timeout : The number of seconds to wait for a response from a
server , before timing out .
@type timeout : float
@ivar lifetime : The total number of seconds to spend trying to get an
answer to the question . If the lifetime expires , a Timeout exception
will occur .
@type lifetime : float
@ivar keyring : The TSIG keyring to use . The default is None .
@type keyring : dict
@ivar keyname : The TSIG keyname to use . The default is None .
@type keyname : dns . name . Name object
@ivar keyalgorithm : The TSIG key algorithm to use . The default is
dns . tsig . default_algorithm .
@type keyalgorithm : string
@ivar edns : The EDNS level to use . The default is - 1 , no Edns .
@type edns : int
@ivar ednsflags : The EDNS flags
@type ednsflags : int
@ivar payload : The EDNS payload size . The default is 0.
@type payload : int
@ivar cache : The cache to use . The default is None .
@type cache : dns . resolver . Cache object
"""
def __init__ ( self , filename = ' /etc/resolv.conf ' , configure = True ) :
""" Initialize a resolver instance.
@param filename : The filename of a configuration file in
standard / etc / resolv . conf format . This parameter is meaningful
only when I { configure } is true and the platform is POSIX .
@type filename : string or file object
@param configure : If True ( the default ) , the resolver instance
is configured in the normal fashion for the operating system
the resolver is running on . ( I . e . a / etc / resolv . conf file on
POSIX systems and from the registry on Windows systems . )
@type configure : bool """
self . reset ( )
if configure :
if sys . platform == ' win32 ' :
self . read_registry ( )
elif filename :
self . read_resolv_conf ( filename )
def reset ( self ) :
""" Reset all resolver configuration to the defaults. """
self . domain = \
dns . name . Name ( dns . name . from_text ( socket . gethostname ( ) ) [ 1 : ] )
if len ( self . domain ) == 0 :
self . domain = dns . name . root
self . nameservers = [ ]
self . search = [ ]
self . port = 53
self . timeout = 2.0
self . lifetime = 30.0
self . keyring = None
self . keyname = None
self . keyalgorithm = dns . tsig . default_algorithm
self . edns = - 1
self . ednsflags = 0
self . payload = 0
self . cache = None
def read_resolv_conf ( self , f ) :
""" Process f as a file in the /etc/resolv.conf format. If f is
a string , it is used as the name of the file to open ; otherwise it
is treated as the file itself . """
if isinstance ( f , str ) or isinstance ( f , unicode ) :
try :
f = open ( f , ' r ' )
except IOError :
# /etc/resolv.conf doesn't exist, can't be read, etc.
# We'll just use the default resolver configuration.
self . nameservers = [ ' 127.0.0.1 ' ]
return
want_close = True
else :
want_close = False
try :
for l in f :
if len ( l ) == 0 or l [ 0 ] == ' # ' or l [ 0 ] == ' ; ' :
continue
tokens = l . split ( )
if len ( tokens ) == 0 :
continue
if tokens [ 0 ] == ' nameserver ' :
self . nameservers . append ( tokens [ 1 ] )
elif tokens [ 0 ] == ' domain ' :
self . domain = dns . name . from_text ( tokens [ 1 ] )
elif tokens [ 0 ] == ' search ' :
for suffix in tokens [ 1 : ] :
self . search . append ( dns . name . from_text ( suffix ) )
finally :
if want_close :
f . close ( )
if len ( self . nameservers ) == 0 :
self . nameservers . append ( ' 127.0.0.1 ' )
def _determine_split_char ( self , entry ) :
#
# The windows registry irritatingly changes the list element
# delimiter in between ' ' and ',' (and vice-versa) in various
# versions of windows.
#
if entry . find ( ' ' ) > = 0 :
split_char = ' '
elif entry . find ( ' , ' ) > = 0 :
split_char = ' , '
else :
# probably a singleton; treat as a space-separated list.
split_char = ' '
return split_char
def _config_win32_nameservers ( self , nameservers ) :
""" Configure a NameServer registry entry. """
# we call str() on nameservers to convert it from unicode to ascii
nameservers = str ( nameservers )
split_char = self . _determine_split_char ( nameservers )
ns_list = nameservers . split ( split_char )
for ns in ns_list :
if not ns in self . nameservers :
self . nameservers . append ( ns )
def _config_win32_domain ( self , domain ) :
""" Configure a Domain registry entry. """
# we call str() on domain to convert it from unicode to ascii
self . domain = dns . name . from_text ( str ( domain ) )
def _config_win32_search ( self , search ) :
""" Configure a Search registry entry. """
# we call str() on search to convert it from unicode to ascii
search = str ( search )
split_char = self . _determine_split_char ( search )
search_list = search . split ( split_char )
for s in search_list :
if not s in self . search :
self . search . append ( dns . name . from_text ( s ) )
def _config_win32_fromkey ( self , key ) :
""" Extract DNS info from a registry key. """
try :
servers , rtype = _winreg . QueryValueEx ( key , ' NameServer ' )
except WindowsError :
servers = None
if servers :
self . _config_win32_nameservers ( servers )
try :
dom , rtype = _winreg . QueryValueEx ( key , ' Domain ' )
if dom :
self . _config_win32_domain ( dom )
except WindowsError :
pass
else :
try :
servers , rtype = _winreg . QueryValueEx ( key , ' DhcpNameServer ' )
except WindowsError :
servers = None
if servers :
self . _config_win32_nameservers ( servers )
try :
dom , rtype = _winreg . QueryValueEx ( key , ' DhcpDomain ' )
if dom :
self . _config_win32_domain ( dom )
except WindowsError :
pass
try :
search , rtype = _winreg . QueryValueEx ( key , ' SearchList ' )
except WindowsError :
search = None
if search :
self . _config_win32_search ( search )
def read_registry ( self ) :
""" Extract resolver configuration from the Windows registry. """
lm = _winreg . ConnectRegistry ( None , _winreg . HKEY_LOCAL_MACHINE )
want_scan = False
try :
try :
# XP, 2000
tcp_params = _winreg . OpenKey ( lm ,
r ' SYSTEM \ CurrentControlSet '
r ' \ Services \ Tcpip \ Parameters ' )
want_scan = True
except EnvironmentError :
# ME
tcp_params = _winreg . OpenKey ( lm ,
r ' SYSTEM \ CurrentControlSet '
r ' \ Services \ VxD \ MSTCP ' )
try :
self . _config_win32_fromkey ( tcp_params )
finally :
tcp_params . Close ( )
if want_scan :
interfaces = _winreg . OpenKey ( lm ,
r ' SYSTEM \ CurrentControlSet '
r ' \ Services \ Tcpip \ Parameters '
r ' \ Interfaces ' )
try :
i = 0
while True :
try :
guid = _winreg . EnumKey ( interfaces , i )
i + = 1
key = _winreg . OpenKey ( interfaces , guid )
if not self . _win32_is_nic_enabled ( lm , guid , key ) :
continue
try :
self . _config_win32_fromkey ( key )
finally :
key . Close ( )
except EnvironmentError :
break
finally :
interfaces . Close ( )
finally :
lm . Close ( )
def _win32_is_nic_enabled ( self , lm , guid , interface_key ) :
# Look in the Windows Registry to determine whether the network
# interface corresponding to the given guid is enabled.
#
# (Code contributed by Paul Marks, thanks!)
#
try :
# This hard-coded location seems to be consistent, at least
# from Windows 2000 through Vista.
connection_key = _winreg . OpenKey (
lm ,
r ' SYSTEM \ CurrentControlSet \ Control \ Network '
r ' \ { 4D36E972-E325-11CE-BFC1-08002BE10318} '
r ' \ %s \ Connection ' % guid )
try :
# The PnpInstanceID points to a key inside Enum
( pnp_id , ttype ) = _winreg . QueryValueEx (
connection_key , ' PnpInstanceID ' )
if ttype != _winreg . REG_SZ :
raise ValueError
device_key = _winreg . OpenKey (
lm , r ' SYSTEM \ CurrentControlSet \ Enum \ %s ' % pnp_id )
try :
# Get ConfigFlags for this device
( flags , ttype ) = _winreg . QueryValueEx (
device_key , ' ConfigFlags ' )
if ttype != _winreg . REG_DWORD :
raise ValueError
# Based on experimentation, bit 0x1 indicates that the
# device is disabled.
return not ( flags & 0x1 )
finally :
device_key . Close ( )
finally :
connection_key . Close ( )
except ( EnvironmentError , ValueError ) :
# Pre-vista, enabled interfaces seem to have a non-empty
# NTEContextList; this was how dnspython detected enabled
# nics before the code above was contributed. We've retained
# the old method since we don't know if the code above works
# on Windows 95/98/ME.
try :
( nte , ttype ) = _winreg . QueryValueEx ( interface_key ,
' NTEContextList ' )
return nte is not None
except WindowsError :
return False
def _compute_timeout ( self , start ) :
now = time . time ( )
if now < start :
if start - now > 1 :
# Time going backwards is bad. Just give up.
raise Timeout
else :
# Time went backwards, but only a little. This can
# happen, e.g. under vmware with older linux kernels.
# Pretend it didn't happen.
now = start
duration = now - start
if duration > = self . lifetime :
raise Timeout
return min ( self . lifetime - duration , self . timeout )
def query ( self , qname , rdtype = dns . rdatatype . A , rdclass = dns . rdataclass . IN ,
tcp = False , source = None ) :
""" Query nameservers to find the answer to the question.
The I { qname } , I { rdtype } , and I { rdclass } parameters may be objects
of the appropriate type , or strings that can be converted into objects
of the appropriate type . E . g . For I { rdtype } the integer 2 and the
the string ' NS ' both mean to query for records with DNS rdata type NS .
@param qname : the query name
@type qname : dns . name . Name object or string
@param rdtype : the query type
@type rdtype : int or string
@param rdclass : the query class
@type rdclass : int or string
@param tcp : use TCP to make the query ( default is False ) .
@type tcp : bool
@param source : bind to this IP address ( defaults to machine default IP ) .
@type source : IP address in dotted quad notation
@rtype : dns . resolver . Answer instance
@raises Timeout : no answers could be found in the specified lifetime
@raises NXDOMAIN : the query name does not exist
@raises NoAnswer : the response did not contain an answer
@raises NoNameservers : no non - broken nameservers are available to
answer the question . """
if isinstance ( qname , ( str , unicode ) ) :
qname = dns . name . from_text ( qname , None )
2010-12-09 16:53:45 +03:00
if isinstance ( rdtype , ( str , unicode ) ) :
2010-03-05 03:45:40 +03:00
rdtype = dns . rdatatype . from_text ( rdtype )
2010-12-20 04:31:40 +03:00
if dns . rdatatype . is_metatype ( rdtype ) :
raise NoMetaqueries
2010-12-09 16:53:45 +03:00
if isinstance ( rdclass , ( str , unicode ) ) :
2010-03-05 03:45:40 +03:00
rdclass = dns . rdataclass . from_text ( rdclass )
2010-12-20 04:31:40 +03:00
if dns . rdataclass . is_metaclass ( rdclass ) :
raise NoMetaqueries
2010-03-05 03:45:40 +03:00
qnames_to_try = [ ]
if qname . is_absolute ( ) :
qnames_to_try . append ( qname )
else :
if len ( qname ) > 1 :
qnames_to_try . append ( qname . concatenate ( dns . name . root ) )
if self . search :
for suffix in self . search :
qnames_to_try . append ( qname . concatenate ( suffix ) )
else :
qnames_to_try . append ( qname . concatenate ( self . domain ) )
all_nxdomain = True
start = time . time ( )
for qname in qnames_to_try :
if self . cache :
answer = self . cache . get ( ( qname , rdtype , rdclass ) )
if answer :
return answer
request = dns . message . make_query ( qname , rdtype , rdclass )
if not self . keyname is None :
2010-09-05 01:05:16 +04:00
request . use_tsig ( self . keyring , self . keyname ,
algorithm = self . keyalgorithm )
2010-03-05 03:45:40 +03:00
request . use_edns ( self . edns , self . ednsflags , self . payload )
response = None
#
# make a copy of the servers list so we can alter it later.
#
nameservers = self . nameservers [ : ]
backoff = 0.10
while response is None :
if len ( nameservers ) == 0 :
raise NoNameservers
for nameserver in nameservers [ : ] :
timeout = self . _compute_timeout ( start )
try :
if tcp :
response = dns . query . tcp ( request , nameserver ,
timeout , self . port ,
source = source )
else :
response = dns . query . udp ( request , nameserver ,
timeout , self . port ,
source = source )
except ( socket . error , dns . exception . Timeout ) :
#
# Communication failure or timeout. Go to the
# next server
#
response = None
continue
except dns . query . UnexpectedSource :
#
# Who knows? Keep going.
#
response = None
continue
except dns . exception . FormError :
#
# We don't understand what this server is
# saying. Take it out of the mix and
# continue.
#
nameservers . remove ( nameserver )
response = None
continue
rcode = response . rcode ( )
if rcode == dns . rcode . NOERROR or \
rcode == dns . rcode . NXDOMAIN :
break
#
# We got a response, but we're not happy with the
# rcode in it. Remove the server from the mix if
# the rcode isn't SERVFAIL.
#
if rcode != dns . rcode . SERVFAIL :
nameservers . remove ( nameserver )
response = None
if not response is None :
break
#
# All nameservers failed!
#
if len ( nameservers ) > 0 :
#
# But we still have servers to try. Sleep a bit
# so we don't pound them!
#
timeout = self . _compute_timeout ( start )
sleep_time = min ( timeout , backoff )
backoff * = 2
time . sleep ( sleep_time )
if response . rcode ( ) == dns . rcode . NXDOMAIN :
continue
all_nxdomain = False
break
if all_nxdomain :
raise NXDOMAIN
answer = Answer ( qname , rdtype , rdclass , response )
if self . cache :
self . cache . put ( ( qname , rdtype , rdclass ) , answer )
return answer
def use_tsig ( self , keyring , keyname = None ,
algorithm = dns . tsig . default_algorithm ) :
""" Add a TSIG signature to the query.
@param keyring : The TSIG keyring to use ; defaults to None .
@type keyring : dict
@param keyname : The name of the TSIG key to use ; defaults to None .
The key must be defined in the keyring . If a keyring is specified
but a keyname is not , then the key used will be the first key in the
keyring . Note that the order of keys in a dictionary is not defined ,
so applications should supply a keyname when a keyring is used , unless
they know the keyring contains only one key .
@param algorithm : The TSIG key algorithm to use . The default
is dns . tsig . default_algorithm .
@type algorithm : string """
self . keyring = keyring
if keyname is None :
self . keyname = self . keyring . keys ( ) [ 0 ]
else :
self . keyname = keyname
self . keyalgorithm = algorithm
def use_edns ( self , edns , ednsflags , payload ) :
""" Configure Edns.
@param edns : The EDNS level to use . The default is - 1 , no Edns .
@type edns : int
@param ednsflags : The EDNS flags
@type ednsflags : int
@param payload : The EDNS payload size . The default is 0.
@type payload : int """
if edns is None :
edns = - 1
self . edns = edns
self . ednsflags = ednsflags
self . payload = payload
default_resolver = None
def get_default_resolver ( ) :
""" Get the default resolver, initializing it if necessary. """
global default_resolver
if default_resolver is None :
default_resolver = Resolver ( )
return default_resolver
def query ( qname , rdtype = dns . rdatatype . A , rdclass = dns . rdataclass . IN ,
tcp = False , source = None ) :
""" Query nameservers to find the answer to the question.
This is a convenience function that uses the default resolver
object to make the query .
@see : L { dns . resolver . Resolver . query } for more information on the
parameters . """
return get_default_resolver ( ) . query ( qname , rdtype , rdclass , tcp , source )
def zone_for_name ( name , rdclass = dns . rdataclass . IN , tcp = False , resolver = None ) :
""" Find the name of the zone which contains the specified name.
@param name : the query name
@type name : absolute dns . name . Name object or string
@param rdclass : The query class
@type rdclass : int
@param tcp : use TCP to make the query ( default is False ) .
@type tcp : bool
@param resolver : the resolver to use
@type resolver : dns . resolver . Resolver object or None
@rtype : dns . name . Name """
if isinstance ( name , ( str , unicode ) ) :
name = dns . name . from_text ( name , dns . name . root )
if resolver is None :
resolver = get_default_resolver ( )
if not name . is_absolute ( ) :
raise NotAbsolute ( name )
while 1 :
try :
answer = resolver . query ( name , dns . rdatatype . SOA , rdclass , tcp )
2010-12-09 16:53:45 +03:00
if answer . rrset . name == name :
return name
# otherwise we were CNAMEd or DNAMEd and need to look higher
2010-03-05 03:45:40 +03:00
except ( dns . resolver . NXDOMAIN , dns . resolver . NoAnswer ) :
2010-12-09 16:53:45 +03:00
pass
try :
name = name . parent ( )
except dns . name . NoParent :
raise NoRootSOA