Hello community,

here is the log from the commit of package python-zeroconf for openSUSE:Factory 
checked in at 2017-02-19 01:03:56
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-zeroconf (Old)
 and      /work/SRC/openSUSE:Factory/.python-zeroconf.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-zeroconf"

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-zeroconf/python-zeroconf.changes  
2017-02-05 16:25:43.682482791 +0100
+++ /work/SRC/openSUSE:Factory/.python-zeroconf.new/python-zeroconf.changes     
2017-02-19 01:03:57.478920065 +0100
@@ -1,0 +2,19 @@
+Sat Feb 18 10:46:56 UTC 2017 - [email protected]
+
+- Update to 0.18.0:
+  * Dropped Python 2.6 support
+  * Improved error handling inside code executed when Zeroconf
+    object is being closed
+- Changes from 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%
+
+-------------------------------------------------------------------

Old:
----
  zeroconf-0.17.6.tar.gz

New:
----
  zeroconf-0.18.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-zeroconf.spec ++++++
--- /var/tmp/diff_new_pack.1Luybv/_old  2017-02-19 01:03:57.938855442 +0100
+++ /var/tmp/diff_new_pack.1Luybv/_new  2017-02-19 01:03:57.942854880 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-zeroconf
 #
-# Copyright (c) 2016 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany.
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -17,7 +17,7 @@
 
 
 Name:           python-zeroconf
-Version:        0.17.6
+Version:        0.18.0
 Release:        0
 Summary:        Pure Python Multicast DNS Service Discovery Library 
(Bonjour/Avahi compatible)
 License:        LGPL-2.0

++++++ zeroconf-0.17.6.tar.gz -> zeroconf-0.18.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/zeroconf-0.17.6/PKG-INFO new/zeroconf-0.18.0/PKG-INFO
--- old/zeroconf-0.17.6/PKG-INFO        2016-07-08 01:21:53.000000000 +0200
+++ new/zeroconf-0.18.0/PKG-INFO        2017-02-03 15:16:32.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: zeroconf
-Version: 0.17.6
+Version: 0.18.0
 Summary: Pure Python Multicast DNS Service Discovery Library (Bonjour/Avahi 
compatible)
 Home-page: https://github.com/jstasiak/python-zeroconf
 Author: Paul Scott-Murphy, William McBrine, Jakub Stasiak
@@ -51,7 +51,7 @@
         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+
         
@@ -86,7 +86,7 @@
         How do I use it?
         ================
         
-        Here's an example:
+        Here's an example of browsing for a service:
         
         .. code-block:: python
         
@@ -130,6 +130,28 @@
         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
         ------
         
@@ -333,5 +355,7 @@
 Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3.3
 Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: Implementation :: CPython
 Classifier: Programming Language :: Python :: Implementation :: PyPy
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/zeroconf-0.17.6/README.rst 
new/zeroconf-0.18.0/README.rst
--- old/zeroconf-0.17.6/README.rst      2016-07-08 01:18:17.000000000 +0200
+++ new/zeroconf-0.18.0/README.rst      2017-02-03 15:13:25.000000000 +0100
@@ -43,7 +43,7 @@
 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 @@
 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 @@
 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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/zeroconf-0.17.6/setup.cfg 
new/zeroconf-0.18.0/setup.cfg
--- old/zeroconf-0.17.6/setup.cfg       2016-07-08 01:21:53.000000000 +0200
+++ new/zeroconf-0.18.0/setup.cfg       2017-02-03 15:16:32.000000000 +0100
@@ -10,5 +10,4 @@
 [egg_info]
 tag_build = 
 tag_date = 0
-tag_svn_revision = 0
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/zeroconf-0.17.6/setup.py new/zeroconf-0.18.0/setup.py
--- old/zeroconf-0.17.6/setup.py        2016-03-14 21:51:50.000000000 +0100
+++ new/zeroconf-0.18.0/setup.py        2017-02-03 15:10:02.000000000 +0100
@@ -45,6 +45,8 @@
         '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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/zeroconf-0.17.6/zeroconf.egg-info/PKG-INFO 
new/zeroconf-0.18.0/zeroconf.egg-info/PKG-INFO
--- old/zeroconf-0.17.6/zeroconf.egg-info/PKG-INFO      2016-07-08 
01:21:53.000000000 +0200
+++ new/zeroconf-0.18.0/zeroconf.egg-info/PKG-INFO      2017-02-03 
15:16:32.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: zeroconf
-Version: 0.17.6
+Version: 0.18.0
 Summary: Pure Python Multicast DNS Service Discovery Library (Bonjour/Avahi 
compatible)
 Home-page: https://github.com/jstasiak/python-zeroconf
 Author: Paul Scott-Murphy, William McBrine, Jakub Stasiak
