home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / arm / util / hostnames.py < prev    next >
Encoding:
Python Source  |  2012-05-18  |  14.4 KB  |  392 lines

  1. """
  2. Service providing hostname resolution via reverse DNS lookups. This provides
  3. both resolution via a thread pool (looking up several addresses at a time) and
  4. caching of the results. If used, it's advisable that this service is stopped
  5. when it's no longer needed. All calls are both non-blocking and thread safe.
  6.  
  7. Be aware that this relies on querying the system's DNS servers, possibly
  8. leaking the requested addresses to third parties.
  9. """
  10.  
  11. # The only points of concern in terms of concurrent calls are the RESOLVER and
  12. # RESOLVER.resolvedCache. This services provides (mostly) non-locking thread
  13. # safety via the following invariants:
  14. # - Resolver and cache instances are non-destructible
  15. #     Nothing can be removed or invalidated. Rather, halting resolvers and
  16. #     trimming the cache are done via reassignment (pointing the RESOLVER or
  17. #     RESOLVER.resolvedCache to another copy).
  18. # - Functions create and use local references to the resolver and its cache
  19. #     This is for consistency (ie, all operations are done on the same resolver
  20. #     or cache instance regardless of concurrent assignments). Usually it's
  21. #     assigned to a local variable called 'resolverRef' or 'cacheRef'.
  22. # - Locks aren't necessary, but used to help in the following cases:
  23. #     - When assigning to the RESOLVER (to avoid orphaned instances with
  24. #       running thread pools).
  25. #     - When adding/removing from the cache (prevents workers from updating
  26. #       an outdated cache reference).
  27.  
  28. import time
  29. import socket
  30. import threading
  31. import itertools
  32. import Queue
  33. import distutils.sysconfig
  34.  
  35. from util import log, sysTools
  36.  
  37. RESOLVER = None                       # hostname resolver (service is stopped if None)
  38. RESOLVER_LOCK = threading.RLock()     # regulates assignment to the RESOLVER
  39. RESOLVER_COUNTER = itertools.count()  # atomic counter, providing the age for new entries (for trimming)
  40. DNS_ERROR_CODES = ("1(FORMERR)", "2(SERVFAIL)", "3(NXDOMAIN)", "4(NOTIMP)", "5(REFUSED)", "6(YXDOMAIN)",
  41.                    "7(YXRRSET)", "8(NXRRSET)", "9(NOTAUTH)", "10(NOTZONE)", "16(BADVERS)")
  42.  
  43. CONFIG = {"queries.hostnames.poolSize": 5,
  44.           "queries.hostnames.useSocketModule": False,
  45.           "cache.hostnames.size": 700000,
  46.           "cache.hostnames.trimSize": 200000,
  47.           "log.hostnameCacheTrimmed": log.INFO}
  48.  
  49. def loadConfig(config):
  50.   config.update(CONFIG, {
  51.     "queries.hostnames.poolSize": 1,
  52.     "cache.hostnames.size": 100,
  53.     "cache.hostnames.trimSize": 10})
  54.   
  55.   CONFIG["cache.hostnames.trimSize"] = min(CONFIG["cache.hostnames.trimSize"], CONFIG["cache.hostnames.size"] / 2)
  56.  
  57. def start():
  58.   """
  59.   Primes the service to start resolving addresses. Calling this explicitly is
  60.   not necessary since resolving any address will start the service if it isn't
  61.   already running.
  62.   """
  63.   
  64.   global RESOLVER
  65.   RESOLVER_LOCK.acquire()
  66.   if not isRunning(): RESOLVER = _Resolver()
  67.   RESOLVER_LOCK.release()
  68.  
  69. def stop():
  70.   """
  71.   Halts further resolutions and stops the service. This joins on the resolver's
  72.   thread pool and clears its lookup cache.
  73.   """
  74.   
  75.   global RESOLVER
  76.   RESOLVER_LOCK.acquire()
  77.   if isRunning():
  78.     # Releases resolver instance. This is done first so concurrent calls to the
  79.     # service won't try to use it. However, using a halted instance is fine and
  80.     # all calls currently in progress can still proceed on the RESOLVER's local
  81.     # references.
  82.     resolverRef, RESOLVER = RESOLVER, None
  83.     
  84.     # joins on its worker thread pool
  85.     resolverRef.stop()
  86.     for t in resolverRef.threadPool: t.join()
  87.   RESOLVER_LOCK.release()
  88.  
  89. def setPaused(isPause):
  90.   """
  91.   Allows or prevents further hostname resolutions (resolutions still make use of
  92.   cached entries if available). This starts the service if it isn't already
  93.   running.
  94.   
  95.   Arguments:
  96.     isPause - puts a freeze on further resolutions if true, allows them to
  97.               continue otherwise
  98.   """
  99.   
  100.   # makes sure a running resolver is set with the pausing setting
  101.   RESOLVER_LOCK.acquire()
  102.   start()
  103.   RESOLVER.isPaused = isPause
  104.   RESOLVER_LOCK.release()
  105.  
  106. def isRunning():
  107.   """
  108.   Returns True if the service is currently running, False otherwise.
  109.   """
  110.   
  111.   return bool(RESOLVER)
  112.  
  113. def isPaused():
  114.   """
  115.   Returns True if the resolver is paused, False otherwise.
  116.   """
  117.   
  118.   resolverRef = RESOLVER
  119.   if resolverRef: return resolverRef.isPaused
  120.   else: return False
  121.  
  122. def isResolving():
  123.   """
  124.   Returns True if addresses are currently waiting to be resolved, False
  125.   otherwise.
  126.   """
  127.   
  128.   resolverRef = RESOLVER
  129.   if resolverRef: return not resolverRef.unresolvedQueue.empty()
  130.   else: return False
  131.  
  132. def resolve(ipAddr, timeout = 0, suppressIOExc = True):
  133.   """
  134.   Provides the hostname associated with a given IP address. By default this is
  135.   a non-blocking call, fetching cached results if available and queuing the
  136.   lookup if not. This provides None if the lookup fails (with a suppressed
  137.   exception) or timeout is reached without resolution. This starts the service
  138.   if it isn't already running.
  139.   
  140.   If paused this simply returns the cached reply (no request is queued and
  141.   returns immediately regardless of the timeout argument).
  142.   
  143.   Requests may raise the following exceptions:
  144.   - ValueError - address was unresolvable (includes the DNS error response)
  145.   - IOError - lookup failed due to os or network issues (suppressed by default)
  146.   
  147.   Arguments:
  148.     ipAddr        - ip address to be resolved
  149.     timeout       - maximum duration to wait for a resolution (blocks to
  150.                     completion if None)
  151.     suppressIOExc - suppresses lookup errors and re-runs failed calls if true,
  152.                     raises otherwise
  153.   """
  154.   
  155.   # starts the service if it isn't already running (making sure we have an
  156.   # instance in a thread safe fashion before continuing)
  157.   resolverRef = RESOLVER
  158.   if resolverRef == None:
  159.     RESOLVER_LOCK.acquire()
  160.     start()
  161.     resolverRef = RESOLVER
  162.     RESOLVER_LOCK.release()
  163.   
  164.   if resolverRef.isPaused:
  165.     # get cache entry, raising if an exception and returning if a hostname
  166.     cacheRef = resolverRef.resolvedCache
  167.     
  168.     if ipAddr in cacheRef:
  169.       entry = cacheRef[ipAddr][0]
  170.       if suppressIOExc and type(entry) == IOError: return None
  171.       elif isinstance(entry, Exception): raise entry
  172.       else: return entry
  173.     else: return None
  174.   elif suppressIOExc:
  175.     # if resolver has cached an IOError then flush the entry (this defaults to
  176.     # suppression since these error may be transient)
  177.     cacheRef = resolverRef.resolvedCache
  178.     flush = ipAddr in cacheRef and type(cacheRef[ipAddr]) == IOError
  179.     
  180.     try: return resolverRef.getHostname(ipAddr, timeout, flush)
  181.     except IOError: return None
  182.   else: return resolverRef.getHostname(ipAddr, timeout)
  183.  
  184. def getPendingCount():
  185.   """
  186.   Provides an approximate count of the number of addresses still pending
  187.   resolution.
  188.   """
  189.   
  190.   resolverRef = RESOLVER
  191.   if resolverRef: return resolverRef.unresolvedQueue.qsize()
  192.   else: return 0
  193.  
  194. def getRequestCount():
  195.   """
  196.   Provides the number of resolutions requested since starting the service.
  197.   """
  198.   
  199.   resolverRef = RESOLVER
  200.   if resolverRef: return resolverRef.totalResolves
  201.   else: return 0
  202.  
  203. def _resolveViaSocket(ipAddr):
  204.   """
  205.   Performs hostname lookup via the socket module's gethostbyaddr function. This
  206.   raises an IOError if the lookup fails (network issue) and a ValueError in
  207.   case of DNS errors (address unresolvable).
  208.   
  209.   Arguments:
  210.     ipAddr - ip address to be resolved
  211.   """
  212.   
  213.   try:
  214.     # provides tuple like: ('localhost', [], ['127.0.0.1'])
  215.     return socket.gethostbyaddr(ipAddr)[0]
  216.   except socket.herror, exc:
  217.     if exc[0] == 2: raise IOError(exc[1]) # "Host name lookup failure"
  218.     else: raise ValueError(exc[1]) # usually "Unknown host"
  219.   except socket.error, exc: raise ValueError(exc[1])
  220.  
  221. def _resolveViaHost(ipAddr):
  222.   """
  223.   Performs a host lookup for the given IP, returning the resolved hostname.
  224.   This raises an IOError if the lookup fails (os or network issue), and a
  225.   ValueError in the case of DNS errors (address is unresolvable).
  226.   
  227.   Arguments:
  228.     ipAddr - ip address to be resolved
  229.   """
  230.   
  231.   hostname = sysTools.call("host %s" % ipAddr)[0].split()[-1:][0]
  232.   
  233.   if hostname == "reached":
  234.     # got message: ";; connection timed out; no servers could be reached"
  235.     raise IOError("lookup timed out")
  236.   elif hostname in DNS_ERROR_CODES:
  237.     # got error response (can't do resolution on address)
  238.     raise ValueError("address is unresolvable: %s" % hostname)
  239.   else:
  240.     # strips off ending period and returns hostname
  241.     return hostname[:-1]
  242.  
  243. class _Resolver():
  244.   """
  245.   Performs reverse DNS resolutions. Lookups are a network bound operation so
  246.   this spawns a pool of worker threads to do several at a time in parallel.
  247.   """
  248.   
  249.   def __init__(self):
  250.     # IP Address => (hostname/error, age), resolution failures result in a
  251.     # ValueError with the lookup's status
  252.     self.resolvedCache = {}
  253.     
  254.     self.resolvedLock = threading.RLock() # governs concurrent access when modifying resolvedCache
  255.     self.unresolvedQueue = Queue.Queue()  # unprocessed lookup requests
  256.     self.recentQueries = []               # recent resolution requests to prevent duplicate requests
  257.     self.threadPool = []                  # worker threads that process requests
  258.     self.totalResolves = 0                # counter for the total number of addresses queried to be resolved
  259.     self.isPaused = False                 # prevents further resolutions if true
  260.     self.halt = False                     # if true, tells workers to stop
  261.     self.cond = threading.Condition()     # used for pausing threads
  262.     
  263.     # Determines if resolutions are made using os 'host' calls or python's
  264.     # 'socket.gethostbyaddr'. The following checks if the system has the
  265.     # gethostbyname_r function, which determines if python resolutions can be
  266.     # done in parallel or not. If so, this is preferable.
  267.     isSocketResolutionParallel = distutils.sysconfig.get_config_var("HAVE_GETHOSTBYNAME_R")
  268.     self.useSocketResolution = CONFIG["queries.hostnames.useSocketModule"] and isSocketResolutionParallel
  269.     
  270.     for _ in range(CONFIG["queries.hostnames.poolSize"]):
  271.       t = threading.Thread(target = self._workerLoop)
  272.       t.setDaemon(True)
  273.       t.start()
  274.       self.threadPool.append(t)
  275.   
  276.   def getHostname(self, ipAddr, timeout, flushCache = False):
  277.     """
  278.     Provides the hostname, queuing the request and returning None if the
  279.     timeout is reached before resolution. If a problem's encountered then this
  280.     either raises an IOError (for os and network issues) or ValueError (for DNS
  281.     resolution errors).
  282.     
  283.     Arguments:
  284.       ipAddr     - ip address to be resolved
  285.       timeout    - maximum duration to wait for a resolution (blocks to
  286.                    completion if None)
  287.       flushCache - if true the cache is skipped and address re-resolved
  288.     """
  289.     
  290.     # if outstanding requests are done then clear recentQueries to allow
  291.     # entries removed from the cache to be re-run
  292.     if self.unresolvedQueue.empty(): self.recentQueries = []
  293.     
  294.     # copies reference cache (this is important in case the cache is trimmed
  295.     # during this call)
  296.     cacheRef = self.resolvedCache
  297.     
  298.     if not flushCache and ipAddr in cacheRef:
  299.       # cached response is available - raise if an error, return if a hostname
  300.       response = cacheRef[ipAddr][0]
  301.       if isinstance(response, Exception): raise response
  302.       else: return response
  303.     elif flushCache or ipAddr not in self.recentQueries:
  304.       # new request - queue for resolution
  305.       self.totalResolves += 1
  306.       self.recentQueries.append(ipAddr)
  307.       self.unresolvedQueue.put(ipAddr)
  308.     
  309.     # periodically check cache if requester is willing to wait
  310.     if timeout == None or timeout > 0:
  311.       startTime = time.time()
  312.       
  313.       while timeout == None or time.time() - startTime < timeout:
  314.         if ipAddr in cacheRef:
  315.           # address was resolved - raise if an error, return if a hostname
  316.           response = cacheRef[ipAddr][0]
  317.           if isinstance(response, Exception): raise response
  318.           else: return response
  319.         else: time.sleep(0.1)
  320.     
  321.     return None # timeout reached without resolution
  322.   
  323.   def stop(self):
  324.     """
  325.     Halts further resolutions and terminates the thread.
  326.     """
  327.     
  328.     self.cond.acquire()
  329.     self.halt = True
  330.     self.cond.notifyAll()
  331.     self.cond.release()
  332.   
  333.   def _workerLoop(self):
  334.     """
  335.     Simple producer-consumer loop followed by worker threads. This takes
  336.     addresses from the unresolvedQueue, attempts to look up its hostname, and
  337.     adds its results or the error to the resolved cache. Resolver reference
  338.     provides shared resources used by the thread pool.
  339.     """
  340.     
  341.     while not self.halt:
  342.       # if resolver is paused then put a hold on further resolutions
  343.       if self.isPaused:
  344.         self.cond.acquire()
  345.         if not self.halt: self.cond.wait(1)
  346.         self.cond.release()
  347.         continue
  348.       
  349.       # snags next available ip, timeout is because queue can't be woken up
  350.       # when 'halt' is set
  351.       try: ipAddr = self.unresolvedQueue.get_nowait()
  352.       except Queue.Empty:
  353.         # no elements ready, wait a little while and try again
  354.         self.cond.acquire()
  355.         if not self.halt: self.cond.wait(1)
  356.         self.cond.release()
  357.         continue
  358.       if self.halt: break
  359.       
  360.       try:
  361.         if self.useSocketResolution: result = _resolveViaSocket(ipAddr)
  362.         else: result = _resolveViaHost(ipAddr)
  363.       except IOError, exc: result = exc # lookup failed
  364.       except ValueError, exc: result = exc # dns error
  365.       
  366.       self.resolvedLock.acquire()
  367.       self.resolvedCache[ipAddr] = (result, RESOLVER_COUNTER.next())
  368.       
  369.       # trim cache if excessively large (clearing out oldest entries)
  370.       if len(self.resolvedCache) > CONFIG["cache.hostnames.size"]:
  371.         # Providing for concurrent, non-blocking calls require that entries are
  372.         # never removed from the cache, so this creates a new, trimmed version
  373.         # instead.
  374.         
  375.         # determines minimum age of entries to be kept
  376.         currentCount = RESOLVER_COUNTER.next()
  377.         newCacheSize = CONFIG["cache.hostnames.size"] - CONFIG["cache.hostnames.trimSize"]
  378.         threshold = currentCount - newCacheSize
  379.         newCache = {}
  380.         
  381.         msg = "trimming hostname cache from %i entries to %i" % (len(self.resolvedCache), newCacheSize)
  382.         log.log(CONFIG["log.hostnameCacheTrimmed"], msg)
  383.         
  384.         # checks age of each entry, adding to toDelete if too old
  385.         for ipAddr, entry in self.resolvedCache.iteritems():
  386.           if entry[1] >= threshold: newCache[ipAddr] = entry
  387.         
  388.         self.resolvedCache = newCache
  389.       
  390.       self.resolvedLock.release()
  391.   
  392.