[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