Skip to content

Commit 136dce9

Browse files
authored
Merge pull request python-zeroconf#75 from stephenrauch/Fix-name-change
Fix for python-zeroconf#29
2 parents 6b67c0d + 788a48f commit 136dce9

4 files changed

Lines changed: 153 additions & 40 deletions

File tree

README.rst

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ The easiest way to install python-zeroconf is using pip::
7878
How do I use it?
7979
================
8080

81-
Here's an example:
81+
Here's an example of browsing for a service:
8282

8383
.. code-block:: python
8484
@@ -122,6 +122,21 @@ See examples directory for more.
122122
Changelog
123123
=========
124124

125+
0.17.7.dev
126+
------
127+
128+
* Better Handling of DNS Incoming Packets parsing exceptions
129+
* Many exceptions will now log a warning the first time they are seen
130+
* Catch and log sendto() errors
131+
* Fix/Implement duplicate name change
132+
* Greatly improve handling of oversized packets including:
133+
134+
- Implement name compression per RFC1035
135+
- Limit size of generated packets to 9000 bytes as per RFC6762
136+
- Better handle over sized incoming packets
137+
138+
* Increased test coverage to 94%
139+
125140
0.17.6
126141
------
127142

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
'Programming Language :: Python :: 3',
4646
'Programming Language :: Python :: 3.3',
4747
'Programming Language :: Python :: 3.4',
48+
'Programming Language :: Python :: 3.5',
4849
'Programming Language :: Python :: Implementation :: CPython',
4950
'Programming Language :: Python :: Implementation :: PyPy',
5051
],

test_zeroconf.py

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ def test_service_info_dunder(self):
9191
assert not info != info
9292
repr(info)
9393

94+
def test_dns_outgoing_repr(self):
95+
dns_outgoing = r.DNSOutgoing(r._FLAGS_QR_QUERY)
96+
repr(dns_outgoing)
97+
9498

9599
class PacketGeneration(unittest.TestCase):
96100

@@ -216,6 +220,15 @@ def test_lots_of_names(self):
216220
# instantiate a zeroconf instance
217221
zc = Zeroconf(interfaces=['127.0.0.1'])
218222

223+
# create a bunch of servers
224+
type_ = "_my-service._tcp.local."
225+
name = 'a wonderful service'
226+
server_count = 300
227+
self.generate_many_hosts(zc, type_, name, server_count)
228+
229+
# verify that name changing works
230+
self.verify_name_change(zc, type_, name, server_count)
231+
219232
# we are going to monkey patch the zeroconf send to check packet sizes
220233
old_send = zc.send
221234

@@ -233,24 +246,14 @@ def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT):
233246
# monkey patch the zeroconf send
234247
zc.send = send
235248

236-
# create a bunch of servers
237-
type_ = "_my-service._tcp.local."
238-
server_count = 300
239-
records_per_server = 2
240-
for i in range(int(server_count / 10)):
241-
self.generate_many_hosts(zc, type_, 10)
242-
sleep_count = 0
243-
while sleep_count < 100 and server_count * records_per_server > len(
244-
zc.cache.entries_with_name(type_)):
245-
sleep_count += 1
246-
time.sleep(0.01)
247-
248249
# dummy service callback
249250
def on_service_state_change(zeroconf, service_type, state_change, name):
250251
pass
251252

252-
# start a browser and run for a bit
253+
# start a browser
253254
browser = ServiceBrowser(zc, type_, [on_service_state_change])
255+
256+
# wait until the browse request packet has maxed out in size
254257
sleep_count = 0
255258
while sleep_count < 100 and \
256259
longest_packet[0] < r._MAX_MSG_ABSOLUTE - 100:
@@ -265,7 +268,7 @@ def on_service_state_change(zeroconf, service_type, state_change, name):
265268
sleep_count, longest_packet[0])
266269

267270
# now the browser has sent at least one request, verify the size
268-
assert longest_packet[0] < r._MAX_MSG_ABSOLUTE
271+
assert longest_packet[0] <= r._MAX_MSG_ABSOLUTE
269272
assert longest_packet[0] >= r._MAX_MSG_ABSOLUTE - 100
270273

271274
# mock zeroconf's logger warning() and debug()
@@ -333,11 +336,35 @@ def on_service_state_change(zeroconf, service_type, state_change, name):
333336
mocked_log_warn.stop()
334337
mocked_log_debug.stop()
335338

