[Python-modules-commits] [python-zeroconf] 01/05: Import python-zeroconf_0.18.0.orig.tar.gz

Ruben Undheim rubund-guest at moszumanska.debian.org
Fri Mar 17 14:35:36 UTC 2017


This is an automated email from the git hooks/post-receive script.

rubund-guest pushed a commit to branch master
in repository python-zeroconf.

commit 8d604d4bb0cba8c3dcdc2cb60df2218d0739aed3
Author: Ruben Undheim <ruben.undheim at gmail.com>
Date:   Fri Mar 17 15:26:10 2017 +0100

    Import python-zeroconf_0.18.0.orig.tar.gz
---
 .travis.yml          |   2 +-
 README.rst           |  26 ++-
 requirements-dev.txt |   3 +-
 setup.py             |   2 +
 test_zeroconf.py     | 306 +++++++++++++++++++++++++++++++++---
 zeroconf.py          | 435 +++++++++++++++++++++++++++++++++++----------------
 6 files changed, 616 insertions(+), 158 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 94f8d95..2a48f51 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,10 @@
 language: python
 python:
-    - "2.6"
     - "2.7"
     - "3.3"
     - "3.4"
     - "3.5"
+    - "3.6"
     - "pypy"
     - "pypy3"
 matrix:
diff --git a/README.rst b/README.rst
index 314dba4..251ed0b 100644
--- a/README.rst
+++ b/README.rst
@@ -43,7 +43,7 @@ Compared to some other Zeroconf/Bonjour/Avahi Python packages, python-zeroconf:
 Python compatibility
 --------------------
 
-* CPython 2.6, 2.7, 3.3+
+* CPython 2.7, 3.3+
 * PyPy 2.2+ (possibly 1.9-2.1 as well)
 * PyPy3 2.4+
 
@@ -78,7 +78,7 @@ The easiest way to install python-zeroconf is using pip::
 How do I use it?
 ================
 
-Here's an example:
+Here's an example of browsing for a service:
 
 .. code-block:: python
 
@@ -122,6 +122,28 @@ See examples directory for more.
 Changelog
 =========
 
+0.18.0 (not released yet)
+-------------------------
+
+* Dropped Python 2.6 support
+* Improved error handling inside code executed when Zeroconf object is being closed
+
+0.17.7
+------
+
+* Better Handling of DNS Incoming Packets parsing exceptions
+* Many exceptions will now log a warning the first time they are seen
+* Catch and log sendto() errors
+* Fix/Implement duplicate name change
+* Fix overly strict name validation introduced in 0.17.6
+* Greatly improve handling of oversized packets including:
+
+  - Implement name compression per RFC1035
+  - Limit size of generated packets to 9000 bytes as per RFC6762
+  - Better handle over sized incoming packets
+
+* Increased test coverage to 95%
+
 0.17.6
 ------
 
diff --git a/requirements-dev.txt b/requirements-dev.txt
index bc92af5..83145b4 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -2,7 +2,8 @@ autopep8
 coveralls
 coverage
 enum34
-flake8
+# Upper bound because of https://github.com/PyCQA/flake8-import-order/issues/79
+flake8<3
 flake8-blind-except
 # Upper bound because of https://github.com/public/flake8-import-order/issues/42
 flake8-import-order>=0.4.0, <0.6.0
diff --git a/setup.py b/setup.py
index 75fdf2d..a8af0a9 100755
--- a/setup.py
+++ b/setup.py
@@ -45,6 +45,8 @@ setup(
         'Programming Language :: Python :: 3',
         'Programming Language :: Python :: 3.3',
         'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: Implementation :: CPython',
         'Programming Language :: Python :: Implementation :: PyPy',
     ],
diff --git a/test_zeroconf.py b/test_zeroconf.py
index e0e368a..c04c37c 100644
--- a/test_zeroconf.py
+++ b/test_zeroconf.py
@@ -38,6 +38,64 @@ def teardown_module():
     log.setLevel(original_logging_level[0])
 
 
