123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- # -*- encoding: utf-8 -*-
- """
- sleekxmpp.xmlstream.dns
- ~~~~~~~~~~~~~~~~~~~~~~~
- :copyright: (c) 2012 Nathanael C. Fritz
- :license: MIT, see LICENSE for more details
- """
- import socket
- import logging
- import random
- log = logging.getLogger(__name__)
- #: Global flag indicating the availability of the ``dnspython`` package.
- #: Installing ``dnspython`` can be done via:
- #:
- #: .. code-block:: sh
- #:
- #: pip install dnspython
- #:
- #: For Python3, installation may require installing from source using
- #: the ``python3`` branch:
- #:
- #: .. code-block:: sh
- #:
- #: git clone http://github.com/rthalley/dnspython
- #: cd dnspython
- #: git checkout python3
- #: python3 setup.py install
- DNSPYTHON_AVAILABLE = False
- try:
- import dns.resolver
- DNSPYTHON_AVAILABLE = True
- except ImportError as e:
- log.debug("Could not find dnspython package. " + \
- "Not all features will be available")
- def default_resolver():
- """Return a basic DNS resolver object.
- :returns: A :class:`dns.resolver.Resolver` object if dnspython
- is available. Otherwise, ``None``.
- """
- if DNSPYTHON_AVAILABLE:
- return dns.resolver.get_default_resolver()
- return None
- def resolve(host, port=None, service=None, proto='tcp',
- resolver=None, use_ipv6=True, use_dnspython=True):
- """Peform DNS resolution for a given hostname.
- Resolution may perform SRV record lookups if a service and protocol
- are specified. The returned addresses will be sorted according to
- the SRV priorities and weights.
- If no resolver is provided, the dnspython resolver will be used if
- available. Otherwise the built-in socket facilities will be used,
- but those do not provide SRV support.
- If SRV records were used, queries to resolve alternative hosts will
- be made as needed instead of all at once.
- :param host: The hostname to resolve.
- :param port: A default port to connect with. SRV records may
- dictate use of a different port.
- :param service: Optional SRV service name without leading underscore.
- :param proto: Optional SRV protocol name without leading underscore.
- :param resolver: Optionally provide a DNS resolver object that has
- been custom configured.
- :param use_ipv6: Optionally control the use of IPv6 in situations
- where it is either not available, or performance
- is degraded. Defaults to ``True``.
- :param use_dnspython: Optionally control if dnspython is used to make
- the DNS queries instead of the built-in DNS
- library.
- :type host: string
- :type port: int
- :type service: string
- :type proto: string
- :type resolver: :class:`dns.resolver.Resolver`
- :type use_ipv6: bool
- :type use_dnspython: bool
- :return: An iterable of IP address, port pairs in the order
- dictated by SRV priorities and weights, if applicable.
- """
- if not use_dnspython:
- if DNSPYTHON_AVAILABLE:
- log.debug("DNS: Not using dnspython, but dnspython is installed.")
- else:
- log.debug("DNS: Not using dnspython.")
- if not use_ipv6:
- log.debug("DNS: Use of IPv6 has been disabled.")
- if resolver is None and DNSPYTHON_AVAILABLE and use_dnspython:
- resolver = dns.resolver.get_default_resolver()
- # An IPv6 literal is allowed to be enclosed in square brackets, but
- # the brackets must be stripped in order to process the literal;
- # otherwise, things break.
- host = host.strip('[]')
- try:
- # If `host` is an IPv4 literal, we can return it immediately.
- ipv4 = socket.inet_aton(host)
- yield (host, host, port)
- except socket.error:
- pass
- if use_ipv6:
- try:
- # Likewise, If `host` is an IPv6 literal, we can return
- # it immediately.
- if hasattr(socket, 'inet_pton'):
- ipv6 = socket.inet_pton(socket.AF_INET6, host)
- yield (host, host, port)
- except (socket.error, ValueError):
- pass
- # If no service was provided, then we can just do A/AAAA lookups on the
- # provided host. Otherwise we need to get an ordered list of hosts to
- # resolve based on SRV records.
- if not service:
- hosts = [(host, port)]
- else:
- hosts = get_SRV(host, port, service, proto,
- resolver=resolver,
- use_dnspython=use_dnspython)
- for host, port in hosts:
- results = []
- if host == 'localhost':
- if use_ipv6:
- results.append((host, '::1', port))
- results.append((host, '127.0.0.1', port))
- if use_ipv6:
- for address in get_AAAA(host, resolver=resolver,
- use_dnspython=use_dnspython):
- results.append((host, address, port))
- for address in get_A(host, resolver=resolver,
- use_dnspython=use_dnspython):
- results.append((host, address, port))
- for host, address, port in results:
- yield host, address, port
- def get_A(host, resolver=None, use_dnspython=True):
- """Lookup DNS A records for a given host.
- If ``resolver`` is not provided, or is ``None``, then resolution will
- be performed using the built-in :mod:`socket` module.
- :param host: The hostname to resolve for A record IPv4 addresses.
- :param resolver: Optional DNS resolver object to use for the query.
- :param use_dnspython: Optionally control if dnspython is used to make
- the DNS queries instead of the built-in DNS
- library.
- :type host: string
- :type resolver: :class:`dns.resolver.Resolver` or ``None``
- :type use_dnspython: bool
- :return: A list of IPv4 literals.
- """
- log.debug("DNS: Querying %s for A records." % host)
- # If not using dnspython, attempt lookup using the OS level
- # getaddrinfo() method.
- if resolver is None or not use_dnspython:
- try:
- recs = socket.getaddrinfo(host, None, socket.AF_INET,
- socket.SOCK_STREAM)
- return [rec[4][0] for rec in recs]
- except socket.gaierror:
- log.debug("DNS: Error retreiving A address info for %s." % host)
- return []
- # Using dnspython:
- try:
- recs = resolver.query(host, dns.rdatatype.A)
- return [rec.to_text() for rec in recs]
- except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
- log.debug("DNS: No A records for %s" % host)
- return []
- except dns.exception.Timeout:
- log.debug("DNS: A record resolution timed out for %s" % host)
- return []
- except dns.exception.DNSException as e:
- log.debug("DNS: Error querying A records for %s" % host)
- log.exception(e)
- return []
- def get_AAAA(host, resolver=None, use_dnspython=True):
- """Lookup DNS AAAA records for a given host.
- If ``resolver`` is not provided, or is ``None``, then resolution will
- be performed using the built-in :mod:`socket` module.
- :param host: The hostname to resolve for AAAA record IPv6 addresses.
- :param resolver: Optional DNS resolver object to use for the query.
- :param use_dnspython: Optionally control if dnspython is used to make
- the DNS queries instead of the built-in DNS
- library.
- :type host: string
- :type resolver: :class:`dns.resolver.Resolver` or ``None``
- :type use_dnspython: bool
- :return: A list of IPv6 literals.
- """
- log.debug("DNS: Querying %s for AAAA records." % host)
- # If not using dnspython, attempt lookup using the OS level
- # getaddrinfo() method.
- if resolver is None or not use_dnspython:
- if not socket.has_ipv6:
- log.debug("Unable to query %s for AAAA records: IPv6 is not supported", host)
- return []
- try:
- recs = socket.getaddrinfo(host, None, socket.AF_INET6,
- socket.SOCK_STREAM)
- return [rec[4][0] for rec in recs]
- except (OSError, socket.gaierror):
- log.debug("DNS: Error retreiving AAAA address " + \
- "info for %s." % host)
- return []
- # Using dnspython:
- try:
- recs = resolver.query(host, dns.rdatatype.AAAA)
- return [rec.to_text() for rec in recs]
- except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
- log.debug("DNS: No AAAA records for %s" % host)
- return []
- except dns.exception.Timeout:
- log.debug("DNS: AAAA record resolution timed out for %s" % host)
- return []
- except dns.exception.DNSException as e:
- log.debug("DNS: Error querying AAAA records for %s" % host)
- log.exception(e)
- return []
- def get_SRV(host, port, service, proto='tcp', resolver=None, use_dnspython=True):
- """Perform SRV record resolution for a given host.
- .. note::
- This function requires the use of the ``dnspython`` package. Calling
- :func:`get_SRV` without ``dnspython`` will return the provided host
- and port without performing any DNS queries.
- :param host: The hostname to resolve.
- :param port: A default port to connect with. SRV records may
- dictate use of a different port.
- :param service: Optional SRV service name without leading underscore.
- :param proto: Optional SRV protocol name without leading underscore.
- :param resolver: Optionally provide a DNS resolver object that has
- been custom configured.
- :type host: string
- :type port: int
- :type service: string
- :type proto: string
- :type resolver: :class:`dns.resolver.Resolver`
- :return: A list of hostname, port pairs in the order dictacted
- by SRV priorities and weights.
- """
- if resolver is None or not use_dnspython:
- log.warning("DNS: dnspython not found. Can not use SRV lookup.")
- return [(host, port)]
- log.debug("DNS: Querying SRV records for %s" % host)
- try:
- recs = resolver.query('_%s._%s.%s' % (service, proto, host),
- dns.rdatatype.SRV)
- except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
- log.debug("DNS: No SRV records for %s." % host)
- return [(host, port)]
- except dns.exception.Timeout:
- log.debug("DNS: SRV record resolution timed out for %s." % host)
- return [(host, port)]
- except dns.exception.DNSException as e:
- log.debug("DNS: Error querying SRV records for %s." % host)
- log.exception(e)
- return [(host, port)]
- if len(recs) == 1 and recs[0].target == '.':
- return [(host, port)]
- answers = {}
- for rec in recs:
- if rec.priority not in answers:
- answers[rec.priority] = []
- if rec.weight == 0:
- answers[rec.priority].insert(0, rec)
- else:
- answers[rec.priority].append(rec)
- sorted_recs = []
- for priority in sorted(answers.keys()):
- while answers[priority]:
- running_sum = 0
- sums = {}
- for rec in answers[priority]:
- running_sum += rec.weight
- sums[running_sum] = rec
- selected = random.randint(0, running_sum + 1)
- for running_sum in sums:
- if running_sum >= selected:
- rec = sums[running_sum]
- host = rec.target.to_text()
- if host.endswith('.'):
- host = host[:-1]
- sorted_recs.append((host, rec.port))
- answers[priority].remove(rec)
- break
- return sorted_recs
|