@@ -51,7 +51,7 @@
         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+
         
@@ -86,7 +86,7 @@
         How do I use it?
         ================
         
-        Here's an example:
+        Here's an example of browsing for a service:
         
         .. code-block:: python
         
@@ -130,6 +130,28 @@
         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
         ------
         
@@ -333,5 +355,7 @@
 Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3.3
 Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: Implementation :: CPython
 Classifier: Programming Language :: Python :: Implementation :: PyPy
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/zeroconf-0.17.6/zeroconf.py 
new/zeroconf-0.18.0/zeroconf.py
--- old/zeroconf-0.17.6/zeroconf.py     2016-07-08 01:18:17.000000000 +0200
+++ new/zeroconf-0.18.0/zeroconf.py     2017-02-03 15:14:15.000000000 +0100
@@ -30,6 +30,7 @@
 import select
 import socket
 import struct
+import sys
 import threading
 import time
 from functools import reduce
@@ -40,19 +41,10 @@
 
 __author__ = 'Paul Scott-Murphy, William McBrine'
 __maintainer__ = 'Jakub Stasiak <[email protected]>'
-__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 @@
 
 
 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_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 @@
 
 _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]')
+
+
[email protected]
+class InterfaceChoice(enum.Enum):
+    Default = 1
+    All = 2
+
+
[email protected]
+class ServiceStateChange(enum.Enum):
+    Added = 1
+    Removed = 2
+
+
+HOST_ONLY_NETWORK_MASK = '255.255.255.255'
+
 
 # utility functions
 
@@ -195,61 +204,79 @@
 
     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 @@
     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 @@
         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 @@
     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 @@
         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 @@
         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 @@
             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 @@
                     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 @@
         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 @@
             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 @@
         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 @@
         self.write_string(value)
 
     def write_name(self, name):
-        """Writes a domain name to the packet"""
+        """
+        Write names to packet
+
+        18.14. Name Compression
 
-        if name in self.names:
-            # Find existing instance of this name in packet
-            #
-            index = self.names[name]
+        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].
+        """
 
-            # An index was found, so write a pointer to it
-            #
+        # split name into each label
+        parts = name.split('.')
+        if not parts[-1]:
+            parts.pop()
+
+        # 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 @@
     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:
@@ -820,34 +955,47 @@
         else:
             self.write_int(record.get_remaining_ttl(now))
         index = len(self.data)
+
         # Adjust size for the short we will write before this record
-        #
         self.size += 2
         record.write(self)
         self.size -= 2
 
-        length = len(b''.join(self.data[index:]))
-        self.insert_short(index, length)  # Here is the short we adjusted for
+        length = sum((len(d) for d in self.data[index:]))
+        # Here is the short we adjusted for
+        self.insert_short(index, length)
+
+        # if we go over, then rollback and quit
+        if self.size > _MAX_MSG_ABSOLUTE:
+            while len(self.data) > start_data_length:
+                self.data.pop()
+            self.size = start_size
+            self.state = self.State.finished
+            return 1
+        return 0
 
     def packet(self):
         """Returns a string containing the packet's bytes
 
         No further parts should be added to the packet once this
         is done."""
-        if not self.finished:
-            self.finished = True
+
+        overrun_answers, overrun_authorities, overrun_additionals = 0, 0, 0
+
+        if self.state != self.State.finished:
             for question in self.questions:
                 self.write_question(question)
             for answer, time_ in self.answers:
-                self.write_record(answer, time_)
+                overrun_answers += self.write_record(answer, time_)
             for authority in self.authorities:
-                self.write_record(authority, 0)
+                overrun_authorities += self.write_record(authority, 0)
             for additional in self.additionals:
-                self.write_record(additional, 0)
+                overrun_additionals += self.write_record(additional, 0)
+            self.state = self.State.finished
 
-            self.insert_short(0, len(self.additionals))
-            self.insert_short(0, len(self.authorities))
-            self.insert_short(0, len(self.answers))
+            self.insert_short(0, len(self.additionals) - overrun_additionals)
+            self.insert_short(0, len(self.authorities) - overrun_authorities)
+            self.insert_short(0, len(self.answers) - overrun_answers)
             self.insert_short(0, len(self.questions))
             self.insert_short(0, self.flags)
             if self.multicast:
@@ -896,10 +1044,18 @@
     def entries_with_name(self, name):
         """Returns a list of entries whose key matches the name."""
         try:
-            return self.cache[name]
+            return self.cache[name.lower()]
         except KeyError:
             return []
 
+    def current_entry_with_name_and_alias(self, name, alias):
+        now = current_time_millis()
+        for record in self.entries_with_name(name):
+            if (record.type == _TYPE_PTR and
+                    not record.is_expired(now) and
+                    record.alias == alias):
+                return record
+
     def entries(self):
         """Returns a list of all entries"""
         if not self.cache:
@@ -950,10 +1106,10 @@
                             if reader:
                                 reader.handle_read(socket_)
 
-                except socket.error as e:
+                except (select.error, socket.error) as e:
                     # If the socket was closed by another thread, during
                     # shutdown, ignore it and exit
-                    if e.errno != socket.EBADF or not self.zc.done:
+                    if e.args[0] != socket.EBADF or not self.zc.done:
                         raise
 
     def add_reader(self, reader, socket_):
@@ -967,7 +1123,7 @@
             self.condition.notify()
 
 
-class Listener(object):
+class Listener(QuietLogger):
 
     """A Listener is used by this module to listen on the multicast
     group to which DNS messages are sent, allowing the implementation
@@ -981,22 +1137,30 @@
         self.data = None
 
     def handle_read(self, socket_):
-        data, (addr, port) = socket_.recvfrom(_MAX_MSG_ABSOLUTE)
-        log.debug('Received %r from %r:%r', data, addr, port)
+        try:
+            data, (addr, port) = socket_.recvfrom(_MAX_MSG_ABSOLUTE)
+        except Exception:
+            self.log_exception_warning()
+            return
+
+        log.debug('Received from %r:%r: %r ', addr, port, data)
 
         self.data = data
         msg = DNSIncoming(data)
-        if msg.is_query():
+        if not msg.valid:
+            pass
+
+        elif msg.is_query():
             # Always multicast responses
-            #
             if port == _MDNS_PORT:
                 self.zc.handle_query(msg, _MDNS_ADDR, _MDNS_PORT)
+
             # If it's not a multicast query, reply via unicast
             # and multicast
-            #
             elif port == _DNS_PORT:
                 self.zc.handle_query(msg, addr, port)
                 self.zc.handle_query(msg, _MDNS_ADDR, _MDNS_PORT)
+
         else:
             self.zc.handle_response(msg)
 
@@ -1154,13 +1318,13 @@
             if self.zc.done or self.done:
                 return
             now = current_time_millis()
-
             if self.next_time <= now:
                 out = DNSOutgoing(_FLAGS_QR_QUERY)
                 out.add_question(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
                 for record in self.services.values():
                     if not record.is_expired(now):
                         out.add_answer_at_time(record, now)
+
                 self.zc.send(out)
                 self.next_time = now + self.delay
                 self.delay = min(20 * 1000, self.delay * 2)
@@ -1358,9 +1522,7 @@
 
     def __eq__(self, other):
         """Tests equality of service name"""
-        if isinstance(other, ServiceInfo):
-            return other.name == self.name
-        return False
+        return isinstance(other, ServiceInfo) and other.name == self.name
 
     def __ne__(self, other):
         """Non-equality test"""
@@ -1394,7 +1556,7 @@
         pass
 
     @classmethod
-    def find(cls, zc=None, timeout=5):
+    def find(cls, zc=None, timeout=5, interfaces=InterfaceChoice.All):
         """
         Return all of the advertised services on any local networks.
 
@@ -1403,7 +1565,7 @@
         :param timeout: seconds to wait for any responses
         :return: tuple of service type strings
         """
-        local_zc = zc or Zeroconf()
+        local_zc = zc or Zeroconf(interfaces=interfaces)
         listener = cls()
         browser = ServiceBrowser(
             local_zc, '_services._dns-sd._udp.local.', listener=listener)
@@ -1420,21 +1582,6 @@
         return tuple(sorted(listener.found_services))
 
 
[email protected]
-class InterfaceChoice(enum.Enum):
-    Default = 1
-    All = 2
-
-
[email protected]
-class ServiceStateChange(enum.Enum):
-    Added = 1
-    Removed = 2
-
-
-HOST_ONLY_NETWORK_MASK = '255.255.255.255'
-
-
 def get_all_addresses(address_family):
     return list(set(
         addr['addr']
@@ -1491,7 +1638,7 @@
     return e.args[0]
 
 
-class Zeroconf(object):
+class Zeroconf(QuietLogger):
 
     """Implementation of Zeroconf Multicast DNS Service Discovery
 