+class TestDunder(unittest.TestCase):
+
+    def test_dns_text_repr(self):
+        # There was an issue on Python 3 that prevented DNSText's repr
+        # from working when the text was longer than 10 bytes
+        text = DNSText('irrelevant', None, 0, 0, b'12345678901')
+        repr(text)
+
+        text = DNSText('irrelevant', None, 0, 0, b'123')
+        repr(text)
+
+    def test_dns_hinfo_repr_eq(self):
+        hinfo = DNSHinfo('irrelevant', r._TYPE_HINFO, 0, 0, 'cpu', 'os')
+        assert hinfo == hinfo
+        repr(hinfo)
+
+    def test_dns_pointer_repr(self):
+        pointer = r.DNSPointer(
+            'irrelevant', r._TYPE_PTR, r._CLASS_IN, r._DNS_TTL, '123')
+        repr(pointer)
+
+    def test_dns_address_repr(self):
+        address = r.DNSAddress('irrelevant', r._TYPE_SOA, r._CLASS_IN, 1, b'a')
+        repr(address)
+
+    def test_dns_question_repr(self):
+        question = r.DNSQuestion(
+            'irrelevant', r._TYPE_SRV, r._CLASS_IN | r._CLASS_UNIQUE)
+        repr(question)
+        assert not question != question
+
+    def test_dns_service_repr(self):
+        service = r.DNSService(
+            'irrelevant', r._TYPE_SRV, r._CLASS_IN, r._DNS_TTL, 0, 0, 80, b'a')
+        repr(service)
+
+    def test_dns_record_abc(self):
+        record = r.DNSRecord('irrelevant', r._TYPE_SRV, r._CLASS_IN, r._DNS_TTL)
+        self.assertRaises(r.AbstractMethodException, record.__eq__, record)
+        self.assertRaises(r.AbstractMethodException, record.write, None)
+
+    def test_service_info_dunder(self):
+        type_ = "_test-srvc-type._tcp.local."
+        name = "xxxyyy"
+        registration_name = "%s.%s" % (name, type_)
+        info = ServiceInfo(
+            type_, registration_name,
+            socket.inet_aton("10.0.1.2"), 80, 0, 0,
+            None, "ash-2.local.")
+
+        assert not info != info
+        repr(info)
+
+    def test_dns_outgoing_repr(self):
+        dns_outgoing = r.DNSOutgoing(r._FLAGS_QR_QUERY)
+        repr(dns_outgoing)
+
+
 class PacketGeneration(unittest.TestCase):
 
     def test_parse_own_packet_simple(self):
@@ -157,11 +215,177 @@ class Names(unittest.TestCase):
         generated.add_question(question)
         r.DNSIncoming(generated.packet())
 
