123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- # Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
- # Copyright (C) 2003-2017 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.
- """Talk to a DNS server."""
- import base64
- import socket
- import struct
- import time
- import dns.asyncbackend
- import dns.exception
- import dns.inet
- import dns.name
- import dns.message
- import dns.rcode
- import dns.rdataclass
- import dns.rdatatype
- from dns.query import _compute_times, _matches_destination, BadResponse, ssl, \
- UDPMode, _have_httpx, _have_http2, NoDOH
- if _have_httpx:
- import httpx
- # for brevity
- _lltuple = dns.inet.low_level_address_tuple
- def _source_tuple(af, address, port):
- # Make a high level source tuple, or return None if address and port
- # are both None
- if address or port:
- if address is None:
- if af == socket.AF_INET:
- address = '0.0.0.0'
- elif af == socket.AF_INET6:
- address = '::'
- else:
- raise NotImplementedError(f'unknown address family {af}')
- return (address, port)
- else:
- return None
- def _timeout(expiration, now=None):
- if expiration:
- if not now:
- now = time.time()
- return max(expiration - now, 0)
- else:
- return None
- async def send_udp(sock, what, destination, expiration=None):
- """Send a DNS message to the specified UDP socket.
- *sock*, a ``dns.asyncbackend.DatagramSocket``.
- *what*, a ``bytes`` or ``dns.message.Message``, the message to send.
- *destination*, a destination tuple appropriate for the address family
- of the socket, specifying where to send the query.
- *expiration*, a ``float`` or ``None``, the absolute time at which
- a timeout exception should be raised. If ``None``, no timeout will
- occur.
- Returns an ``(int, float)`` tuple of bytes sent and the sent time.
- """
- if isinstance(what, dns.message.Message):
- what = what.to_wire()
- sent_time = time.time()
- n = await sock.sendto(what, destination, _timeout(expiration, sent_time))
- return (n, sent_time)
- async def receive_udp(sock, destination=None, expiration=None,
- ignore_unexpected=False, one_rr_per_rrset=False,
- keyring=None, request_mac=b'', ignore_trailing=False,
- raise_on_truncation=False):
- """Read a DNS message from a UDP socket.
- *sock*, a ``dns.asyncbackend.DatagramSocket``.
- See :py:func:`dns.query.receive_udp()` for the documentation of the other
- parameters, exceptions, and return type of this method.
- """
- wire = b''
- while 1:
- (wire, from_address) = await sock.recvfrom(65535, _timeout(expiration))
- if _matches_destination(sock.family, from_address, destination,
- ignore_unexpected):
- break
- received_time = time.time()
- r = dns.message.from_wire(wire, keyring=keyring, request_mac=request_mac,
- one_rr_per_rrset=one_rr_per_rrset,
- ignore_trailing=ignore_trailing,
- raise_on_truncation=raise_on_truncation)
- return (r, received_time, from_address)
- async def udp(q, where, timeout=None, port=53, source=None, source_port=0,
- ignore_unexpected=False, one_rr_per_rrset=False,
- ignore_trailing=False, raise_on_truncation=False, sock=None,
- backend=None):
- """Return the response obtained after sending a query via UDP.
- *sock*, a ``dns.asyncbackend.DatagramSocket``, or ``None``,
- the socket to use for the query. If ``None``, the default, a
- socket is created. Note that if a socket is provided, the
- *source*, *source_port*, and *backend* are ignored.
- *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,
- the default, then dnspython will use the default backend.
- See :py:func:`dns.query.udp()` for the documentation of the other
- parameters, exceptions, and return type of this method.
- """
- wire = q.to_wire()
- (begin_time, expiration) = _compute_times(timeout)
- s = None
- # After 3.6 is no longer supported, this can use an AsyncExitStack.
- try:
- af = dns.inet.af_for_address(where)
- destination = _lltuple((where, port), af)
- if sock:
- s = sock
- else:
- if not backend:
- backend = dns.asyncbackend.get_default_backend()
- stuple = _source_tuple(af, source, source_port)
- if backend.datagram_connection_required():
- dtuple = (where, port)
- else:
- dtuple = None
- s = await backend.make_socket(af, socket.SOCK_DGRAM, 0, stuple,
- dtuple)
- await send_udp(s, wire, destination, expiration)
- (r, received_time, _) = await receive_udp(s, destination, expiration,
- ignore_unexpected,
- one_rr_per_rrset,
- q.keyring, q.mac,
- ignore_trailing,
- raise_on_truncation)
- r.time = received_time - begin_time
- if not q.is_response(r):
- raise BadResponse
- return r
- finally:
- if not sock and s:
- await s.close()
- async def udp_with_fallback(q, where, timeout=None, port=53, source=None,
- source_port=0, ignore_unexpected=False,
- one_rr_per_rrset=False, ignore_trailing=False,
- udp_sock=None, tcp_sock=None, backend=None):
- """Return the response to the query, trying UDP first and falling back
- to TCP if UDP results in a truncated response.
- *udp_sock*, a ``dns.asyncbackend.DatagramSocket``, or ``None``,
- the socket to use for the UDP query. If ``None``, the default, a
- socket is created. Note that if a socket is provided the *source*,
- *source_port*, and *backend* are ignored for the UDP query.
- *tcp_sock*, a ``dns.asyncbackend.StreamSocket``, or ``None``, the
- socket to use for the TCP query. If ``None``, the default, a
- socket is created. Note that if a socket is provided *where*,
- *source*, *source_port*, and *backend* are ignored for the TCP query.
- *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,
- the default, then dnspython will use the default backend.
- See :py:func:`dns.query.udp_with_fallback()` for the documentation
- of the other parameters, exceptions, and return type of this
- method.
- """
- try:
- response = await udp(q, where, timeout, port, source, source_port,
- ignore_unexpected, one_rr_per_rrset,
- ignore_trailing, True, udp_sock, backend)
- return (response, False)
- except dns.message.Truncated:
- response = await tcp(q, where, timeout, port, source, source_port,
- one_rr_per_rrset, ignore_trailing, tcp_sock,
- backend)
- return (response, True)
- async def send_tcp(sock, what, expiration=None):
- """Send a DNS message to the specified TCP socket.
- *sock*, a ``dns.asyncbackend.StreamSocket``.
- See :py:func:`dns.query.send_tcp()` for the documentation of the other
- parameters, exceptions, and return type of this method.
- """
- if isinstance(what, dns.message.Message):
- what = what.to_wire()
- l = len(what)
- # copying the wire into tcpmsg is inefficient, but lets us
- # avoid writev() or doing a short write that would get pushed
- # onto the net
- tcpmsg = struct.pack("!H", l) + what
- sent_time = time.time()
- await sock.sendall(tcpmsg, _timeout(expiration, sent_time))
- return (len(tcpmsg), sent_time)
- async def _read_exactly(sock, count, expiration):
- """Read the specified number of bytes from stream. Keep trying until we
- either get the desired amount, or we hit EOF.
- """
- s = b''
- while count > 0:
- n = await sock.recv(count, _timeout(expiration))
- if n == b'':
- raise EOFError
- count = count - len(n)
- s = s + n
- return s
- async def receive_tcp(sock, expiration=None, one_rr_per_rrset=False,
- keyring=None, request_mac=b'', ignore_trailing=False):
- """Read a DNS message from a TCP socket.
- *sock*, a ``dns.asyncbackend.StreamSocket``.
- See :py:func:`dns.query.receive_tcp()` for the documentation of the other
- parameters, exceptions, and return type of this method.
- """
- ldata = await _read_exactly(sock, 2, expiration)
- (l,) = struct.unpack("!H", ldata)
- wire = await _read_exactly(sock, l, expiration)
- received_time = time.time()
- r = dns.message.from_wire(wire, keyring=keyring, request_mac=request_mac,
- one_rr_per_rrset=one_rr_per_rrset,
- ignore_trailing=ignore_trailing)
- return (r, received_time)
- async def tcp(q, where, timeout=None, port=53, source=None, source_port=0,
- one_rr_per_rrset=False, ignore_trailing=False, sock=None,
- backend=None):
- """Return the response obtained after sending a query via TCP.
- *sock*, a ``dns.asyncbacket.StreamSocket``, or ``None``, the
- socket to use for the query. If ``None``, the default, a socket
- is created. Note that if a socket is provided
- *where*, *port*, *source*, *source_port*, and *backend* are ignored.
- *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,
- the default, then dnspython will use the default backend.
- See :py:func:`dns.query.tcp()` for the documentation of the other
- parameters, exceptions, and return type of this method.
- """
- wire = q.to_wire()
- (begin_time, expiration) = _compute_times(timeout)
- s = None
- # After 3.6 is no longer supported, this can use an AsyncExitStack.
- try:
- if sock:
- # Verify that the socket is connected, as if it's not connected,
- # it's not writable, and the polling in send_tcp() will time out or
- # hang forever.
- await sock.getpeername()
- s = sock
- else:
- # These are simple (address, port) pairs, not
- # family-dependent tuples you pass to lowlevel socket
- # code.
- af = dns.inet.af_for_address(where)
- stuple = _source_tuple(af, source, source_port)
- dtuple = (where, port)
- if not backend:
- backend = dns.asyncbackend.get_default_backend()
- s = await backend.make_socket(af, socket.SOCK_STREAM, 0, stuple,
- dtuple, timeout)
- await send_tcp(s, wire, expiration)
- (r, received_time) = await receive_tcp(s, expiration, one_rr_per_rrset,
- q.keyring, q.mac,
- ignore_trailing)
- r.time = received_time - begin_time
- if not q.is_response(r):
- raise BadResponse
- return r
- finally:
- if not sock and s:
- await s.close()
- async def tls(q, where, timeout=None, port=853, source=None, source_port=0,
- one_rr_per_rrset=False, ignore_trailing=False, sock=None,
- backend=None, ssl_context=None, server_hostname=None):
- """Return the response obtained after sending a query via TLS.
- *sock*, an ``asyncbackend.StreamSocket``, or ``None``, the socket
- to use for the query. If ``None``, the default, a socket is
- created. Note that if a socket is provided, it must be a
- connected SSL stream socket, and *where*, *port*,
- *source*, *source_port*, *backend*, *ssl_context*, and *server_hostname*
- are ignored.
- *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,
- the default, then dnspython will use the default backend.
- See :py:func:`dns.query.tls()` for the documentation of the other
- parameters, exceptions, and return type of this method.
- """
- # After 3.6 is no longer supported, this can use an AsyncExitStack.
- (begin_time, expiration) = _compute_times(timeout)
- if not sock:
- if ssl_context is None:
- ssl_context = ssl.create_default_context()
- if server_hostname is None:
- ssl_context.check_hostname = False
- else:
- ssl_context = None
- server_hostname = None
- af = dns.inet.af_for_address(where)
- stuple = _source_tuple(af, source, source_port)
- dtuple = (where, port)
- if not backend:
- backend = dns.asyncbackend.get_default_backend()
- s = await backend.make_socket(af, socket.SOCK_STREAM, 0, stuple,
- dtuple, timeout, ssl_context,
- server_hostname)
- else:
- s = sock
- try:
- timeout = _timeout(expiration)
- response = await tcp(q, where, timeout, port, source, source_port,
- one_rr_per_rrset, ignore_trailing, s, backend)
- end_time = time.time()
- response.time = end_time - begin_time
- return response
- finally:
- if not sock and s:
- await s.close()
- async def https(q, where, timeout=None, port=443, source=None, source_port=0,
- one_rr_per_rrset=False, ignore_trailing=False, client=None,
- path='/dns-query', post=True, verify=True):
- """Return the response obtained after sending a query via DNS-over-HTTPS.
- *client*, a ``httpx.AsyncClient``. If provided, the client to use for
- the query.
- Unlike the other dnspython async functions, a backend cannot be provided
- in this function because httpx always auto-detects the async backend.
- See :py:func:`dns.query.https()` for the documentation of the other
- parameters, exceptions, and return type of this method.
- """
- if not _have_httpx:
- raise NoDOH('httpx is not available.') # pragma: no cover
- wire = q.to_wire()
- try:
- af = dns.inet.af_for_address(where)
- except ValueError:
- af = None
- transport = None
- headers = {
- "accept": "application/dns-message"
- }
- if af is not None:
- if af == socket.AF_INET:
- url = 'https://{}:{}{}'.format(where, port, path)
- elif af == socket.AF_INET6:
- url = 'https://[{}]:{}{}'.format(where, port, path)
- else:
- url = where
- if source is not None:
- transport = httpx.AsyncHTTPTransport(local_address=source[0])
- # After 3.6 is no longer supported, this can use an AsyncExitStack
- client_to_close = None
- try:
- if not client:
- client = httpx.AsyncClient(http1=True, http2=_have_http2,
- verify=verify, transport=transport)
- client_to_close = client
- # see https://tools.ietf.org/html/rfc8484#section-4.1.1 for DoH
- # GET and POST examples
- if post:
- headers.update({
- "content-type": "application/dns-message",
- "content-length": str(len(wire))
- })
- response = await client.post(url, headers=headers, content=wire,
- timeout=timeout)
- else:
- wire = base64.urlsafe_b64encode(wire).rstrip(b"=")
- wire = wire.decode() # httpx does a repr() if we give it bytes
- response = await client.get(url, headers=headers, timeout=timeout,
- params={"dns": wire})
- finally:
- if client_to_close:
- await client.aclose()
- # see https://tools.ietf.org/html/rfc8484#section-4.2.1 for info about DoH
- # status codes
- if response.status_code < 200 or response.status_code > 299:
- raise ValueError('{} responded with status code {}'
- '\nResponse body: {}'.format(where,
- response.status_code,
- response.content))
- r = dns.message.from_wire(response.content,
- keyring=q.keyring,
- request_mac=q.request_mac,
- one_rr_per_rrset=one_rr_per_rrset,
- ignore_trailing=ignore_trailing)
- r.time = response.elapsed
- if not q.is_response(r):
- raise BadResponse
- return r
- async def inbound_xfr(where, txn_manager, query=None,
- port=53, timeout=None, lifetime=None, source=None,
- source_port=0, udp_mode=UDPMode.NEVER, backend=None):
- """Conduct an inbound transfer and apply it via a transaction from the
- txn_manager.
- *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,
- the default, then dnspython will use the default backend.
- See :py:func:`dns.query.inbound_xfr()` for the documentation of
- the other parameters, exceptions, and return type of this method.
- """
- if query is None:
- (query, serial) = dns.xfr.make_query(txn_manager)
- else:
- serial = dns.xfr.extract_serial_from_query(query)
- rdtype = query.question[0].rdtype
- is_ixfr = rdtype == dns.rdatatype.IXFR
- origin = txn_manager.from_wire_origin()
- wire = query.to_wire()
- af = dns.inet.af_for_address(where)
- stuple = _source_tuple(af, source, source_port)
- dtuple = (where, port)
- (_, expiration) = _compute_times(lifetime)
- retry = True
- while retry:
- retry = False
- if is_ixfr and udp_mode != UDPMode.NEVER:
- sock_type = socket.SOCK_DGRAM
- is_udp = True
- else:
- sock_type = socket.SOCK_STREAM
- is_udp = False
- if not backend:
- backend = dns.asyncbackend.get_default_backend()
- s = await backend.make_socket(af, sock_type, 0, stuple, dtuple,
- _timeout(expiration))
- async with s:
- if is_udp:
- await s.sendto(wire, dtuple, _timeout(expiration))
- else:
- tcpmsg = struct.pack("!H", len(wire)) + wire
- await s.sendall(tcpmsg, expiration)
- with dns.xfr.Inbound(txn_manager, rdtype, serial,
- is_udp) as inbound:
- done = False
- tsig_ctx = None
- while not done:
- (_, mexpiration) = _compute_times(timeout)
- if mexpiration is None or \
- (expiration is not None and mexpiration > expiration):
- mexpiration = expiration
- if is_udp:
- destination = _lltuple((where, port), af)
- while True:
- timeout = _timeout(mexpiration)
- (rwire, from_address) = await s.recvfrom(65535,
- timeout)
- if _matches_destination(af, from_address,
- destination, True):
- break
- else:
- ldata = await _read_exactly(s, 2, mexpiration)
- (l,) = struct.unpack("!H", ldata)
- rwire = await _read_exactly(s, l, mexpiration)
- is_ixfr = (rdtype == dns.rdatatype.IXFR)
- r = dns.message.from_wire(rwire, keyring=query.keyring,
- request_mac=query.mac, xfr=True,
- origin=origin, tsig_ctx=tsig_ctx,
- multi=(not is_udp),
- one_rr_per_rrset=is_ixfr)
- try:
- done = inbound.process_message(r)
- except dns.xfr.UseTCP:
- assert is_udp # should not happen if we used TCP!
- if udp_mode == UDPMode.ONLY:
- raise
- done = True
- retry = True
- udp_mode = UDPMode.NEVER
- continue
- tsig_ctx = r.tsig_ctx
- if not retry and query.keyring and not r.had_tsig:
- raise dns.exception.FormError("missing TSIG")
|