codekingpro/portable-devtools
114k
1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license2 3# Copyright (C) 2003-2017 Nominum, Inc.4#5# Permission to use, copy, modify, and distribute this software and its6# documentation for any purpose with or without fee is hereby granted,7# provided that the above copyright notice and this permission notice8# appear in all copies.9#10# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT16# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.17 18"""Talk to a DNS server."""19 20import base6421import contextlib22import socket23import struct24import time25from typing import Any, Dict, Optional, Tuple, Union26 27import dns.asyncbackend28import dns.exception29import dns.inet30import dns.message31import dns.name32import dns.quic33import dns.rcode34import dns.rdataclass35import dns.rdatatype36import dns.transaction37from dns._asyncbackend import NullContext38from dns.query import (39 BadResponse,40 NoDOH,41 NoDOQ,42 UDPMode,43 _compute_times,44 _make_dot_ssl_context,45 _matches_destination,46 _remaining,47 have_doh,48 ssl,49)50 51if have_doh:52 import httpx53 54# for brevity55_lltuple = dns.inet.low_level_address_tuple56 57 58def _source_tuple(af, address, port):59 # Make a high level source tuple, or return None if address and port60 # are both None61 if address or port:62 if address is None:63 if af == socket.AF_INET:64 address = "0.0.0.0"65 elif af == socket.AF_INET6:66 address = "::"67 else:68 raise NotImplementedError(f"unknown address family {af}")69 return (address, port)70 else:71 return None72 73 74def _timeout(expiration, now=None):75 if expiration is not None:76 if not now:77 now = time.time()78 return max(expiration - now, 0)79 else:80 return None81 82 83async def send_udp(84 sock: dns.asyncbackend.DatagramSocket,85 what: Union[dns.message.Message, bytes],86 destination: Any,87 expiration: Optional[float] = None,88) -> Tuple[int, float]:89 """Send a DNS message to the specified UDP socket.90 91 *sock*, a ``dns.asyncbackend.DatagramSocket``.92 93 *what*, a ``bytes`` or ``dns.message.Message``, the message to send.94 95 *destination*, a destination tuple appropriate for the address family96 of the socket, specifying where to send the query.97 98 *expiration*, a ``float`` or ``None``, the absolute time at which99 a timeout exception should be raised. If ``None``, no timeout will100 occur. The expiration value is meaningless for the asyncio backend, as101 asyncio's transport sendto() never blocks.102 103 Returns an ``(int, float)`` tuple of bytes sent and the sent time.104 """105 106 if isinstance(what, dns.message.Message):107 what = what.to_wire()108 sent_time = time.time()109 n = await sock.sendto(what, destination, _timeout(expiration, sent_time))110 return (n, sent_time)111 112 113async def receive_udp(114 sock: dns.asyncbackend.DatagramSocket,115 destination: Optional[Any] = None,116 expiration: Optional[float] = None,117 ignore_unexpected: bool = False,118 one_rr_per_rrset: bool = False,119 keyring: Optional[Dict[dns.name.Name, dns.tsig.Key]] = None,120 request_mac: Optional[bytes] = b"",121 ignore_trailing: bool = False,122 raise_on_truncation: bool = False,123 ignore_errors: bool = False,124 query: Optional[dns.message.Message] = None,125) -> Any:126 """Read a DNS message from a UDP socket.127 128 *sock*, a ``dns.asyncbackend.DatagramSocket``.129 130 See :py:func:`dns.query.receive_udp()` for the documentation of the other131 parameters, and exceptions.132 133 Returns a ``(dns.message.Message, float, tuple)`` tuple of the received message, the134 received time, and the address where the message arrived from.135 """136 137 wire = b""138 while True:139 (wire, from_address) = await sock.recvfrom(65535, _timeout(expiration))140 if not _matches_destination(141 sock.family, from_address, destination, ignore_unexpected142 ):143 continue144 received_time = time.time()145 try:146 r = dns.message.from_wire(147 wire,148 keyring=keyring,149 request_mac=request_mac,150 one_rr_per_rrset=one_rr_per_rrset,151 ignore_trailing=ignore_trailing,152 raise_on_truncation=raise_on_truncation,153 )154 except dns.message.Truncated as e:155 # See the comment in query.py for details.156 if (157 ignore_errors158 and query is not None159 and not query.is_response(e.message())160 ):161 continue162 else:163 raise164 except Exception:165 if ignore_errors:166 continue167 else:168 raise169 if ignore_errors and query is not None and not query.is_response(r):170 continue171 return (r, received_time, from_address)172 173 174async def udp(175 q: dns.message.Message,176 where: str,177 timeout: Optional[float] = None,178 port: int = 53,179 source: Optional[str] = None,180 source_port: int = 0,181 ignore_unexpected: bool = False,182 one_rr_per_rrset: bool = False,183 ignore_trailing: bool = False,184 raise_on_truncation: bool = False,185 sock: Optional[dns.asyncbackend.DatagramSocket] = None,186 backend: Optional[dns.asyncbackend.Backend] = None,187 ignore_errors: bool = False,188) -> dns.message.Message:189 """Return the response obtained after sending a query via UDP.190 191 *sock*, a ``dns.asyncbackend.DatagramSocket``, or ``None``,192 the socket to use for the query. If ``None``, the default, a193 socket is created. Note that if a socket is provided, the194 *source*, *source_port*, and *backend* are ignored.195 196 *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,197 the default, then dnspython will use the default backend.198 199 See :py:func:`dns.query.udp()` for the documentation of the other200 parameters, exceptions, and return type of this method.201 """202 wire = q.to_wire()203 (begin_time, expiration) = _compute_times(timeout)204 af = dns.inet.af_for_address(where)205 destination = _lltuple((where, port), af)206 if sock:207 cm: contextlib.AbstractAsyncContextManager = NullContext(sock)208 else:209 if not backend:210 backend = dns.asyncbackend.get_default_backend()211 stuple = _source_tuple(af, source, source_port)212 if backend.datagram_connection_required():213 dtuple = (where, port)214 else:215 dtuple = None216 cm = await backend.make_socket(af, socket.SOCK_DGRAM, 0, stuple, dtuple)217 async with cm as s:218 await send_udp(s, wire, destination, expiration)219 (r, received_time, _) = await receive_udp(220 s,221 destination,222 expiration,223 ignore_unexpected,224 one_rr_per_rrset,225 q.keyring,226 q.mac,227 ignore_trailing,228 raise_on_truncation,229 ignore_errors,230 q,231 )232 r.time = received_time - begin_time233 # We don't need to check q.is_response() if we are in ignore_errors mode234 # as receive_udp() will have checked it.235 if not (ignore_errors or q.is_response(r)):236 raise BadResponse237 return r238 239 240async def udp_with_fallback(241 q: dns.message.Message,242 where: str,243 timeout: Optional[float] = None,244 port: int = 53,245 source: Optional[str] = None,246 source_port: int = 0,247 ignore_unexpected: bool = False,248 one_rr_per_rrset: bool = False,249 ignore_trailing: bool = False,250 udp_sock: Optional[dns.asyncbackend.DatagramSocket] = None,251 tcp_sock: Optional[dns.asyncbackend.StreamSocket] = None,252 backend: Optional[dns.asyncbackend.Backend] = None,253 ignore_errors: bool = False,254) -> Tuple[dns.message.Message, bool]:255 """Return the response to the query, trying UDP first and falling back256 to TCP if UDP results in a truncated response.257 258 *udp_sock*, a ``dns.asyncbackend.DatagramSocket``, or ``None``,259 the socket to use for the UDP query. If ``None``, the default, a260 socket is created. Note that if a socket is provided the *source*,261 *source_port*, and *backend* are ignored for the UDP query.262 263 *tcp_sock*, a ``dns.asyncbackend.StreamSocket``, or ``None``, the264 socket to use for the TCP query. If ``None``, the default, a265 socket is created. Note that if a socket is provided *where*,266 *source*, *source_port*, and *backend* are ignored for the TCP query.267 268 *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,269 the default, then dnspython will use the default backend.270 271 See :py:func:`dns.query.udp_with_fallback()` for the documentation272 of the other parameters, exceptions, and return type of this273 method.274 """275 try:276 response = await udp(277 q,278 where,279 timeout,280 port,281 source,282 source_port,283 ignore_unexpected,284 one_rr_per_rrset,285 ignore_trailing,286 True,287 udp_sock,288 backend,289 ignore_errors,290 )291 return (response, False)292 except dns.message.Truncated:293 response = await tcp(294 q,295 where,296 timeout,297 port,298 source,299 source_port,300 one_rr_per_rrset,301 ignore_trailing,302 tcp_sock,303 backend,304 )305 return (response, True)306 307 308async def send_tcp(309 sock: dns.asyncbackend.StreamSocket,310 what: Union[dns.message.Message, bytes],311 expiration: Optional[float] = None,312) -> Tuple[int, float]:313 """Send a DNS message to the specified TCP socket.314 315 *sock*, a ``dns.asyncbackend.StreamSocket``.316 317 See :py:func:`dns.query.send_tcp()` for the documentation of the other318 parameters, exceptions, and return type of this method.319 """320 321 if isinstance(what, dns.message.Message):322 tcpmsg = what.to_wire(prepend_length=True)323 else:324 # copying the wire into tcpmsg is inefficient, but lets us325 # avoid writev() or doing a short write that would get pushed326 # onto the net327 tcpmsg = len(what).to_bytes(2, "big") + what328 sent_time = time.time()329 await sock.sendall(tcpmsg, _timeout(expiration, sent_time))330 return (len(tcpmsg), sent_time)331 332 333async def _read_exactly(sock, count, expiration):334 """Read the specified number of bytes from stream. Keep trying until we335 either get the desired amount, or we hit EOF.336 """337 s = b""338 while count > 0:339 n = await sock.recv(count, _timeout(expiration))340 if n == b"":341 raise EOFError342 count = count - len(n)343 s = s + n344 return s345 346 347async def receive_tcp(348 sock: dns.asyncbackend.StreamSocket,349 expiration: Optional[float] = None,350 one_rr_per_rrset: bool = False,351 keyring: Optional[Dict[dns.name.Name, dns.tsig.Key]] = None,352 request_mac: Optional[bytes] = b"",353 ignore_trailing: bool = False,354) -> Tuple[dns.message.Message, float]:355 """Read a DNS message from a TCP socket.356 357 *sock*, a ``dns.asyncbackend.StreamSocket``.358 359 See :py:func:`dns.query.receive_tcp()` for the documentation of the other360 parameters, exceptions, and return type of this method.361 """362 363 ldata = await _read_exactly(sock, 2, expiration)364 (l,) = struct.unpack("!H", ldata)365 wire = await _read_exactly(sock, l, expiration)366 received_time = time.time()367 r = dns.message.from_wire(368 wire,369 keyring=keyring,370 request_mac=request_mac,371 one_rr_per_rrset=one_rr_per_rrset,372 ignore_trailing=ignore_trailing,373 )374 return (r, received_time)375 376 377async def tcp(378 q: dns.message.Message,379 where: str,380 timeout: Optional[float] = None,381 port: int = 53,382 source: Optional[str] = None,383 source_port: int = 0,384 one_rr_per_rrset: bool = False,385 ignore_trailing: bool = False,386 sock: Optional[dns.asyncbackend.StreamSocket] = None,387 backend: Optional[dns.asyncbackend.Backend] = None,388) -> dns.message.Message:389 """Return the response obtained after sending a query via TCP.390 391 *sock*, a ``dns.asyncbacket.StreamSocket``, or ``None``, the392 socket to use for the query. If ``None``, the default, a socket393 is created. Note that if a socket is provided394 *where*, *port*, *source*, *source_port*, and *backend* are ignored.395 396 *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,397 the default, then dnspython will use the default backend.398 399 See :py:func:`dns.query.tcp()` for the documentation of the other400 parameters, exceptions, and return type of this method.401 """402 403 wire = q.to_wire()404 (begin_time, expiration) = _compute_times(timeout)405 if sock:406 # Verify that the socket is connected, as if it's not connected,407 # it's not writable, and the polling in send_tcp() will time out or408 # hang forever.409 await sock.getpeername()410 cm: contextlib.AbstractAsyncContextManager = NullContext(sock)411 else:412 # These are simple (address, port) pairs, not family-dependent tuples413 # you pass to low-level socket code.414 af = dns.inet.af_for_address(where)415 stuple = _source_tuple(af, source, source_port)416 dtuple = (where, port)417 if not backend:418 backend = dns.asyncbackend.get_default_backend()419 cm = await backend.make_socket(420 af, socket.SOCK_STREAM, 0, stuple, dtuple, timeout421 )422 async with cm as s:423 await send_tcp(s, wire, expiration)424 (r, received_time) = await receive_tcp(425 s, expiration, one_rr_per_rrset, q.keyring, q.mac, ignore_trailing426 )427 r.time = received_time - begin_time428 if not q.is_response(r):429 raise BadResponse430 return r431 432 433async def tls(434 q: dns.message.Message,435 where: str,436 timeout: Optional[float] = None,437 port: int = 853,438 source: Optional[str] = None,439 source_port: int = 0,440 one_rr_per_rrset: bool = False,441 ignore_trailing: bool = False,442 sock: Optional[dns.asyncbackend.StreamSocket] = None,443 backend: Optional[dns.asyncbackend.Backend] = None,444 ssl_context: Optional[ssl.SSLContext] = None,445 server_hostname: Optional[str] = None,446 verify: Union[bool, str] = True,447) -> dns.message.Message:448 """Return the response obtained after sending a query via TLS.449 450 *sock*, an ``asyncbackend.StreamSocket``, or ``None``, the socket451 to use for the query. If ``None``, the default, a socket is452 created. Note that if a socket is provided, it must be a453 connected SSL stream socket, and *where*, *port*,454 *source*, *source_port*, *backend*, *ssl_context*, and *server_hostname*455 are ignored.456 457 *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,458 the default, then dnspython will use the default backend.459 460 See :py:func:`dns.query.tls()` for the documentation of the other461 parameters, exceptions, and return type of this method.462 """463 (begin_time, expiration) = _compute_times(timeout)464 if sock:465 cm: contextlib.AbstractAsyncContextManager = NullContext(sock)466 else:467 if ssl_context is None:468 ssl_context = _make_dot_ssl_context(server_hostname, verify)469 af = dns.inet.af_for_address(where)470 stuple = _source_tuple(af, source, source_port)471 dtuple = (where, port)472 if not backend:473 backend = dns.asyncbackend.get_default_backend()474 cm = await backend.make_socket(475 af,476 socket.SOCK_STREAM,477 0,478 stuple,479 dtuple,480 timeout,481 ssl_context,482 server_hostname,483 )484 async with cm as s:485 timeout = _timeout(expiration)486 response = await tcp(487 q,488 where,489 timeout,490 port,491 source,492 source_port,493 one_rr_per_rrset,494 ignore_trailing,495 s,496 backend,497 )498 end_time = time.time()499 response.time = end_time - begin_time500 return response501 502 503async def https(504 q: dns.message.Message,505 where: str,506 timeout: Optional[float] = None,507 port: int = 443,508 source: Optional[str] = None,509 source_port: int = 0, # pylint: disable=W0613510 one_rr_per_rrset: bool = False,511 ignore_trailing: bool = False,512 client: Optional["httpx.AsyncClient"] = None,513 path: str = "/dns-query",514 post: bool = True,515 verify: Union[bool, str] = True,516 bootstrap_address: Optional[str] = None,517 resolver: Optional["dns.asyncresolver.Resolver"] = None,518 family: Optional[int] = socket.AF_UNSPEC,519) -> dns.message.Message:520 """Return the response obtained after sending a query via DNS-over-HTTPS.521 522 *client*, a ``httpx.AsyncClient``. If provided, the client to use for523 the query.524 525 Unlike the other dnspython async functions, a backend cannot be provided526 in this function because httpx always auto-detects the async backend.527 528 See :py:func:`dns.query.https()` for the documentation of the other529 parameters, exceptions, and return type of this method.530 """531 532 if not have_doh:533 raise NoDOH # pragma: no cover534 if client and not isinstance(client, httpx.AsyncClient):535 raise ValueError("session parameter must be an httpx.AsyncClient")536 537 wire = q.to_wire()538 try:539 af = dns.inet.af_for_address(where)540 except ValueError:541 af = None542 transport = None543 headers = {"accept": "application/dns-message"}544 if af is not None and dns.inet.is_address(where):545 if af == socket.AF_INET:546 url = "https://{}:{}{}".format(where, port, path)547 elif af == socket.AF_INET6:548 url = "https://[{}]:{}{}".format(where, port, path)549 else:550 url = where551 552 backend = dns.asyncbackend.get_default_backend()553 554 if source is None:555 local_address = None556 local_port = 0557 else:558 local_address = source559 local_port = source_port560 transport = backend.get_transport_class()(561 local_address=local_address,562 http1=True,563 http2=True,564 verify=verify,565 local_port=local_port,566 bootstrap_address=bootstrap_address,567 resolver=resolver,568 family=family,569 )570 571 if client:572 cm: contextlib.AbstractAsyncContextManager = NullContext(client)573 else:574 cm = httpx.AsyncClient(575 http1=True, http2=True, verify=verify, transport=transport576 )577 578 async with cm as the_client:579 # see https://tools.ietf.org/html/rfc8484#section-4.1.1 for DoH580 # GET and POST examples581 if post:582 headers.update(583 {584 "content-type": "application/dns-message",585 "content-length": str(len(wire)),586 }587 )588 response = await backend.wait_for(589 the_client.post(url, headers=headers, content=wire), timeout590 )591 else:592 wire = base64.urlsafe_b64encode(wire).rstrip(b"=")593 twire = wire.decode() # httpx does a repr() if we give it bytes594 response = await backend.wait_for(595 the_client.get(url, headers=headers, params={"dns": twire}), timeout596 )597 598 # see https://tools.ietf.org/html/rfc8484#section-4.2.1 for info about DoH599 # status codes600 if response.status_code < 200 or response.status_code > 299:601 raise ValueError(602 "{} responded with status code {}"603 "\nResponse body: {!r}".format(604 where, response.status_code, response.content605 )606 )607 r = dns.message.from_wire(608 response.content,609 keyring=q.keyring,610 request_mac=q.request_mac,611 one_rr_per_rrset=one_rr_per_rrset,612 ignore_trailing=ignore_trailing,613 )614 r.time = response.elapsed.total_seconds()615 if not q.is_response(r):616 raise BadResponse617 return r618 619 620async def inbound_xfr(621 where: str,622 txn_manager: dns.transaction.TransactionManager,623 query: Optional[dns.message.Message] = None,624 port: int = 53,625 timeout: Optional[float] = None,626 lifetime: Optional[float] = None,627 source: Optional[str] = None,628 source_port: int = 0,629 udp_mode: UDPMode = UDPMode.NEVER,630 backend: Optional[dns.asyncbackend.Backend] = None,631) -> None:632 """Conduct an inbound transfer and apply it via a transaction from the633 txn_manager.634 635 *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,636 the default, then dnspython will use the default backend.637 638 See :py:func:`dns.query.inbound_xfr()` for the documentation of639 the other parameters, exceptions, and return type of this method.640 """641 if query is None:642 (query, serial) = dns.xfr.make_query(txn_manager)643 else:644 serial = dns.xfr.extract_serial_from_query(query)645 rdtype = query.question[0].rdtype646 is_ixfr = rdtype == dns.rdatatype.IXFR647 origin = txn_manager.from_wire_origin()648 wire = query.to_wire()649 af = dns.inet.af_for_address(where)650 stuple = _source_tuple(af, source, source_port)651 dtuple = (where, port)652 (_, expiration) = _compute_times(lifetime)653 retry = True654 while retry:655 retry = False656 if is_ixfr and udp_mode != UDPMode.NEVER:657 sock_type = socket.SOCK_DGRAM658 is_udp = True659 else:660 sock_type = socket.SOCK_STREAM661 is_udp = False662 if not backend:663 backend = dns.asyncbackend.get_default_backend()664 s = await backend.make_socket(665 af, sock_type, 0, stuple, dtuple, _timeout(expiration)666 )667 async with s:668 if is_udp:669 await s.sendto(wire, dtuple, _timeout(expiration))670 else:671 tcpmsg = struct.pack("!H", len(wire)) + wire672 await s.sendall(tcpmsg, expiration)673 with dns.xfr.Inbound(txn_manager, rdtype, serial, is_udp) as inbound:674 done = False675 tsig_ctx = None676 while not done:677 (_, mexpiration) = _compute_times(timeout)678 if mexpiration is None or (679 expiration is not None and mexpiration > expiration680 ):681 mexpiration = expiration682 if is_udp:683 destination = _lltuple((where, port), af)684 while True:685 timeout = _timeout(mexpiration)686 (rwire, from_address) = await s.recvfrom(65535, timeout)687 if _matches_destination(688 af, from_address, destination, True689 ):690 break691 else:692 ldata = await _read_exactly(s, 2, mexpiration)693 (l,) = struct.unpack("!H", ldata)694 rwire = await _read_exactly(s, l, mexpiration)695 is_ixfr = rdtype == dns.rdatatype.IXFR696 r = dns.message.from_wire(697 rwire,698 keyring=query.keyring,699 request_mac=query.mac,700 xfr=True,701 origin=origin,702 tsig_ctx=tsig_ctx,703 multi=(not is_udp),704 one_rr_per_rrset=is_ixfr,705 )706 try:707 done = inbound.process_message(r)708 except dns.xfr.UseTCP:709 assert is_udp # should not happen if we used TCP!710 if udp_mode == UDPMode.ONLY:711 raise712 done = True713 retry = True714 udp_mode = UDPMode.NEVER715 continue716 tsig_ctx = r.tsig_ctx717 if not retry and query.keyring and not r.had_tsig:718 raise dns.exception.FormError("missing TSIG")719 720 721async def quic(722 q: dns.message.Message,723 where: str,724 timeout: Optional[float] = None,725 port: int = 853,726 source: Optional[str] = None,727 source_port: int = 0,728 one_rr_per_rrset: bool = False,729 ignore_trailing: bool = False,730 connection: Optional[dns.quic.AsyncQuicConnection] = None,731 verify: Union[bool, str] = True,732 backend: Optional[dns.asyncbackend.Backend] = None,733 server_hostname: Optional[str] = None,734) -> dns.message.Message:735 """Return the response obtained after sending an asynchronous query via736 DNS-over-QUIC.737 738 *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,739 the default, then dnspython will use the default backend.740 741 See :py:func:`dns.query.quic()` for the documentation of the other742 parameters, exceptions, and return type of this method.743 """744 745 if not dns.quic.have_quic:746 raise NoDOQ("DNS-over-QUIC is not available.") # pragma: no cover747 748 q.id = 0749 wire = q.to_wire()750 the_connection: dns.quic.AsyncQuicConnection751 if connection:752 cfactory = dns.quic.null_factory753 mfactory = dns.quic.null_factory754 the_connection = connection755 else:756 (cfactory, mfactory) = dns.quic.factories_for_backend(backend)757 758 async with cfactory() as context:759 async with mfactory(760 context, verify_mode=verify, server_name=server_hostname761 ) as the_manager:762 if not connection:763 the_connection = the_manager.connect(where, port, source, source_port)764 (start, expiration) = _compute_times(timeout)765 stream = await the_connection.make_stream(timeout)766 async with stream:767 await stream.send(wire, True)768 wire = await stream.receive(_remaining(expiration))769 finish = time.time()770 r = dns.message.from_wire(771 wire,772 keyring=q.keyring,773 request_mac=q.request_mac,774 one_rr_per_rrset=one_rr_per_rrset,775 ignore_trailing=ignore_trailing,776 )777 r.time = max(finish - start, 0.0)778 if not q.is_response(r):779 raise BadResponse780 return r781 