+    def test_lots_of_names(self):
+
+        # instantiate a zeroconf instance
+        zc = Zeroconf(interfaces=['127.0.0.1'])
+
+        # create a bunch of servers
+        type_ = "_my-service._tcp.local."
+        name = 'a wonderful service'
+        server_count = 300
+        self.generate_many_hosts(zc, type_, name, server_count)
+
+        # verify that name changing works
+        self.verify_name_change(zc, type_, name, server_count)
+
+        # we are going to monkey patch the zeroconf send to check packet sizes
+        old_send = zc.send
+
+        # needs to be a list so that we can modify it in our phony send
+        longest_packet = [0, None]
+
+        def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT):
+            """Sends an outgoing packet."""
+            packet = out.packet()
+            if longest_packet[0] < len(packet):
+                longest_packet[0] = len(packet)
+                longest_packet[1] = out
+            old_send(out, addr=addr, port=port)
+
+        # monkey patch the zeroconf send
+        zc.send = send
+
+        # dummy service callback
+        def on_service_state_change(zeroconf, service_type, state_change, name):
+            pass
+
+        # start a browser
+        browser = ServiceBrowser(zc, type_, [on_service_state_change])
+
+        # wait until the browse request packet has maxed out in size
+        sleep_count = 0
+        while sleep_count < 100 and \
+                longest_packet[0] < r._MAX_MSG_ABSOLUTE - 100:
+            sleep_count += 1
+            time.sleep(0.1)
+
+        browser.cancel()
+        time.sleep(0.5)
+
+        import zeroconf
+        zeroconf.log.debug('sleep_count %d, sized %d',
+                           sleep_count, longest_packet[0])
+
+        # now the browser has sent at least one request, verify the size
+        assert longest_packet[0] <= r._MAX_MSG_ABSOLUTE
+        assert longest_packet[0] >= r._MAX_MSG_ABSOLUTE - 100
+
+        # mock zeroconf's logger warning() and debug()
+        from mock import patch
+        patch_warn = patch('zeroconf.log.warning')
+        patch_debug = patch('zeroconf.log.debug')
+        mocked_log_warn = patch_warn.start()
+        mocked_log_debug = patch_debug.start()
+
+        # now that we have a long packet in our possession, let's verify the
+        # exception handling.
+        out = longest_packet[1]
+        out.data.append(b'\0' * 1000)
+
+        # mock the zeroconf logger and check for the correct logging backoff
+        call_counts = mocked_log_warn.call_count, mocked_log_debug.call_count
+        # try to send an oversized packet
+        zc.send(out)
+        assert mocked_log_warn.call_count == call_counts[0] + 1
+        assert mocked_log_debug.call_count == call_counts[0]
+        zc.send(out)
+        assert mocked_log_warn.call_count == call_counts[0] + 1
+        assert mocked_log_debug.call_count == call_counts[0] + 1
+
+        # force a receive of an oversized packet
+        packet = out.packet()
+        s = zc._respond_sockets[0]
+
+        # mock the zeroconf logger and check for the correct logging backoff
+        call_counts = mocked_log_warn.call_count, mocked_log_debug.call_count
+        # force receive on oversized packet
+        s.sendto(packet, 0, (r._MDNS_ADDR, r._MDNS_PORT))
+        s.sendto(packet, 0, (r._MDNS_ADDR, r._MDNS_PORT))
+        time.sleep(2.0)
+        zeroconf.log.debug('warn %d debug %d was %s',
+                           mocked_log_warn.call_count,
+                           mocked_log_debug.call_count,
+                           call_counts)
+        assert mocked_log_debug.call_count > call_counts[0]
+
+        # close our zeroconf which will close the sockets
+        zc.close()
+
+        # pop the big chunk off the end of the data and send on a closed socket
+        out.data.pop()
+        zc._GLOBAL_DONE = False
+
+        # mock the zeroconf logger and check for the correct logging backoff
+        call_counts = mocked_log_warn.call_count, mocked_log_debug.call_count
+        # send on a closed socket (force a socket error)
+        zc.send(out)
+        zeroconf.log.debug('warn %d debug %d was %s',
+                           mocked_log_warn.call_count,
+                           mocked_log_debug.call_count,
+                           call_counts)
+        assert mocked_log_warn.call_count > call_counts[0]
+        assert mocked_log_debug.call_count > call_counts[0]
+        zc.send(out)
+        zeroconf.log.debug('warn %d debug %d was %s',
+                           mocked_log_warn.call_count,
+                           mocked_log_debug.call_count,
+                           call_counts)
+        assert mocked_log_debug.call_count > call_counts[0] + 2
+
+        mocked_log_warn.stop()
+        mocked_log_debug.stop()
+
+    def verify_name_change(self, zc, type_, name, number_hosts):
+        desc = {'path': '/~paulsm/'}
+        info_service = ServiceInfo(
+            type_, '%s.%s' % (name, type_), socket.inet_aton("10.0.1.2"),
+            80, 0, 0, desc, "ash-2.local.")
+
+        # verify name conflict
+        self.assertRaises(
+            r.NonUniqueNameException,
+            zc.register_service, info_service)
+
+        zc.register_service(info_service, allow_name_change=True)
+        assert info_service.name.split('.')[0] == '%s-%d' % (
+            name, number_hosts + 1)
+
+    def generate_many_hosts(self, zc, type_, name, number_hosts):
+        records_per_server = 2
+        block_size = 25
+        number_hosts = int(((number_hosts - 1) / block_size + 1)) * block_size
+        for i in range(1, number_hosts + 1):
+            next_name = name if i == 1 else '%s-%d' % (name, i)
+            self.generate_host(zc, next_name, type_)
+            if i % block_size == 0:
+                sleep_count = 0
+                while sleep_count < 40 and \
+                        i * records_per_server > len(
+                            zc.cache.entries_with_name(type_)):
+                    sleep_count += 1
+                    time.sleep(0.05)
+
+    @staticmethod
+    def generate_host(zc, host_name, type_):
+        name = '.'.join((host_name, type_))
+        out = r.DNSOutgoing(r._FLAGS_QR_RESPONSE | r._FLAGS_AA)
+        out.add_answer_at_time(
+            r.DNSPointer(type_, r._TYPE_PTR, r._CLASS_IN,
+                         r._DNS_TTL, name), 0)
+        out.add_answer_at_time(
+            r.DNSService(type_, r._TYPE_SRV, r._CLASS_IN,
+                         r._DNS_TTL, 0, 0, 80,
+                         name), 0)
+        zc.send(out)
+
 
 class Framework(unittest.TestCase):
 
     def test_launch_and_close(self):
-        rv = r.Zeroconf()
+        rv = r.Zeroconf(interfaces=r.InterfaceChoice.All)
+        rv.close()
+        rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default)
         rv.close()
 
 
