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 - antoine.belvire@opensuse.org
+
+- 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 '
-__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]')
+
+
+@enum.unique
+class InterfaceChoice(enum.Enum):
+ Default = 1
+ All = 2
+
+
+@enum.unique
+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))
-@enum.unique
-class InterfaceChoice(enum.Enum):
- Default = 1
- All = 2
-
-
-@enum.unique
-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