336-
def generate_many_hosts(self, zc, type_, number_hosts):
337-
import random
338-
for i in range(number_hosts):
339-
name = hex(random.randint(0, 1 << 32 - 1))
340-
self.generate_host(zc, 'a wonderful service' + name[1:], type_)
339+
def verify_name_change(self, zc, type_, name, number_hosts):
340+
desc = {'path': '/~paulsm/'}
341+
info_service = ServiceInfo(
342+
type_, '%s.%s' % (name, type_), socket.inet_aton("10.0.1.2"),
343+
80, 0, 0, desc, "ash-2.local.")
344+
345+
# verify name conflict
346+
self.assertRaises(
347+
r.NonUniqueNameException,
348+
zc.register_service, info_service)
349+
350+
zc.register_service(info_service, allow_name_change=True)
351+
assert info_service.name.split('.')[0] == '%s-%d' % (
352+
name, number_hosts + 1)
353+
354+
def generate_many_hosts(self, zc, type_, name, number_hosts):
355+
records_per_server = 2
356+
block_size = 25
357+
number_hosts = int(((number_hosts - 1) / block_size + 1)) * block_size
358+
for i in range(1, number_hosts + 1):
359+
next_name = name if i == 1 else '%s-%d' % (name, i)
360+
self.generate_host(zc, next_name, type_)
361+
if i % block_size == 0:
362+
sleep_count = 0
363+
while sleep_count < 40 and \
364+
i * records_per_server > len(
365+
zc.cache.entries_with_name(type_)):
366+
sleep_count += 1
367+
time.sleep(0.05)
341368

342369
@staticmethod
343370
def generate_host(zc, host_name, type_):
@@ -356,7 +383,9 @@ def generate_host(zc, host_name, type_):
356383
class Framework(unittest.TestCase):
357384

358385
def test_launch_and_close(self):
359-
rv = r.Zeroconf(interfaces=['127.0.0.1'])
386+
rv = r.Zeroconf(interfaces=r.InterfaceChoice.All)
387+
rv.close()
388+
rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default)
360389
rv.close()
361390

362391