@@ -171,7 +395,7 @@ class Exceptions(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.browser = Zeroconf()
+        cls.browser = Zeroconf(interfaces=['127.0.0.1'])
 
     @classmethod
     def tearDownClass(cls):
@@ -198,17 +422,25 @@ class Exceptions(unittest.TestCase):
             '_22._udp.local.',
             '_2-2._tcp.local.',
             '_1234567890-abcde._udp.local.',
-            '._x._udp.local.',
+            '\x00._x._udp.local.',
         )
         for name in bad_names_to_try:
             self.assertRaises(
                 r.BadTypeInNameException,
                 self.browser.get_service_info, name, 'x.' + name)
 
-    def test_bad_sub_types(self):
-        bad_names_to_try = (
-            '_sub._http._tcp.local.',
+    def test_good_instance_names(self):
+        good_names_to_try = (
+            '.._x._tcp.local.',
             'x.sub._http._tcp.local.',
+            '6d86f882b90facee9170ad3439d72a4d6ee9f511._zget._http._tcp.local.'
+        )
+        for name in good_names_to_try:
+            r.service_type_name(name)
+
+    def test_bad_types(self):
+        bad_names_to_try = (
+            '._x._tcp.local.',
             'a' * 64 + '._sub._http._tcp.local.',
             'a' * 62 + u'â._sub._http._tcp.local.',
         )
@@ -216,6 +448,17 @@ class Exceptions(unittest.TestCase):
             self.assertRaises(
                 r.BadTypeInNameException, r.service_type_name, name)
 
+    def test_bad_sub_types(self):
+        bad_names_to_try = (
+            '_sub._http._tcp.local.',
+            '._sub._http._tcp.local.',
+            '\x7f._sub._http._tcp.local.',
+            '\x1f._sub._http._tcp.local.',
+        )
+        for name in bad_names_to_try:
+            self.assertRaises(
+                r.BadTypeInNameException, r.service_type_name, name)
+
     def test_good_service_names(self):
         good_names_to_try = (
             '_x._tcp.local.',
@@ -229,6 +472,31 @@ class Exceptions(unittest.TestCase):
             r.service_type_name(name)
 
 
+class TestDnsIncoming(unittest.TestCase):
+
+    def test_incoming_exception_handling(self):
+        generated = r.DNSOutgoing(0)
+        packet = generated.packet()
+        packet = packet[:8] + b'deadbeef' + packet[8:]
+        parsed = r.DNSIncoming(packet)
+        parsed = r.DNSIncoming(packet)
+        assert parsed.valid is False
+
+    def test_incoming_unknown_type(self):
+        generated = r.DNSOutgoing(0)
+        answer = r.DNSAddress('a', r._TYPE_SOA, r._CLASS_IN, 1, b'a')
+        generated.add_additional_answer(answer)
+        packet = generated.packet()
+        parsed = r.DNSIncoming(packet)
+        assert len(parsed.answers) == 0
+        assert parsed.is_query() != parsed.is_response()
+
+    def test_incoming_ipv6(self):
+        # ::TODO:: could use a test here if we add IPV6 record handling
+        # ie: _TYPE_AAAA
+        pass
+
+
 class ServiceTypesQuery(unittest.TestCase):
 
     def test_integration_with_listener(self):
@@ -246,7 +514,8 @@ class ServiceTypesQuery(unittest.TestCase):
         zeroconf_registrar.register_service(info)
 
         try:
-            service_types = ZeroconfServiceTypes.find(timeout=0.5)
+            service_types = ZeroconfServiceTypes.find(
+                interfaces=['127.0.0.1'], timeout=0.5)
             assert type_ in service_types
             service_types = ZeroconfServiceTypes.find(
                 zc=zeroconf_registrar, timeout=0.5)
@@ -272,8 +541,8 @@ class ServiceTypesQuery(unittest.TestCase):
         zeroconf_registrar.register_service(info)
 
         try:
-            service_types = ZeroconfServiceTypes.find(timeout=0.5)
-            print(service_types)
+            service_types = ZeroconfServiceTypes.find(
+                interfaces=['127.0.0.1'], timeout=0.5)
             assert discovery_type in service_types
             service_types = ZeroconfServiceTypes.find(
                 zc=zeroconf_registrar, timeout=0.5)
@@ -304,8 +573,9 @@ class ListenerTest(unittest.TestCase):
             def remove_service(self, zeroconf, type, name):
                 service_removed.set()
 
-        zeroconf_browser = Zeroconf()
-        zeroconf_browser.add_service_listener(subtype, MyListener())
+        listener = MyListener()
+        zeroconf_browser = Zeroconf(interfaces=['127.0.0.1'])
+        zeroconf_browser.add_service_listener(subtype, listener)
 
         properties = dict(
             prop_none=None,
@@ -316,7 +586,7 @@ class ListenerTest(unittest.TestCase):
             prop_false=0,
         )
 
-        zeroconf_registrar = Zeroconf()
+        zeroconf_registrar = Zeroconf(interfaces=['127.0.0.1'])
         desc = {'path': '/~paulsm/'}
         desc.update(properties)
         info_service = ServiceInfo(
@@ -354,6 +624,7 @@ class ListenerTest(unittest.TestCase):
             assert service_removed.is_set()
         finally:
             zeroconf_registrar.close()
+            zeroconf_browser.remove_service_listener(listener)
             zeroconf_browser.close()
 
 
@@ -371,10 +642,10 @@ def test_integration():
             elif state_change is ServiceStateChange.Removed:
                 service_removed.set()
 
-    zeroconf_browser = Zeroconf()
+    zeroconf_browser = Zeroconf(interfaces=['127.0.0.1'])
     browser = ServiceBrowser(zeroconf_browser, type_, [on_service_state_change])
 
-    zeroconf_registrar = Zeroconf()
+    zeroconf_registrar = Zeroconf(interfaces=['127.0.0.1'])
     desc = {'path': '/~paulsm/'}
     info = ServiceInfo(
         type_, registration_name,
@@ -391,10 +662,3 @@ def test_integration():
         zeroconf_registrar.close()
         browser.cancel()
         zeroconf_browser.close()
-
-
-def test_dnstext_repr_works():
-    # There was an issue on Python 3 that prevented DNSText's repr
-    # from working when the text was longer than 10 bytes
-    text = DNSText('irrelevant', None, 0, 0, b'12345678901')
-    repr(text)
diff --git a/zeroconf.py b/zeroconf.py
index d1211f7..1d36b7f 100644
--- a/zeroconf.py
+++ b/zeroconf.py
@@ -30,6 +30,7 @@ import re
 import select
 import socket
 import struct
+import sys
 import threading
 import time
 from functools import reduce
@@ -40,19 +41,10 @@ from six.moves import xrange
 
 __author__ = 'Paul Scott-Murphy, William McBrine'
 __maintainer__ = 'Jakub Stasiak <jakub at stasiak.at>'
-__version__ = '0.17.6'
+__version__ = '0.18.0'
 __license__ = 'LGPL'
 
 
-try:
-    NullHandler = logging.NullHandler
-except AttributeError:
-    # Python 2.6 fallback
-    class NullHandler(logging.Handler):
-
-        def emit(self, record):
-            pass
-
 __all__ = [
     "__version__",
     "Zeroconf", "ServiceInfo", "ServiceBrowser",
@@ -61,7 +53,7 @@ __all__ = [
 
 
 log = logging.getLogger(__name__)
-log.addHandler(NullHandler())
+log.addHandler(logging.NullHandler())
 
 if log.level == logging.NOTSET:
     log.setLevel(logging.WARN)
@@ -82,13 +74,13 @@ _DNS_PORT = 53
 _DNS_TTL = 60 * 60  # one hour default TTL
 
 _MAX_MSG_TYPICAL = 1460  # unused
-_MAX_MSG_ABSOLUTE = 8972
+_MAX_MSG_ABSOLUTE = 8966
 
 _FLAGS_QR_MASK = 0x8000  # query response mask
 _FLAGS_QR_QUERY = 0x0000  # query
 _FLAGS_QR_RESPONSE = 0x8000  # response
 
-_FLAGS_AA = 0x0400  # Authorative answer
+_FLAGS_AA = 0x0400  # Authoritative answer
 _FLAGS_TC = 0x0200  # Truncated
 _FLAGS_RD = 0x0100  # Recursion desired
 _FLAGS_RA = 0x8000  # Recursion available
@@ -157,6 +149,23 @@ _TYPES = {_TYPE_A: "a",
 
 _HAS_A_TO_Z = re.compile(r'[A-Za-z]')
 _HAS_ONLY_A_TO_Z_NUM_HYPHEN = re.compile(r'^[A-Za-z0-9\-]+$')
+_HAS_ASCII_CONTROL_CHARS = re.compile(r'[\x00-\x1f\x7f]')
+
+
+ at enum.unique
+class InterfaceChoice(enum.Enum):
+    Default = 1
+    All = 2
+
+
+ at enum.unique
+class ServiceStateChange(enum.Enum):
+    Added = 1
+    Removed = 2
+
+
+HOST_ONLY_NETWORK_MASK = '255.255.255.255'
+
 
 # utility functions
 
@@ -195,61 +204,79 @@ def service_type_name(type_):
 
     The instance name <Instance> and sub type <sub> may be up to 63 bytes.
 
+    The portion of the Service Instance Name is a user-
+    friendly name consisting of arbitrary Net-Unicode text [RFC5198]. It
+    MUST NOT contain ASCII control characters (byte values 0x00-0x1F and
+    0x7F) [RFC20] but otherwise is allowed to contain any characters,
+    without restriction, including spaces, uppercase, lowercase,
+    punctuation -- including dots -- accented characters, non-Roman text,
+    and anything else that may be represented using Net-Unicode.
+
     :param type_: Type, SubType or service name to validate
     :return: fully qualified service name (eg: _http._tcp.local.)
     """
     if not (type_.endswith('._tcp.local.') or type_.endswith('._udp.local.')):
         raise BadTypeInNameException(
-            "Type must end with '._tcp.local.' or '._udp.local.'")
-
-    if type_.startswith('.'):
-        raise BadTypeInNameException("Type must not start with '.'")
+            "Type '%s' must end with '._tcp.local.' or '._udp.local.'" %
+            type_)
 
     remaining = type_[:-len('._tcp.local.')].split('.')
     name = remaining.pop()
     if not name:
         raise BadTypeInNameException("No Service name found")
 
+    if len(remaining) == 1 and len(remaining[0]) == 0:
+        raise BadTypeInNameException(
+            "Type '%s' must not start with '.'" % type_)
+
     if name[0] != '_':
-        raise BadTypeInNameException("Service name must start with '_'")
+        raise BadTypeInNameException(
+            "Service name (%s) must start with '_'" % name)
 
     # remove leading underscore
     name = name[1:]
 
     if len(name) > 15:
-        raise BadTypeInNameException("Service name must be <= 15 bytes")
+        raise BadTypeInNameException(
+            "Service name (%s) must be <= 15 bytes" % name)
 
     if '--' in name:
-        raise BadTypeInNameException("Service name must not contain '--'")
+        raise BadTypeInNameException(
+            "Service name (%s) must not contain '--'" % name)
 
     if '-' in (name[0], name[-1]):
         raise BadTypeInNameException(
-            "Service name may not start or end with '-'")
+            "Service name (%s) may not start or end with '-'" % name)
 
     if not _HAS_A_TO_Z.search(name):
         raise BadTypeInNameException(
-            "Service name must contain at least one letter (eg: 'A-Z')")
+            "Service name (%s) must contain at least one letter (eg: 'A-Z')" %
+            name)
 
     if not _HAS_ONLY_A_TO_Z_NUM_HYPHEN.search(name):
         raise BadTypeInNameException(
-            "Service name must contain only these characters: "
-            "A-Z, a-z, 0-9, hyphen ('-')")
+            "Service name (%s) must contain only these characters: "
+            "A-Z, a-z, 0-9, hyphen ('-')" % name)
 
     if remaining and remaining[-1] == '_sub':
         remaining.pop()
-        if len(remaining) == 0:
+        if len(remaining) == 0 or len(remaining[0]) == 0:
             raise BadTypeInNameException(
                 "_sub requires a subtype name")
 
     if len(remaining) > 1:
-        raise BadTypeInNameException(
-            "Unexpected characters '%s.'" % '.'.join(remaining[1:]))
+        remaining = ['.'.join(remaining)]
 
     if remaining:
         length = len(remaining[0].encode('utf-8'))
         if length > 63:
             raise BadTypeInNameException("Too long: '%s'" % remaining[0])
 
+        if _HAS_ASCII_CONTROL_CHARS.search(remaining[0]):
+            raise BadTypeInNameException(
+                "Ascii control character 0x00-0x1F and 0x7F illegal in '%s'" %
+                remaining[0])
+
     return '_' + name + type_[-len('._tcp.local.'):]
 
 
@@ -260,28 +287,57 @@ class Error(Exception):
     pass
 
 
-class NonLocalNameException(Exception):
+class IncomingDecodeError(Error):
     pass
 
 
-class NonUniqueNameException(Exception):
+class NonUniqueNameException(Error):
     pass
 
 
-class NamePartTooLongException(Exception):
+class NamePartTooLongException(Error):
     pass
 
 
-class AbstractMethodException(Exception):
+class AbstractMethodException(Error):
     pass
 
 
-class BadTypeInNameException(Exception):
+class BadTypeInNameException(Error):
     pass
 
 # implementation classes
 
 
+class QuietLogger(object):
+    _seen_logs = {}
+
+    @classmethod
+    def log_exception_warning(cls, logger_data=None):
+        exc_info = sys.exc_info()
+        exc_str = str(exc_info[1])
+        if exc_str not in cls._seen_logs:
+            # log at warning level the first time this is seen
+            cls._seen_logs[exc_str] = exc_info
+            logger = log.warning
+        else:
+            logger = log.debug
+        if logger_data is not None:
+            logger(*logger_data)
+        logger('Exception occurred:', exc_info=exc_info)
+
+    @classmethod
+    def log_warning_once(cls, *args):
+        msg_str = args[0]
+        if msg_str not in cls._seen_logs:
+            cls._seen_logs[msg_str] = 0
+            logger = log.warning
+        else:
+            logger = log.debug
+        cls._seen_logs[msg_str] += 1
+        logger(*args)
+
+
 class DNSEntry(object):
 
     """A DNS entry"""
@@ -358,8 +414,8 @@ class DNSRecord(DNSEntry):
         self.created = current_time_millis()
 
     def __eq__(self, other):
-        """Tests equality as per DNSRecord"""
-        return isinstance(other, DNSRecord) and DNSEntry.__eq__(self, other)
+        """Abstract method"""
+        raise AbstractMethodException
 
     def suppressed_by(self, msg):
         """Returns true if any answer in a message can suffice for the
@@ -427,10 +483,9 @@ class DNSAddress(DNSRecord):
     def __repr__(self):
         """String representation"""
         try:
-            return socket.inet_ntoa(self.address)
-        except Exception as e:  # TODO stop catching all Exceptions
-            log.exception('Unknown error, possibly benign: %r', e)
-            return self.address
+            return str(socket.inet_ntoa(self.address))
+        except Exception:  # TODO stop catching all Exceptions
+            return str(self.address)
 
 
 class DNSHinfo(DNSRecord):
@@ -541,7 +596,7 @@ class DNSService(DNSRecord):
         return self.to_string("%s:%s" % (self.server, self.port))
 
 
-class DNSIncoming(object):
+class DNSIncoming(QuietLogger):
 
     """Object representation of an incoming DNS packet"""
 
@@ -557,10 +612,17 @@ class DNSIncoming(object):
         self.num_answers = 0
         self.num_authorities = 0
         self.num_additionals = 0
+        self.valid = False
 
-        self.read_header()
-        self.read_questions()
-        self.read_others()
+        try:
+            self.read_header()
+            self.read_questions()
+            self.read_others()
+            self.valid = True
+
+        except (IndexError, struct.error, IncomingDecodeError):
+            self.log_exception_warning((
+                'Choked at offset %d while unpacking %r', self.offset, data))
 
     def unpack(self, format_):
         length = struct.calcsize(format_)
@@ -583,9 +645,9 @@ class DNSIncoming(object):
             question = DNSQuestion(name, type_, class_)
             self.questions.append(question)
 
-    def read_int(self):
-        """Reads an integer from the packet"""
-        return self.unpack(b'!I')[0]
+    # def read_int(self):
+    #     """Reads an integer from the packet"""
+    #     return self.unpack(b'!I')[0]
 
     def read_character_string(self):
         """Reads a character string from the packet"""
@@ -675,12 +737,11 @@ class DNSIncoming(object):
                     next_ = off + 1
                 off = ((length & 0x3F) << 8) | indexbytes(self.data, off)
                 if off >= first:
-                    # TODO raise more specific exception
-                    raise Exception("Bad domain name (circular) at %s" % (off,))
+                    raise IncomingDecodeError(
+                        "Bad domain name (circular) at %s" % (off,))
                 first = off
             else:
-                # TODO raise more specific exception
-                raise Exception("Bad domain name at %s" % (off,))
+                raise IncomingDecodeError("Bad domain name at %s" % (off,))
 
         if next_ >= 0:
             self.offset = next_
@@ -702,12 +763,27 @@ class DNSOutgoing(object):
         self.names = {}
         self.data = []
         self.size = 12
+        self.state = self.State.init
 
         self.questions = []
         self.answers = []
         self.authorities = []
         self.additionals = []
 
+    def __repr__(self):
+        return '<DNSOutgoing:{%s}>' % ', '.join([
+            'multicast=%s' % self.multicast,
+            'flags=%s' % self.flags,
+            'questions=%s' % self.questions,
+            'answers=%s' % self.answers,
+            'authorities=%s' % self.authorities,
+            'additionals=%s' % self.additionals,
+        ])
+
+    class State(enum.Enum):
+        init = 0
+        finished = 1
+
     def add_question(self, record):
         """Adds a question"""
         self.questions.append(record)
@@ -718,7 +794,7 @@ class DNSOutgoing(object):
             self.add_answer_at_time(record, 0)
 
     def add_answer_at_time(self, record, now):
-        """Adds an answer if if does not expire by a certain time"""
+        """Adds an answer if it does not expire by a certain time"""
         if record is not None:
             if now == 0 or not record.is_expired(now):
                 self.answers.append((record, now))
@@ -728,7 +804,41 @@ class DNSOutgoing(object):
         self.authorities.append(record)
 
     def add_additional_answer(self, record):
-        """Adds an additional answer"""
+        """ Adds an additional answer
+
+        From: RFC 6763, DNS-Based Service Discovery, February 2013
+
+        12.  DNS Additional Record Generation
+
+           DNS has an efficiency feature whereby a DNS server may place
+           additional records in the additional section of the DNS message.
+           These additional records are records that the client did not
+           explicitly request, but the server has reasonable grounds to expect
+           that the client might request them shortly, so including them can
+           save the client from having to issue additional queries.
+
+           This section recommends which additional records SHOULD be generated
+           to improve network efficiency, for both Unicast and Multicast DNS-SD
+           responses.
+
+        12.1.  PTR Records
+
+           When including a DNS-SD Service Instance Enumeration or Selective
+           Instance Enumeration (subtype) PTR record in a response packet, the
+           server/responder SHOULD include the following additional records:
+
+           o  The SRV record(s) named in the PTR rdata.
+           o  The TXT record(s) named in the PTR rdata.
+           o  All address records (type "A" and "AAAA") named in the SRV rdata.
+
+        12.2.  SRV Records
+
+           When including an SRV record in a response packet, the
+           server/responder SHOULD include the following additional records:
+
+           o  All address records (type "A" and "AAAA") named in the SRV rdata.
+
+        """
         self.additionals.append(record)
 
     def pack(self, format_, value):
@@ -776,28 +886,49 @@ class DNSOutgoing(object):
         self.write_string(value)
 
     def write_name(self, name):
-        """Writes a domain name to the packet"""
+        """
+        Write names to packet
+
+        18.14. Name Compression
+
+        When generating Multicast DNS messages, implementations SHOULD use
+        name compression wherever possible to compress the names of resource
+        records, by replacing some or all of the resource record name with a
+        compact two-byte reference to an appearance of that data somewhere
+        earlier in the message [RFC1035].
+        """
 
-        if name in self.names:
-            # Find existing instance of this name in packet
-            #
-            index = self.names[name]
+        # split name into each label
+        parts = name.split('.')
+        if not parts[-1]:
+            parts.pop()
 
-            # An index was found, so write a pointer to it
-            #
+        # construct each suffix
+        name_suffices = ['.'.join(parts[i:]) for i in range(len(parts))]
+
+        # look for an existing name or suffix
+        for count, sub_name in enumerate(name_suffices):
+            if sub_name in self.names:
+                break
+        else:
+            count += 1
+
+        # note the new names we are saving into the packet
+        for suffix in name_suffices[:count]:
+            self.names[suffix] = self.size + len(name) - len(suffix) - 1
+
+        # write the new names out.
+        for part in parts[:count]:
+            self.write_utf(part)
+
+        # if we wrote part of the name, create a pointer to the rest
+        if count != len(name_suffices):
+            # Found substring in packet, create pointer
+            index = self.names[name_suffices[count]]
             self.write_byte((index >> 8) | 0xC0)
             self.write_byte(index & 0xFF)
         else:
-            # No record of this name already, so write it
-            # out as normal, recording the location of the name
-            # for future pointers to it.
-            #
-            self.names[name] = self.size
-            parts = name.split('.')
-            if parts[-1] == '':
-                parts = parts[:-1]
-            for part in parts:
-                self.write_utf(part)
+            # this is the end of a name
             self.write_byte(0)
 
     def write_question(self, question):
@@ -809,6 +940,10 @@ class DNSOutgoing(object):
     def write_record(self, record, now):
         """Writes a record (answer, authoritative answer, additional) to
         the packet"""
+        if self.state == self.State.finished:
+            return 1
+
+        start_data_length, start_size = len(self.data), self.size
         self.write_name(record.name)
         self.write_short(record.type)
         if record.unique and self.multicast:
... 340 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-zeroconf.git



More information about the Python-modules-commits mailing list