@@ -1580,7 +1727,6 @@
         info = ServiceInfo(type_, name)
         if info.request(self, timeout):
             return info
-        return None
 
     def add_service_listener(self, type_, listener):
         """Adds a listener for a particular service type.  This object
@@ -1600,12 +1746,12 @@
         for listener in [k for k in self.browsers]:
             self.remove_service_listener(listener)
 
-    def register_service(self, info, ttl=_DNS_TTL):
+    def register_service(self, info, ttl=_DNS_TTL, allow_name_change=False):
         """Registers service information to the network with a default TTL
         of 60 seconds.  Zeroconf will then respond to requests for
         information for that service.  The name of the service may be
         changed if needed to make it unique on the network."""
-        self.check_service(info)
+        self.check_service(info, allow_name_change)
         self.services[info.name.lower()] = info
         if info.type in self.servicetypes:
             self.servicetypes[info.type] += 1
@@ -1700,28 +1846,42 @@
                 i += 1
                 next_time += _UNREGISTER_TIME
 
-    def check_service(self, info):
+    def check_service(self, info, allow_name_change):
         """Checks the network for a unique service name, modifying the
         ServiceInfo passed in if it is not unique."""
+
+        # This is kind of funky because of the subtype based tests
+        # need to make subtypes a first class citizen
+        service_name = service_type_name(info.name)
+        if not info.type.endswith(service_name):
+            raise BadTypeInNameException
+
+        instance_name = info.name[:-len(service_name) - 1]
+        next_instance_number = 2
+
         now = current_time_millis()
         next_time = now
         i = 0
         while i < 3:
-            for record in self.cache.entries_with_name(info.type):
-                if (record.type == _TYPE_PTR and
-                        not record.is_expired(now) and
-                        record.alias == info.name):
-                    if info.name.find('.') < 0:
-                        info.name = '%s.[%s:%s].%s' % (
-                            info.name, info.address, info.port, info.type)
-
-                        self.check_service(info)
-                        return
+            # check for a name conflict
+            while self.cache.current_entry_with_name_and_alias(
+                    info.type, info.name):
+                if not allow_name_change:
                     raise NonUniqueNameException
+
+                # change the name and look for a conflict
+                info.name = '%s-%s.%s' % (
+                    instance_name, next_instance_number, info.type)
+                next_instance_number += 1
+                service_type_name(info.name)
+                next_time = now
+                i = 0
+
             if now < next_time:
                 self.wait(next_time - now)
                 now = current_time_millis()
                 continue
+
             out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
             self.debug = out
             out.add_question(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
@@ -1785,7 +1945,7 @@
         # Support unicast client responses
         #
         if port != _MDNS_PORT:
-            out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, False)
+            out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, multicast=False)
             for question in msg.questions:
                 out.add_question(question)
 
@@ -1836,8 +1996,8 @@
                         out.add_additional_answer(DNSAddress(
                             service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE,
                             _DNS_TTL, service.address))
-                except Exception as e:  # TODO stop catching all Exceptions
-                    log.exception('Unknown error, possibly benign: %r', e)
+                except Exception:  # TODO stop catching all Exceptions
+                    self.log_exception_warning()
 
         if out is not None and out.answers:
             out.id = msg.id
@@ -1846,15 +2006,24 @@
     def send(self, out, addr=_MDNS_ADDR, port=_MDNS_PORT):
         """Sends an outgoing packet."""
         packet = out.packet()
-        log.debug('Sending %r as %r...', out, packet)
+        if len(packet) > _MAX_MSG_ABSOLUTE:
+            self.log_warning_once("Dropping %r over-sized packet (%d bytes) 
%r",
+                                  out, len(packet), packet)
+            return
+        log.debug('Sending %r (%d bytes) as %r...', out, len(packet), packet)
         for s in self._respond_sockets:
             if self._GLOBAL_DONE:
                 return
-            bytes_sent = s.sendto(packet, 0, (addr, port))
-            if bytes_sent != len(packet):
-                raise Error(
-                    'Should not happen, sent %d out of %d bytes' % (
-                        bytes_sent, len(packet)))
+            try:
+                bytes_sent = s.sendto(packet, 0, (addr, port))
+            except Exception:   # TODO stop catching all Exceptions
+                # on send errors, log the exception and keep going
+                self.log_exception_warning()
+            else:
+                if bytes_sent != len(packet):
+                    self.log_warning_once(
+                        '!!! sent %d out of %d bytes to %r' % (
+                            bytes_sent, len(packet)), s)
 
     def close(self):
         """Ends the background threads, and prevent this instance from


Reply via email to