zeroconf.py

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,12 @@ def service_type_name(type_):
217217
"""
218218
if not (type_.endswith('._tcp.local.') or type_.endswith('._udp.local.')):
219219
raise BadTypeInNameException(
220-
"Type must end with '._tcp.local.' or '._udp.local.'")
220+
"Type '%s' must end with '._tcp.local.' or '._udp.local.'" %
221+
type_)
221222

222223
if type_.startswith('.'):
223-
raise BadTypeInNameException("Type must not start with '.'")
224+
raise BadTypeInNameException(
225+
"Type '%s' must not start with '.'" % type_)
224226

225227
remaining = type_[:-len('._tcp.local.')].split('.')
226228
name = remaining.pop()
@@ -264,7 +266,7 @@ def service_type_name(type_):
264266

265267
if len(remaining) > 1:
266268
raise BadTypeInNameException(
267-
"Unexpected characters '%s.' in %s" % (
269+
"Unexpected characters '%s.' in '%s'" % (
268270
'.'.join(remaining[1:]), type_))
269271

270272
if remaining:
@@ -765,6 +767,16 @@ def __init__(self, flags, multicast=True):
765767
self.authorities = []
766768
self.additionals = []
767769

770+
def __repr__(self):
771+
return '<DNSOutgoing:{%s}>' % ', '.join([
772+
'multicast=%s' % self.multicast,
773+
'flags=%s' % self.flags,
774+
'questions=%s' % self.questions,
775+
'answers=%s' % self.answers,
776+
'authorities=%s' % self.authorities,
777+
'additionals=%s' % self.additionals,
778+
])
779+
768780
class State(enum.Enum):
769781
init = 0
770782
finished = 1
@@ -789,7 +801,41 @@ def add_authorative_answer(self, record):
789801
self.authorities.append(record)
790802

791803
def add_additional_answer(self, record):
792-
"""Adds an additional answer"""
804+
""" Adds an additional answer
805+
806+
From: RFC 6763, DNS-Based Service Discovery, February 2013
807+
808+
12. DNS Additional Record Generation
809+
810+
DNS has an efficiency feature whereby a DNS server may place
811+
additional records in the additional section of the DNS message.
812+
These additional records are records that the client did not
813+
explicitly request, but the server has reasonable grounds to expect
814+
that the client might request them shortly, so including them can
815+
save the client from having to issue additional queries.
816+
817+
This section recommends which additional records SHOULD be generated
818+
to improve network efficiency, for both Unicast and Multicast DNS-SD
819+
responses.
820+
821+
12.1. PTR Records
822+
823+
When including a DNS-SD Service Instance Enumeration or Selective
824+
Instance Enumeration (subtype) PTR record in a response packet, the
825+
server/responder SHOULD include the following additional records:
826+
827+
o The SRV record(s) named in the PTR rdata.
828+
o The TXT record(s) named in the PTR rdata.
829+
o All address records (type "A" and "AAAA") named in the SRV rdata.
830+
831+
12.2. SRV Records
832+
833+
When including an SRV record in a response packet, the
834+
server/responder SHOULD include the following additional records:
835+
836+
o All address records (type "A" and "AAAA") named in the SRV rdata.
837+
838+
"""
793839
self.additionals.append(record)
794840

795841
def pack(self, format_, value):
@@ -995,10 +1041,18 @@ def get_by_details(self, name, type_, class_):
9951041
def entries_with_name(self, name):
9961042
"""Returns a list of entries whose key matches the name."""
9971043
try:
998-
return self.cache[name]
1044+
return self.cache[name.lower()]
9991045
except KeyError:
10001046
return []
10011047

1048+
def current_entry_with_name_and_alias(self, name, alias):
1049+
now = current_time_millis()
1050+
for record in self.entries_with_name(name):
1051+
if (record.type == _TYPE_PTR and
1052+
not record.is_expired(now) and
1053+
record.alias == alias):
1054+
return record
1055+
10021056
def entries(self):
10031057
"""Returns a list of all entries"""
10041058
if not self.cache:
@@ -1689,12 +1743,12 @@ def remove_all_service_listeners(self):
16891743
for listener in [k for k in self.browsers]:
16901744
self.remove_service_listener(listener)
16911745

1692-
def register_service(self, info, ttl=_DNS_TTL):
1746+
def register_service(self, info, ttl=_DNS_TTL, allow_name_change=False):
16931747
"""Registers service information to the network with a default TTL
16941748
of 60 seconds. Zeroconf will then respond to requests for
16951749
information for that service. The name of the service may be
16961750
changed if needed to make it unique on the network."""
1697-
self.check_service(info)
1751+
self.check_service(info, allow_name_change)
16981752
self.services[info.name.lower()] = info
16991753
if info.type in self.servicetypes:
17001754
self.servicetypes[info.type] += 1
@@ -1789,28 +1843,42 @@ def unregister_all_services(self):
17891843
i += 1
17901844
next_time += _UNREGISTER_TIME
17911845

1792-
def check_service(self, info):
1846+
def check_service(self, info, allow_name_change):
17931847
"""Checks the network for a unique service name, modifying the
17941848
ServiceInfo passed in if it is not unique."""
1849+
service_name = service_type_name(info.name)
1850+
1851+
# This asserts breaks on the current subtype based tests
1852+
# need to make subtypes a first class citizen
1853+
# assert service_name == info.type
1854+
# instead try:
1855+
assert service_name == '.'.join(info.type.split('.')[-4:])
1856+
instance_name = info.name[:-len(service_name) - 1]
1857+
next_instance_number = 2
1858+
17951859
now = current_time_millis()
17961860
next_time = now
17971861
i = 0
17981862
while i < 3:
1799-
for record in self.cache.entries_with_name(info.type):
1800-
if (record.type == _TYPE_PTR and
1801-
not record.is_expired(now) and
1802-
record.alias == info.name):
1803-
if info.name.find('.') < 0:
1804-
info.name = '%s.[%s:%s].%s' % (
1805-
info.name, info.address, info.port, info.type)
1806-
1807-
self.check_service(info)
1808-
return
1863+
# check for a name conflict
1864+
while self.cache.current_entry_with_name_and_alias(
1865+
info.type, info.name):
1866+
if not allow_name_change:
18091867
raise NonUniqueNameException
1868+
1869+
# change the name and look for a conflict
1870+
info.name = '%s-%s.%s' % (
1871+
instance_name, next_instance_number, info.type)
1872+
next_instance_number += 1
1873+
service_type_name(info.name)
1874+
next_time = now
1875+
i = 0
1876+
18101877
if now < next_time:
18111878
self.wait(next_time - now)
18121879
now = current_time_millis()
18131880
continue
1881+
18141882
out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
18151883
self.debug = out
18161884
out.add_question(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
@@ -1874,7 +1942,7 @@ def handle_query(self, msg, addr, port):
18741942
# Support unicast client responses
18751943
#
18761944
if port != _MDNS_PORT:
1877-
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, False)
1945+
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, multicast=False)
18781946
for question in msg.questions:
18791947
out.add_question(question)
18801948

0 commit comments

Comments
 (0)