##// END OF EJS Templates
zeroconf: use str instead of bytes when indexing `globals()`...
Matt Harbison -
r52893:c76c1c94 default
parent child Browse files
Show More
@@ -1,1903 +1,1903
1 from __future__ import annotations
1 from __future__ import annotations
2
2
3 """ Multicast DNS Service Discovery for Python, v0.12
3 """ Multicast DNS Service Discovery for Python, v0.12
4 Copyright (C) 2003, Paul Scott-Murphy
4 Copyright (C) 2003, Paul Scott-Murphy
5
5
6 This module provides a framework for the use of DNS Service Discovery
6 This module provides a framework for the use of DNS Service Discovery
7 using IP multicast. It has been tested against the JRendezvous
7 using IP multicast. It has been tested against the JRendezvous
8 implementation from <a href="http://strangeberry.com">StrangeBerry</a>,
8 implementation from <a href="http://strangeberry.com">StrangeBerry</a>,
9 and against the mDNSResponder from Mac OS X 10.3.8.
9 and against the mDNSResponder from Mac OS X 10.3.8.
10
10
11 This library is free software; you can redistribute it and/or
11 This library is free software; you can redistribute it and/or
12 modify it under the terms of the GNU Lesser General Public
12 modify it under the terms of the GNU Lesser General Public
13 License as published by the Free Software Foundation; either
13 License as published by the Free Software Foundation; either
14 version 2.1 of the License, or (at your option) any later version.
14 version 2.1 of the License, or (at your option) any later version.
15
15
16 This library is distributed in the hope that it will be useful,
16 This library is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 Lesser General Public License for more details.
19 Lesser General Public License for more details.
20
20
21 You should have received a copy of the GNU Lesser General Public
21 You should have received a copy of the GNU Lesser General Public
22 License along with this library; if not, see
22 License along with this library; if not, see
23 <http://www.gnu.org/licenses/>.
23 <http://www.gnu.org/licenses/>.
24
24
25 """
25 """
26
26
27 """0.12 update - allow selection of binding interface
27 """0.12 update - allow selection of binding interface
28 typo fix - Thanks A. M. Kuchlingi
28 typo fix - Thanks A. M. Kuchlingi
29 removed all use of word 'Rendezvous' - this is an API change"""
29 removed all use of word 'Rendezvous' - this is an API change"""
30
30
31 """0.11 update - correction to comments for addListener method
31 """0.11 update - correction to comments for addListener method
32 support for new record types seen from OS X
32 support for new record types seen from OS X
33 - IPv6 address
33 - IPv6 address
34 - hostinfo
34 - hostinfo
35 ignore unknown DNS record types
35 ignore unknown DNS record types
36 fixes to name decoding
36 fixes to name decoding
37 works alongside other processes using port 5353 (e.g. Mac OS X)
37 works alongside other processes using port 5353 (e.g. Mac OS X)
38 tested against Mac OS X 10.3.2's mDNSResponder
38 tested against Mac OS X 10.3.2's mDNSResponder
39 corrections to removal of list entries for service browser"""
39 corrections to removal of list entries for service browser"""
40
40
41 """0.10 update - Jonathon Paisley contributed these corrections:
41 """0.10 update - Jonathon Paisley contributed these corrections:
42 always multicast replies, even when query is unicast
42 always multicast replies, even when query is unicast
43 correct a pointer encoding problem
43 correct a pointer encoding problem
44 can now write records in any order
44 can now write records in any order
45 traceback shown on failure
45 traceback shown on failure
46 better TXT record parsing
46 better TXT record parsing
47 server is now separate from name
47 server is now separate from name
48 can cancel a service browser
48 can cancel a service browser
49
49
50 modified some unit tests to accommodate these changes"""
50 modified some unit tests to accommodate these changes"""
51
51
52 """0.09 update - remove all records on service unregistration
52 """0.09 update - remove all records on service unregistration
53 fix DOS security problem with readName"""
53 fix DOS security problem with readName"""
54
54
55 """0.08 update - changed licensing to LGPL"""
55 """0.08 update - changed licensing to LGPL"""
56
56
57 """0.07 update - faster shutdown on engine
57 """0.07 update - faster shutdown on engine
58 pointer encoding of outgoing names
58 pointer encoding of outgoing names
59 ServiceBrowser now works
59 ServiceBrowser now works
60 new unit tests"""
60 new unit tests"""
61
61
62 """0.06 update - small improvements with unit tests
62 """0.06 update - small improvements with unit tests
63 added defined exception types
63 added defined exception types
64 new style objects
64 new style objects
65 fixed hostname/interface problem
65 fixed hostname/interface problem
66 fixed socket timeout problem
66 fixed socket timeout problem
67 fixed addServiceListener() typo bug
67 fixed addServiceListener() typo bug
68 using select() for socket reads
68 using select() for socket reads
69 tested on Debian unstable with Python 2.2.2"""
69 tested on Debian unstable with Python 2.2.2"""
70
70
71 """0.05 update - ensure case insensitivity on domain names
71 """0.05 update - ensure case insensitivity on domain names
72 support for unicast DNS queries"""
72 support for unicast DNS queries"""
73
73
74 """0.04 update - added some unit tests
74 """0.04 update - added some unit tests
75 added __ne__ adjuncts where required
75 added __ne__ adjuncts where required
76 ensure names end in '.local.'
76 ensure names end in '.local.'
77 timeout on receiving socket for clean shutdown"""
77 timeout on receiving socket for clean shutdown"""
78
78
79 __author__ = b"Paul Scott-Murphy"
79 __author__ = b"Paul Scott-Murphy"
80 __email__ = b"paul at scott dash murphy dot com"
80 __email__ = b"paul at scott dash murphy dot com"
81 __version__ = b"0.12"
81 __version__ = b"0.12"
82
82
83 import errno
83 import errno
84 import itertools
84 import itertools
85 import select
85 import select
86 import socket
86 import socket
87 import struct
87 import struct
88 import threading
88 import threading
89 import time
89 import time
90 import traceback
90 import traceback
91
91
92 from mercurial import pycompat
92 from mercurial import pycompat
93
93
94 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
94 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
95
95
96 # hook for threads
96 # hook for threads
97
97
98 globals()[b'_GLOBAL_DONE'] = 0
98 globals()['_GLOBAL_DONE'] = 0
99
99
100 # Some timing constants
100 # Some timing constants
101
101
102 _UNREGISTER_TIME = 125
102 _UNREGISTER_TIME = 125
103 _CHECK_TIME = 175
103 _CHECK_TIME = 175
104 _REGISTER_TIME = 225
104 _REGISTER_TIME = 225
105 _LISTENER_TIME = 200
105 _LISTENER_TIME = 200
106 _BROWSER_TIME = 500
106 _BROWSER_TIME = 500
107
107
108 # Some DNS constants
108 # Some DNS constants
109
109
110 _MDNS_ADDR = r'224.0.0.251'
110 _MDNS_ADDR = r'224.0.0.251'
111 _MDNS_PORT = 5353
111 _MDNS_PORT = 5353
112 _DNS_PORT = 53
112 _DNS_PORT = 53
113 _DNS_TTL = 60 * 60 # one hour default TTL
113 _DNS_TTL = 60 * 60 # one hour default TTL
114
114
115 _MAX_MSG_TYPICAL = 1460 # unused
115 _MAX_MSG_TYPICAL = 1460 # unused
116 _MAX_MSG_ABSOLUTE = 8972
116 _MAX_MSG_ABSOLUTE = 8972
117
117
118 _FLAGS_QR_MASK = 0x8000 # query response mask
118 _FLAGS_QR_MASK = 0x8000 # query response mask
119 _FLAGS_QR_QUERY = 0x0000 # query
119 _FLAGS_QR_QUERY = 0x0000 # query
120 _FLAGS_QR_RESPONSE = 0x8000 # response
120 _FLAGS_QR_RESPONSE = 0x8000 # response
121
121
122 _FLAGS_AA = 0x0400 # Authoritative answer
122 _FLAGS_AA = 0x0400 # Authoritative answer
123 _FLAGS_TC = 0x0200 # Truncated
123 _FLAGS_TC = 0x0200 # Truncated
124 _FLAGS_RD = 0x0100 # Recursion desired
124 _FLAGS_RD = 0x0100 # Recursion desired
125 _FLAGS_RA = 0x8000 # Recursion available
125 _FLAGS_RA = 0x8000 # Recursion available
126
126
127 _FLAGS_Z = 0x0040 # Zero
127 _FLAGS_Z = 0x0040 # Zero
128 _FLAGS_AD = 0x0020 # Authentic data
128 _FLAGS_AD = 0x0020 # Authentic data
129 _FLAGS_CD = 0x0010 # Checking disabled
129 _FLAGS_CD = 0x0010 # Checking disabled
130
130
131 _CLASS_IN = 1
131 _CLASS_IN = 1
132 _CLASS_CS = 2
132 _CLASS_CS = 2
133 _CLASS_CH = 3
133 _CLASS_CH = 3
134 _CLASS_HS = 4
134 _CLASS_HS = 4
135 _CLASS_NONE = 254
135 _CLASS_NONE = 254
136 _CLASS_ANY = 255
136 _CLASS_ANY = 255
137 _CLASS_MASK = 0x7FFF
137 _CLASS_MASK = 0x7FFF
138 _CLASS_UNIQUE = 0x8000
138 _CLASS_UNIQUE = 0x8000
139
139
140 _TYPE_A = 1
140 _TYPE_A = 1
141 _TYPE_NS = 2
141 _TYPE_NS = 2
142 _TYPE_MD = 3
142 _TYPE_MD = 3
143 _TYPE_MF = 4
143 _TYPE_MF = 4
144 _TYPE_CNAME = 5
144 _TYPE_CNAME = 5
145 _TYPE_SOA = 6
145 _TYPE_SOA = 6
146 _TYPE_MB = 7
146 _TYPE_MB = 7
147 _TYPE_MG = 8
147 _TYPE_MG = 8
148 _TYPE_MR = 9
148 _TYPE_MR = 9
149 _TYPE_NULL = 10
149 _TYPE_NULL = 10
150 _TYPE_WKS = 11
150 _TYPE_WKS = 11
151 _TYPE_PTR = 12
151 _TYPE_PTR = 12
152 _TYPE_HINFO = 13
152 _TYPE_HINFO = 13
153 _TYPE_MINFO = 14
153 _TYPE_MINFO = 14
154 _TYPE_MX = 15
154 _TYPE_MX = 15
155 _TYPE_TXT = 16
155 _TYPE_TXT = 16
156 _TYPE_AAAA = 28
156 _TYPE_AAAA = 28
157 _TYPE_SRV = 33
157 _TYPE_SRV = 33
158 _TYPE_ANY = 255
158 _TYPE_ANY = 255
159
159
160 # Mapping constants to names
160 # Mapping constants to names
161
161
162 _CLASSES = {
162 _CLASSES = {
163 _CLASS_IN: b"in",
163 _CLASS_IN: b"in",
164 _CLASS_CS: b"cs",
164 _CLASS_CS: b"cs",
165 _CLASS_CH: b"ch",
165 _CLASS_CH: b"ch",
166 _CLASS_HS: b"hs",
166 _CLASS_HS: b"hs",
167 _CLASS_NONE: b"none",
167 _CLASS_NONE: b"none",
168 _CLASS_ANY: b"any",
168 _CLASS_ANY: b"any",
169 }
169 }
170
170
171 _TYPES = {
171 _TYPES = {
172 _TYPE_A: b"a",
172 _TYPE_A: b"a",
173 _TYPE_NS: b"ns",
173 _TYPE_NS: b"ns",
174 _TYPE_MD: b"md",
174 _TYPE_MD: b"md",
175 _TYPE_MF: b"mf",
175 _TYPE_MF: b"mf",
176 _TYPE_CNAME: b"cname",
176 _TYPE_CNAME: b"cname",
177 _TYPE_SOA: b"soa",
177 _TYPE_SOA: b"soa",
178 _TYPE_MB: b"mb",
178 _TYPE_MB: b"mb",
179 _TYPE_MG: b"mg",
179 _TYPE_MG: b"mg",
180 _TYPE_MR: b"mr",
180 _TYPE_MR: b"mr",
181 _TYPE_NULL: b"null",
181 _TYPE_NULL: b"null",
182 _TYPE_WKS: b"wks",
182 _TYPE_WKS: b"wks",
183 _TYPE_PTR: b"ptr",
183 _TYPE_PTR: b"ptr",
184 _TYPE_HINFO: b"hinfo",
184 _TYPE_HINFO: b"hinfo",
185 _TYPE_MINFO: b"minfo",
185 _TYPE_MINFO: b"minfo",
186 _TYPE_MX: b"mx",
186 _TYPE_MX: b"mx",
187 _TYPE_TXT: b"txt",
187 _TYPE_TXT: b"txt",
188 _TYPE_AAAA: b"quada",
188 _TYPE_AAAA: b"quada",
189 _TYPE_SRV: b"srv",
189 _TYPE_SRV: b"srv",
190 _TYPE_ANY: b"any",
190 _TYPE_ANY: b"any",
191 }
191 }
192
192
193 # utility functions
193 # utility functions
194
194
195
195
196 def currentTimeMillis():
196 def currentTimeMillis():
197 """Current system time in milliseconds"""
197 """Current system time in milliseconds"""
198 return time.time() * 1000
198 return time.time() * 1000
199
199
200
200
201 # Exceptions
201 # Exceptions
202
202
203
203
204 class NonLocalNameException(Exception):
204 class NonLocalNameException(Exception):
205 pass
205 pass
206
206
207
207
208 class NonUniqueNameException(Exception):
208 class NonUniqueNameException(Exception):
209 pass
209 pass
210
210
211
211
212 class NamePartTooLongException(Exception):
212 class NamePartTooLongException(Exception):
213 pass
213 pass
214
214
215
215
216 class AbstractMethodException(Exception):
216 class AbstractMethodException(Exception):
217 pass
217 pass
218
218
219
219
220 class BadTypeInNameException(Exception):
220 class BadTypeInNameException(Exception):
221 pass
221 pass
222
222
223
223
224 class BadDomainName(Exception):
224 class BadDomainName(Exception):
225 def __init__(self, pos):
225 def __init__(self, pos):
226 Exception.__init__(self, b"at position %s" % pos)
226 Exception.__init__(self, b"at position %s" % pos)
227
227
228
228
229 class BadDomainNameCircular(BadDomainName):
229 class BadDomainNameCircular(BadDomainName):
230 pass
230 pass
231
231
232
232
233 # implementation classes
233 # implementation classes
234
234
235 _SOL_IP = socket.SOL_IP
235 _SOL_IP = socket.SOL_IP
236
236
237 if pycompat.iswindows:
237 if pycompat.iswindows:
238 # XXX: Not sure if there are newer versions of python where this would fail,
238 # XXX: Not sure if there are newer versions of python where this would fail,
239 # but apparently socket.SOL_IP used to be 0, and socket.IPPROTO_IP is 0, so
239 # but apparently socket.SOL_IP used to be 0, and socket.IPPROTO_IP is 0, so
240 # this would work with older versions of python.
240 # this would work with older versions of python.
241 #
241 #
242 # https://github.com/python/cpython/issues/101960
242 # https://github.com/python/cpython/issues/101960
243 _SOL_IP = socket.IPPROTO_IP
243 _SOL_IP = socket.IPPROTO_IP
244
244
245
245
246 class DNSEntry:
246 class DNSEntry:
247 """A DNS entry"""
247 """A DNS entry"""
248
248
249 def __init__(self, name, type, clazz):
249 def __init__(self, name, type, clazz):
250 self.key = name.lower()
250 self.key = name.lower()
251 self.name = name
251 self.name = name
252 self.type = type
252 self.type = type
253 self.clazz = clazz & _CLASS_MASK
253 self.clazz = clazz & _CLASS_MASK
254 self.unique = (clazz & _CLASS_UNIQUE) != 0
254 self.unique = (clazz & _CLASS_UNIQUE) != 0
255
255
256 def __eq__(self, other):
256 def __eq__(self, other):
257 """Equality test on name, type, and class"""
257 """Equality test on name, type, and class"""
258 if isinstance(other, DNSEntry):
258 if isinstance(other, DNSEntry):
259 return (
259 return (
260 self.name == other.name
260 self.name == other.name
261 and self.type == other.type
261 and self.type == other.type
262 and self.clazz == other.clazz
262 and self.clazz == other.clazz
263 )
263 )
264 return 0
264 return 0
265
265
266 def __ne__(self, other):
266 def __ne__(self, other):
267 """Non-equality test"""
267 """Non-equality test"""
268 return not self.__eq__(other)
268 return not self.__eq__(other)
269
269
270 def getClazz(self, clazz):
270 def getClazz(self, clazz):
271 """Class accessor"""
271 """Class accessor"""
272 try:
272 try:
273 return _CLASSES[clazz]
273 return _CLASSES[clazz]
274 except KeyError:
274 except KeyError:
275 return b"?(%s)" % clazz
275 return b"?(%s)" % clazz
276
276
277 def getType(self, type):
277 def getType(self, type):
278 """Type accessor"""
278 """Type accessor"""
279 try:
279 try:
280 return _TYPES[type]
280 return _TYPES[type]
281 except KeyError:
281 except KeyError:
282 return b"?(%s)" % type
282 return b"?(%s)" % type
283
283
284 def toString(self, hdr, other):
284 def toString(self, hdr, other):
285 """String representation with additional information"""
285 """String representation with additional information"""
286 result = b"%s[%s,%s" % (
286 result = b"%s[%s,%s" % (
287 hdr,
287 hdr,
288 self.getType(self.type),
288 self.getType(self.type),
289 self.getClazz(self.clazz),
289 self.getClazz(self.clazz),
290 )
290 )
291 if self.unique:
291 if self.unique:
292 result += b"-unique,"
292 result += b"-unique,"
293 else:
293 else:
294 result += b","
294 result += b","
295 result += self.name
295 result += self.name
296 if other is not None:
296 if other is not None:
297 result += b",%s]" % other
297 result += b",%s]" % other
298 else:
298 else:
299 result += b"]"
299 result += b"]"
300 return result
300 return result
301
301
302
302
303 class DNSQuestion(DNSEntry):
303 class DNSQuestion(DNSEntry):
304 """A DNS question entry"""
304 """A DNS question entry"""
305
305
306 def __init__(self, name, type, clazz):
306 def __init__(self, name, type, clazz):
307 if isinstance(name, str):
307 if isinstance(name, str):
308 name = name.encode('ascii')
308 name = name.encode('ascii')
309 if not name.endswith(b".local."):
309 if not name.endswith(b".local."):
310 raise NonLocalNameException(name)
310 raise NonLocalNameException(name)
311 DNSEntry.__init__(self, name, type, clazz)
311 DNSEntry.__init__(self, name, type, clazz)
312
312
313 def answeredBy(self, rec):
313 def answeredBy(self, rec):
314 """Returns true if the question is answered by the record"""
314 """Returns true if the question is answered by the record"""
315 return (
315 return (
316 self.clazz == rec.clazz
316 self.clazz == rec.clazz
317 and (self.type == rec.type or self.type == _TYPE_ANY)
317 and (self.type == rec.type or self.type == _TYPE_ANY)
318 and self.name == rec.name
318 and self.name == rec.name
319 )
319 )
320
320
321 def __repr__(self):
321 def __repr__(self):
322 """String representation"""
322 """String representation"""
323 return DNSEntry.toString(self, b"question", None)
323 return DNSEntry.toString(self, b"question", None)
324
324
325
325
326 class DNSRecord(DNSEntry):
326 class DNSRecord(DNSEntry):
327 """A DNS record - like a DNS entry, but has a TTL"""
327 """A DNS record - like a DNS entry, but has a TTL"""
328
328
329 def __init__(self, name, type, clazz, ttl):
329 def __init__(self, name, type, clazz, ttl):
330 DNSEntry.__init__(self, name, type, clazz)
330 DNSEntry.__init__(self, name, type, clazz)
331 self.ttl = ttl
331 self.ttl = ttl
332 self.created = currentTimeMillis()
332 self.created = currentTimeMillis()
333
333
334 def __eq__(self, other):
334 def __eq__(self, other):
335 """Tests equality as per DNSRecord"""
335 """Tests equality as per DNSRecord"""
336 if isinstance(other, DNSRecord):
336 if isinstance(other, DNSRecord):
337 return DNSEntry.__eq__(self, other)
337 return DNSEntry.__eq__(self, other)
338 return 0
338 return 0
339
339
340 def suppressedBy(self, msg):
340 def suppressedBy(self, msg):
341 """Returns true if any answer in a message can suffice for the
341 """Returns true if any answer in a message can suffice for the
342 information held in this record."""
342 information held in this record."""
343 for record in msg.answers:
343 for record in msg.answers:
344 if self.suppressedByAnswer(record):
344 if self.suppressedByAnswer(record):
345 return 1
345 return 1
346 return 0
346 return 0
347
347
348 def suppressedByAnswer(self, other):
348 def suppressedByAnswer(self, other):
349 """Returns true if another record has same name, type and class,
349 """Returns true if another record has same name, type and class,
350 and if its TTL is at least half of this record's."""
350 and if its TTL is at least half of this record's."""
351 if self == other and other.ttl > (self.ttl / 2):
351 if self == other and other.ttl > (self.ttl / 2):
352 return 1
352 return 1
353 return 0
353 return 0
354
354
355 def getExpirationTime(self, percent):
355 def getExpirationTime(self, percent):
356 """Returns the time at which this record will have expired
356 """Returns the time at which this record will have expired
357 by a certain percentage."""
357 by a certain percentage."""
358 return self.created + (percent * self.ttl * 10)
358 return self.created + (percent * self.ttl * 10)
359
359
360 def getRemainingTTL(self, now):
360 def getRemainingTTL(self, now):
361 """Returns the remaining TTL in seconds."""
361 """Returns the remaining TTL in seconds."""
362 return max(0, (self.getExpirationTime(100) - now) / 1000)
362 return max(0, (self.getExpirationTime(100) - now) / 1000)
363
363
364 def isExpired(self, now):
364 def isExpired(self, now):
365 """Returns true if this record has expired."""
365 """Returns true if this record has expired."""
366 return self.getExpirationTime(100) <= now
366 return self.getExpirationTime(100) <= now
367
367
368 def isStale(self, now):
368 def isStale(self, now):
369 """Returns true if this record is at least half way expired."""
369 """Returns true if this record is at least half way expired."""
370 return self.getExpirationTime(50) <= now
370 return self.getExpirationTime(50) <= now
371
371
372 def resetTTL(self, other):
372 def resetTTL(self, other):
373 """Sets this record's TTL and created time to that of
373 """Sets this record's TTL and created time to that of
374 another record."""
374 another record."""
375 self.created = other.created
375 self.created = other.created
376 self.ttl = other.ttl
376 self.ttl = other.ttl
377
377
378 def write(self, out):
378 def write(self, out):
379 """Abstract method"""
379 """Abstract method"""
380 raise AbstractMethodException
380 raise AbstractMethodException
381
381
382 def toString(self, other):
382 def toString(self, other):
383 """String representation with additional information"""
383 """String representation with additional information"""
384 arg = b"%s/%s,%s" % (
384 arg = b"%s/%s,%s" % (
385 self.ttl,
385 self.ttl,
386 self.getRemainingTTL(currentTimeMillis()),
386 self.getRemainingTTL(currentTimeMillis()),
387 other,
387 other,
388 )
388 )
389 return DNSEntry.toString(self, b"record", arg)
389 return DNSEntry.toString(self, b"record", arg)
390
390
391
391
392 class DNSAddress(DNSRecord):
392 class DNSAddress(DNSRecord):
393 """A DNS address record"""
393 """A DNS address record"""
394
394
395 def __init__(self, name, type, clazz, ttl, address):
395 def __init__(self, name, type, clazz, ttl, address):
396 DNSRecord.__init__(self, name, type, clazz, ttl)
396 DNSRecord.__init__(self, name, type, clazz, ttl)
397 self.address = address
397 self.address = address
398
398
399 def write(self, out):
399 def write(self, out):
400 """Used in constructing an outgoing packet"""
400 """Used in constructing an outgoing packet"""
401 out.writeString(self.address, len(self.address))
401 out.writeString(self.address, len(self.address))
402
402
403 def __eq__(self, other):
403 def __eq__(self, other):
404 """Tests equality on address"""
404 """Tests equality on address"""
405 if isinstance(other, DNSAddress):
405 if isinstance(other, DNSAddress):
406 return self.address == other.address
406 return self.address == other.address
407 return 0
407 return 0
408
408
409 def __repr__(self):
409 def __repr__(self):
410 """String representation"""
410 """String representation"""
411 try:
411 try:
412 return socket.inet_ntoa(self.address)
412 return socket.inet_ntoa(self.address)
413 except Exception:
413 except Exception:
414 return self.address
414 return self.address
415
415
416
416
417 class DNSHinfo(DNSRecord):
417 class DNSHinfo(DNSRecord):
418 """A DNS host information record"""
418 """A DNS host information record"""
419
419
420 def __init__(self, name, type, clazz, ttl, cpu, os):
420 def __init__(self, name, type, clazz, ttl, cpu, os):
421 DNSRecord.__init__(self, name, type, clazz, ttl)
421 DNSRecord.__init__(self, name, type, clazz, ttl)
422 self.cpu = cpu
422 self.cpu = cpu
423 self.os = os
423 self.os = os
424
424
425 def write(self, out):
425 def write(self, out):
426 """Used in constructing an outgoing packet"""
426 """Used in constructing an outgoing packet"""
427 out.writeString(self.cpu, len(self.cpu))
427 out.writeString(self.cpu, len(self.cpu))
428 out.writeString(self.os, len(self.os))
428 out.writeString(self.os, len(self.os))
429
429
430 def __eq__(self, other):
430 def __eq__(self, other):
431 """Tests equality on cpu and os"""
431 """Tests equality on cpu and os"""
432 if isinstance(other, DNSHinfo):
432 if isinstance(other, DNSHinfo):
433 return self.cpu == other.cpu and self.os == other.os
433 return self.cpu == other.cpu and self.os == other.os
434 return 0
434 return 0
435
435
436 def __repr__(self):
436 def __repr__(self):
437 """String representation"""
437 """String representation"""
438 return self.cpu + b" " + self.os
438 return self.cpu + b" " + self.os
439
439
440
440
441 class DNSPointer(DNSRecord):
441 class DNSPointer(DNSRecord):
442 """A DNS pointer record"""
442 """A DNS pointer record"""
443
443
444 def __init__(self, name, type, clazz, ttl, alias):
444 def __init__(self, name, type, clazz, ttl, alias):
445 DNSRecord.__init__(self, name, type, clazz, ttl)
445 DNSRecord.__init__(self, name, type, clazz, ttl)
446 self.alias = alias
446 self.alias = alias
447
447
448 def write(self, out):
448 def write(self, out):
449 """Used in constructing an outgoing packet"""
449 """Used in constructing an outgoing packet"""
450 out.writeName(self.alias)
450 out.writeName(self.alias)
451
451
452 def __eq__(self, other):
452 def __eq__(self, other):
453 """Tests equality on alias"""
453 """Tests equality on alias"""
454 if isinstance(other, DNSPointer):
454 if isinstance(other, DNSPointer):
455 return self.alias == other.alias
455 return self.alias == other.alias
456 return 0
456 return 0
457
457
458 def __repr__(self):
458 def __repr__(self):
459 """String representation"""
459 """String representation"""
460 return self.toString(self.alias)
460 return self.toString(self.alias)
461
461
462
462
463 class DNSText(DNSRecord):
463 class DNSText(DNSRecord):
464 """A DNS text record"""
464 """A DNS text record"""
465
465
466 def __init__(self, name, type, clazz, ttl, text):
466 def __init__(self, name, type, clazz, ttl, text):
467 DNSRecord.__init__(self, name, type, clazz, ttl)
467 DNSRecord.__init__(self, name, type, clazz, ttl)
468 self.text = text
468 self.text = text
469
469
470 def write(self, out):
470 def write(self, out):
471 """Used in constructing an outgoing packet"""
471 """Used in constructing an outgoing packet"""
472 out.writeString(self.text, len(self.text))
472 out.writeString(self.text, len(self.text))
473
473
474 def __eq__(self, other):
474 def __eq__(self, other):
475 """Tests equality on text"""
475 """Tests equality on text"""
476 if isinstance(other, DNSText):
476 if isinstance(other, DNSText):
477 return self.text == other.text
477 return self.text == other.text
478 return 0
478 return 0
479
479
480 def __repr__(self):
480 def __repr__(self):
481 """String representation"""
481 """String representation"""
482 if len(self.text) > 10:
482 if len(self.text) > 10:
483 return self.toString(self.text[:7] + b"...")
483 return self.toString(self.text[:7] + b"...")
484 else:
484 else:
485 return self.toString(self.text)
485 return self.toString(self.text)
486
486
487
487
488 class DNSService(DNSRecord):
488 class DNSService(DNSRecord):
489 """A DNS service record"""
489 """A DNS service record"""
490
490
491 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
491 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
492 DNSRecord.__init__(self, name, type, clazz, ttl)
492 DNSRecord.__init__(self, name, type, clazz, ttl)
493 self.priority = priority
493 self.priority = priority
494 self.weight = weight
494 self.weight = weight
495 self.port = port
495 self.port = port
496 self.server = server
496 self.server = server
497
497
498 def write(self, out):
498 def write(self, out):
499 """Used in constructing an outgoing packet"""
499 """Used in constructing an outgoing packet"""
500 out.writeShort(self.priority)
500 out.writeShort(self.priority)
501 out.writeShort(self.weight)
501 out.writeShort(self.weight)
502 out.writeShort(self.port)
502 out.writeShort(self.port)
503 out.writeName(self.server)
503 out.writeName(self.server)
504
504
505 def __eq__(self, other):
505 def __eq__(self, other):
506 """Tests equality on priority, weight, port and server"""
506 """Tests equality on priority, weight, port and server"""
507 if isinstance(other, DNSService):
507 if isinstance(other, DNSService):
508 return (
508 return (
509 self.priority == other.priority
509 self.priority == other.priority
510 and self.weight == other.weight
510 and self.weight == other.weight
511 and self.port == other.port
511 and self.port == other.port
512 and self.server == other.server
512 and self.server == other.server
513 )
513 )
514 return 0
514 return 0
515
515
516 def __repr__(self):
516 def __repr__(self):
517 """String representation"""
517 """String representation"""
518 return self.toString(b"%s:%s" % (self.server, self.port))
518 return self.toString(b"%s:%s" % (self.server, self.port))
519
519
520
520
521 class DNSIncoming:
521 class DNSIncoming:
522 """Object representation of an incoming DNS packet"""
522 """Object representation of an incoming DNS packet"""
523
523
524 def __init__(self, data):
524 def __init__(self, data):
525 """Constructor from string holding bytes of packet"""
525 """Constructor from string holding bytes of packet"""
526 self.offset = 0
526 self.offset = 0
527 self.data = data
527 self.data = data
528 self.questions = []
528 self.questions = []
529 self.answers = []
529 self.answers = []
530 self.numquestions = 0
530 self.numquestions = 0
531 self.numanswers = 0
531 self.numanswers = 0
532 self.numauthorities = 0
532 self.numauthorities = 0
533 self.numadditionals = 0
533 self.numadditionals = 0
534
534
535 self.readHeader()
535 self.readHeader()
536 self.readQuestions()
536 self.readQuestions()
537 self.readOthers()
537 self.readOthers()
538
538
539 def readHeader(self):
539 def readHeader(self):
540 """Reads header portion of packet"""
540 """Reads header portion of packet"""
541 format = b'!HHHHHH'
541 format = b'!HHHHHH'
542 length = struct.calcsize(format)
542 length = struct.calcsize(format)
543 info = struct.unpack(
543 info = struct.unpack(
544 format, self.data[self.offset : self.offset + length]
544 format, self.data[self.offset : self.offset + length]
545 )
545 )
546 self.offset += length
546 self.offset += length
547
547
548 self.id = info[0]
548 self.id = info[0]
549 self.flags = info[1]
549 self.flags = info[1]
550 self.numquestions = info[2]
550 self.numquestions = info[2]
551 self.numanswers = info[3]
551 self.numanswers = info[3]
552 self.numauthorities = info[4]
552 self.numauthorities = info[4]
553 self.numadditionals = info[5]
553 self.numadditionals = info[5]
554
554
555 def readQuestions(self):
555 def readQuestions(self):
556 """Reads questions section of packet"""
556 """Reads questions section of packet"""
557 format = b'!HH'
557 format = b'!HH'
558 length = struct.calcsize(format)
558 length = struct.calcsize(format)
559 for i in range(0, self.numquestions):
559 for i in range(0, self.numquestions):
560 name = self.readName()
560 name = self.readName()
561 info = struct.unpack(
561 info = struct.unpack(
562 format, self.data[self.offset : self.offset + length]
562 format, self.data[self.offset : self.offset + length]
563 )
563 )
564 self.offset += length
564 self.offset += length
565
565
566 try:
566 try:
567 question = DNSQuestion(name, info[0], info[1])
567 question = DNSQuestion(name, info[0], info[1])
568 self.questions.append(question)
568 self.questions.append(question)
569 except NonLocalNameException:
569 except NonLocalNameException:
570 pass
570 pass
571
571
572 def readInt(self):
572 def readInt(self):
573 """Reads an integer from the packet"""
573 """Reads an integer from the packet"""
574 format = b'!I'
574 format = b'!I'
575 length = struct.calcsize(format)
575 length = struct.calcsize(format)
576 info = struct.unpack(
576 info = struct.unpack(
577 format, self.data[self.offset : self.offset + length]
577 format, self.data[self.offset : self.offset + length]
578 )
578 )
579 self.offset += length
579 self.offset += length
580 return info[0]
580 return info[0]
581
581
582 def readCharacterString(self):
582 def readCharacterString(self):
583 """Reads a character string from the packet"""
583 """Reads a character string from the packet"""
584 length = ord(self.data[self.offset])
584 length = ord(self.data[self.offset])
585 self.offset += 1
585 self.offset += 1
586 return self.readString(length)
586 return self.readString(length)
587
587
588 def readString(self, len):
588 def readString(self, len):
589 """Reads a string of a given length from the packet"""
589 """Reads a string of a given length from the packet"""
590 format = b'!%ds' % len
590 format = b'!%ds' % len
591 length = struct.calcsize(format)
591 length = struct.calcsize(format)
592 info = struct.unpack(
592 info = struct.unpack(
593 format, self.data[self.offset : self.offset + length]
593 format, self.data[self.offset : self.offset + length]
594 )
594 )
595 self.offset += length
595 self.offset += length
596 return info[0]
596 return info[0]
597
597
598 def readUnsignedShort(self):
598 def readUnsignedShort(self):
599 """Reads an unsigned short from the packet"""
599 """Reads an unsigned short from the packet"""
600 format = b'!H'
600 format = b'!H'
601 length = struct.calcsize(format)
601 length = struct.calcsize(format)
602 info = struct.unpack(
602 info = struct.unpack(
603 format, self.data[self.offset : self.offset + length]
603 format, self.data[self.offset : self.offset + length]
604 )
604 )
605 self.offset += length
605 self.offset += length
606 return info[0]
606 return info[0]
607
607
608 def readOthers(self):
608 def readOthers(self):
609 """Reads answers, authorities and additionals section of the packet"""
609 """Reads answers, authorities and additionals section of the packet"""
610 format = b'!HHiH'
610 format = b'!HHiH'
611 length = struct.calcsize(format)
611 length = struct.calcsize(format)
612 n = self.numanswers + self.numauthorities + self.numadditionals
612 n = self.numanswers + self.numauthorities + self.numadditionals
613 for i in range(0, n):
613 for i in range(0, n):
614 domain = self.readName()
614 domain = self.readName()
615 info = struct.unpack(
615 info = struct.unpack(
616 format, self.data[self.offset : self.offset + length]
616 format, self.data[self.offset : self.offset + length]
617 )
617 )
618 self.offset += length
618 self.offset += length
619
619
620 rec = None
620 rec = None
621 if info[0] == _TYPE_A:
621 if info[0] == _TYPE_A:
622 rec = DNSAddress(
622 rec = DNSAddress(
623 domain, info[0], info[1], info[2], self.readString(4)
623 domain, info[0], info[1], info[2], self.readString(4)
624 )
624 )
625 elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR:
625 elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR:
626 rec = DNSPointer(
626 rec = DNSPointer(
627 domain, info[0], info[1], info[2], self.readName()
627 domain, info[0], info[1], info[2], self.readName()
628 )
628 )
629 elif info[0] == _TYPE_TXT:
629 elif info[0] == _TYPE_TXT:
630 rec = DNSText(
630 rec = DNSText(
631 domain, info[0], info[1], info[2], self.readString(info[3])
631 domain, info[0], info[1], info[2], self.readString(info[3])
632 )
632 )
633 elif info[0] == _TYPE_SRV:
633 elif info[0] == _TYPE_SRV:
634 rec = DNSService(
634 rec = DNSService(
635 domain,
635 domain,
636 info[0],
636 info[0],
637 info[1],
637 info[1],
638 info[2],
638 info[2],
639 self.readUnsignedShort(),
639 self.readUnsignedShort(),
640 self.readUnsignedShort(),
640 self.readUnsignedShort(),
641 self.readUnsignedShort(),
641 self.readUnsignedShort(),
642 self.readName(),
642 self.readName(),
643 )
643 )
644 elif info[0] == _TYPE_HINFO:
644 elif info[0] == _TYPE_HINFO:
645 rec = DNSHinfo(
645 rec = DNSHinfo(
646 domain,
646 domain,
647 info[0],
647 info[0],
648 info[1],
648 info[1],
649 info[2],
649 info[2],
650 self.readCharacterString(),
650 self.readCharacterString(),
651 self.readCharacterString(),
651 self.readCharacterString(),
652 )
652 )
653 elif info[0] == _TYPE_AAAA:
653 elif info[0] == _TYPE_AAAA:
654 rec = DNSAddress(
654 rec = DNSAddress(
655 domain, info[0], info[1], info[2], self.readString(16)
655 domain, info[0], info[1], info[2], self.readString(16)
656 )
656 )
657 else:
657 else:
658 # Try to ignore types we don't know about
658 # Try to ignore types we don't know about
659 # this may mean the rest of the name is
659 # this may mean the rest of the name is
660 # unable to be parsed, and may show errors
660 # unable to be parsed, and may show errors
661 # so this is left for debugging. New types
661 # so this is left for debugging. New types
662 # encountered need to be parsed properly.
662 # encountered need to be parsed properly.
663 #
663 #
664 # print "UNKNOWN TYPE = " + str(info[0])
664 # print "UNKNOWN TYPE = " + str(info[0])
665 # raise BadTypeInNameException
665 # raise BadTypeInNameException
666 self.offset += info[3]
666 self.offset += info[3]
667
667
668 if rec is not None:
668 if rec is not None:
669 self.answers.append(rec)
669 self.answers.append(rec)
670
670
671 def isQuery(self):
671 def isQuery(self):
672 """Returns true if this is a query"""
672 """Returns true if this is a query"""
673 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
673 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
674
674
675 def isResponse(self):
675 def isResponse(self):
676 """Returns true if this is a response"""
676 """Returns true if this is a response"""
677 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
677 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
678
678
679 def readUTF(self, offset, len):
679 def readUTF(self, offset, len):
680 """Reads a UTF-8 string of a given length from the packet"""
680 """Reads a UTF-8 string of a given length from the packet"""
681 return self.data[offset : offset + len].decode('utf-8')
681 return self.data[offset : offset + len].decode('utf-8')
682
682
683 def readName(self):
683 def readName(self):
684 """Reads a domain name from the packet"""
684 """Reads a domain name from the packet"""
685 result = r''
685 result = r''
686 off = self.offset
686 off = self.offset
687 next = -1
687 next = -1
688 first = off
688 first = off
689
689
690 while True:
690 while True:
691 len = ord(self.data[off : off + 1])
691 len = ord(self.data[off : off + 1])
692 off += 1
692 off += 1
693 if len == 0:
693 if len == 0:
694 break
694 break
695 t = len & 0xC0
695 t = len & 0xC0
696 if t == 0x00:
696 if t == 0x00:
697 result = ''.join((result, self.readUTF(off, len) + '.'))
697 result = ''.join((result, self.readUTF(off, len) + '.'))
698 off += len
698 off += len
699 elif t == 0xC0:
699 elif t == 0xC0:
700 if next < 0:
700 if next < 0:
701 next = off + 1
701 next = off + 1
702 off = ((len & 0x3F) << 8) | ord(self.data[off : off + 1])
702 off = ((len & 0x3F) << 8) | ord(self.data[off : off + 1])
703 if off >= first:
703 if off >= first:
704 raise BadDomainNameCircular(off)
704 raise BadDomainNameCircular(off)
705 first = off
705 first = off
706 else:
706 else:
707 raise BadDomainName(off)
707 raise BadDomainName(off)
708
708
709 if next >= 0:
709 if next >= 0:
710 self.offset = next
710 self.offset = next
711 else:
711 else:
712 self.offset = off
712 self.offset = off
713
713
714 return result
714 return result
715
715
716
716
717 class DNSOutgoing:
717 class DNSOutgoing:
718 """Object representation of an outgoing packet"""
718 """Object representation of an outgoing packet"""
719
719
720 def __init__(self, flags, multicast=1):
720 def __init__(self, flags, multicast=1):
721 self.finished = 0
721 self.finished = 0
722 self.id = 0
722 self.id = 0
723 self.multicast = multicast
723 self.multicast = multicast
724 self.flags = flags
724 self.flags = flags
725 self.names = {}
725 self.names = {}
726 self.data = []
726 self.data = []
727 self.size = 12
727 self.size = 12
728
728
729 self.questions = []
729 self.questions = []
730 self.answers = []
730 self.answers = []
731 self.authorities = []
731 self.authorities = []
732 self.additionals = []
732 self.additionals = []
733
733
734 def addQuestion(self, record):
734 def addQuestion(self, record):
735 """Adds a question"""
735 """Adds a question"""
736 self.questions.append(record)
736 self.questions.append(record)
737
737
738 def addAnswer(self, inp, record):
738 def addAnswer(self, inp, record):
739 """Adds an answer"""
739 """Adds an answer"""
740 if not record.suppressedBy(inp):
740 if not record.suppressedBy(inp):
741 self.addAnswerAtTime(record, 0)
741 self.addAnswerAtTime(record, 0)
742
742
743 def addAnswerAtTime(self, record, now):
743 def addAnswerAtTime(self, record, now):
744 """Adds an answer if if does not expire by a certain time"""
744 """Adds an answer if if does not expire by a certain time"""
745 if record is not None:
745 if record is not None:
746 if now == 0 or not record.isExpired(now):
746 if now == 0 or not record.isExpired(now):
747 self.answers.append((record, now))
747 self.answers.append((record, now))
748
748
749 def addAuthoritativeAnswer(self, record):
749 def addAuthoritativeAnswer(self, record):
750 """Adds an authoritative answer"""
750 """Adds an authoritative answer"""
751 self.authorities.append(record)
751 self.authorities.append(record)
752
752
753 def addAdditionalAnswer(self, record):
753 def addAdditionalAnswer(self, record):
754 """Adds an additional answer"""
754 """Adds an additional answer"""
755 self.additionals.append(record)
755 self.additionals.append(record)
756
756
757 def writeByte(self, value):
757 def writeByte(self, value):
758 """Writes a single byte to the packet"""
758 """Writes a single byte to the packet"""
759 format = b'!c'
759 format = b'!c'
760 self.data.append(struct.pack(format, chr(value)))
760 self.data.append(struct.pack(format, chr(value)))
761 self.size += 1
761 self.size += 1
762
762
763 def insertShort(self, index, value):
763 def insertShort(self, index, value):
764 """Inserts an unsigned short in a certain position in the packet"""
764 """Inserts an unsigned short in a certain position in the packet"""
765 format = b'!H'
765 format = b'!H'
766 self.data.insert(index, struct.pack(format, value))
766 self.data.insert(index, struct.pack(format, value))
767 self.size += 2
767 self.size += 2
768
768
769 def writeShort(self, value):
769 def writeShort(self, value):
770 """Writes an unsigned short to the packet"""
770 """Writes an unsigned short to the packet"""
771 format = b'!H'
771 format = b'!H'
772 self.data.append(struct.pack(format, value))
772 self.data.append(struct.pack(format, value))
773 self.size += 2
773 self.size += 2
774
774
775 def writeInt(self, value):
775 def writeInt(self, value):
776 """Writes an unsigned integer to the packet"""
776 """Writes an unsigned integer to the packet"""
777 format = b'!I'
777 format = b'!I'
778 self.data.append(struct.pack(format, int(value)))
778 self.data.append(struct.pack(format, int(value)))
779 self.size += 4
779 self.size += 4
780
780
781 def writeString(self, value, length):
781 def writeString(self, value, length):
782 """Writes a string to the packet"""
782 """Writes a string to the packet"""
783 format = '!' + str(length) + 's'
783 format = '!' + str(length) + 's'
784 self.data.append(struct.pack(format, value))
784 self.data.append(struct.pack(format, value))
785 self.size += length
785 self.size += length
786
786
787 def writeUTF(self, s):
787 def writeUTF(self, s):
788 """Writes a UTF-8 string of a given length to the packet"""
788 """Writes a UTF-8 string of a given length to the packet"""
789 utfstr = s.encode('utf-8')
789 utfstr = s.encode('utf-8')
790 length = len(utfstr)
790 length = len(utfstr)
791 if length > 64:
791 if length > 64:
792 raise NamePartTooLongException
792 raise NamePartTooLongException
793 self.writeByte(length)
793 self.writeByte(length)
794 self.writeString(utfstr, length)
794 self.writeString(utfstr, length)
795
795
796 def writeName(self, name):
796 def writeName(self, name):
797 """Writes a domain name to the packet"""
797 """Writes a domain name to the packet"""
798
798
799 try:
799 try:
800 # Find existing instance of this name in packet
800 # Find existing instance of this name in packet
801 #
801 #
802 index = self.names[name]
802 index = self.names[name]
803 except KeyError:
803 except KeyError:
804 # No record of this name already, so write it
804 # No record of this name already, so write it
805 # out as normal, recording the location of the name
805 # out as normal, recording the location of the name
806 # for future pointers to it.
806 # for future pointers to it.
807 #
807 #
808 self.names[name] = self.size
808 self.names[name] = self.size
809 parts = name.split(b'.')
809 parts = name.split(b'.')
810 if parts[-1] == b'':
810 if parts[-1] == b'':
811 parts = parts[:-1]
811 parts = parts[:-1]
812 for part in parts:
812 for part in parts:
813 self.writeUTF(part)
813 self.writeUTF(part)
814 self.writeByte(0)
814 self.writeByte(0)
815 return
815 return
816
816
817 # An index was found, so write a pointer to it
817 # An index was found, so write a pointer to it
818 #
818 #
819 self.writeByte((index >> 8) | 0xC0)
819 self.writeByte((index >> 8) | 0xC0)
820 self.writeByte(index)
820 self.writeByte(index)
821
821
822 def writeQuestion(self, question):
822 def writeQuestion(self, question):
823 """Writes a question to the packet"""
823 """Writes a question to the packet"""
824 self.writeName(question.name)
824 self.writeName(question.name)
825 self.writeShort(question.type)
825 self.writeShort(question.type)
826 self.writeShort(question.clazz)
826 self.writeShort(question.clazz)
827
827
828 def writeRecord(self, record, now):
828 def writeRecord(self, record, now):
829 """Writes a record (answer, authoritative answer, additional) to
829 """Writes a record (answer, authoritative answer, additional) to
830 the packet"""
830 the packet"""
831 self.writeName(record.name)
831 self.writeName(record.name)
832 self.writeShort(record.type)
832 self.writeShort(record.type)
833 if record.unique and self.multicast:
833 if record.unique and self.multicast:
834 self.writeShort(record.clazz | _CLASS_UNIQUE)
834 self.writeShort(record.clazz | _CLASS_UNIQUE)
835 else:
835 else:
836 self.writeShort(record.clazz)
836 self.writeShort(record.clazz)
837 if now == 0:
837 if now == 0:
838 self.writeInt(record.ttl)
838 self.writeInt(record.ttl)
839 else:
839 else:
840 self.writeInt(record.getRemainingTTL(now))
840 self.writeInt(record.getRemainingTTL(now))
841 index = len(self.data)
841 index = len(self.data)
842 # Adjust size for the short we will write before this record
842 # Adjust size for the short we will write before this record
843 #
843 #
844 self.size += 2
844 self.size += 2
845 record.write(self)
845 record.write(self)
846 self.size -= 2
846 self.size -= 2
847
847
848 length = len(b''.join(self.data[index:]))
848 length = len(b''.join(self.data[index:]))
849 self.insertShort(index, length) # Here is the short we adjusted for
849 self.insertShort(index, length) # Here is the short we adjusted for
850
850
851 def packet(self):
851 def packet(self):
852 """Returns a string containing the packet's bytes
852 """Returns a string containing the packet's bytes
853
853
854 No further parts should be added to the packet once this
854 No further parts should be added to the packet once this
855 is done."""
855 is done."""
856 if not self.finished:
856 if not self.finished:
857 self.finished = 1
857 self.finished = 1
858 for question in self.questions:
858 for question in self.questions:
859 self.writeQuestion(question)
859 self.writeQuestion(question)
860 for answer, time_ in self.answers:
860 for answer, time_ in self.answers:
861 self.writeRecord(answer, time_)
861 self.writeRecord(answer, time_)
862 for authority in self.authorities:
862 for authority in self.authorities:
863 self.writeRecord(authority, 0)
863 self.writeRecord(authority, 0)
864 for additional in self.additionals:
864 for additional in self.additionals:
865 self.writeRecord(additional, 0)
865 self.writeRecord(additional, 0)
866
866
867 self.insertShort(0, len(self.additionals))
867 self.insertShort(0, len(self.additionals))
868 self.insertShort(0, len(self.authorities))
868 self.insertShort(0, len(self.authorities))
869 self.insertShort(0, len(self.answers))
869 self.insertShort(0, len(self.answers))
870 self.insertShort(0, len(self.questions))
870 self.insertShort(0, len(self.questions))
871 self.insertShort(0, self.flags)
871 self.insertShort(0, self.flags)
872 if self.multicast:
872 if self.multicast:
873 self.insertShort(0, 0)
873 self.insertShort(0, 0)
874 else:
874 else:
875 self.insertShort(0, self.id)
875 self.insertShort(0, self.id)
876 return b''.join(self.data)
876 return b''.join(self.data)
877
877
878
878
879 class DNSCache:
879 class DNSCache:
880 """A cache of DNS entries"""
880 """A cache of DNS entries"""
881
881
882 def __init__(self):
882 def __init__(self):
883 self.cache = {}
883 self.cache = {}
884
884
885 def add(self, entry):
885 def add(self, entry):
886 """Adds an entry"""
886 """Adds an entry"""
887 try:
887 try:
888 list = self.cache[entry.key]
888 list = self.cache[entry.key]
889 except KeyError:
889 except KeyError:
890 list = self.cache[entry.key] = []
890 list = self.cache[entry.key] = []
891 list.append(entry)
891 list.append(entry)
892
892
893 def remove(self, entry):
893 def remove(self, entry):
894 """Removes an entry"""
894 """Removes an entry"""
895 try:
895 try:
896 list = self.cache[entry.key]
896 list = self.cache[entry.key]
897 list.remove(entry)
897 list.remove(entry)
898 except KeyError:
898 except KeyError:
899 pass
899 pass
900
900
901 def get(self, entry):
901 def get(self, entry):
902 """Gets an entry by key. Will return None if there is no
902 """Gets an entry by key. Will return None if there is no
903 matching entry."""
903 matching entry."""
904 try:
904 try:
905 list = self.cache[entry.key]
905 list = self.cache[entry.key]
906 return list[list.index(entry)]
906 return list[list.index(entry)]
907 except (KeyError, ValueError):
907 except (KeyError, ValueError):
908 return None
908 return None
909
909
910 def getByDetails(self, name, type, clazz):
910 def getByDetails(self, name, type, clazz):
911 """Gets an entry by details. Will return None if there is
911 """Gets an entry by details. Will return None if there is
912 no matching entry."""
912 no matching entry."""
913 entry = DNSEntry(name, type, clazz)
913 entry = DNSEntry(name, type, clazz)
914 return self.get(entry)
914 return self.get(entry)
915
915
916 def entriesWithName(self, name):
916 def entriesWithName(self, name):
917 """Returns a list of entries whose key matches the name."""
917 """Returns a list of entries whose key matches the name."""
918 try:
918 try:
919 return self.cache[name]
919 return self.cache[name]
920 except KeyError:
920 except KeyError:
921 return []
921 return []
922
922
923 def entries(self):
923 def entries(self):
924 """Returns a list of all entries"""
924 """Returns a list of all entries"""
925 try:
925 try:
926 return list(itertools.chain.from_iterable(self.cache.values()))
926 return list(itertools.chain.from_iterable(self.cache.values()))
927 except Exception:
927 except Exception:
928 return []
928 return []
929
929
930
930
931 class Engine(threading.Thread):
931 class Engine(threading.Thread):
932 """An engine wraps read access to sockets, allowing objects that
932 """An engine wraps read access to sockets, allowing objects that
933 need to receive data from sockets to be called back when the
933 need to receive data from sockets to be called back when the
934 sockets are ready.
934 sockets are ready.
935
935
936 A reader needs a handle_read() method, which is called when the socket
936 A reader needs a handle_read() method, which is called when the socket
937 it is interested in is ready for reading.
937 it is interested in is ready for reading.
938
938
939 Writers are not implemented here, because we only send short
939 Writers are not implemented here, because we only send short
940 packets.
940 packets.
941 """
941 """
942
942
943 def __init__(self, zeroconf):
943 def __init__(self, zeroconf):
944 threading.Thread.__init__(self)
944 threading.Thread.__init__(self)
945 self.zeroconf = zeroconf
945 self.zeroconf = zeroconf
946 self.readers = {} # maps socket to reader
946 self.readers = {} # maps socket to reader
947 self.timeout = 5
947 self.timeout = 5
948 self.condition = threading.Condition()
948 self.condition = threading.Condition()
949 self.start()
949 self.start()
950
950
951 def run(self):
951 def run(self):
952 while not globals()[b'_GLOBAL_DONE']:
952 while not globals()['_GLOBAL_DONE']:
953 rs = self.getReaders()
953 rs = self.getReaders()
954 if len(rs) == 0:
954 if len(rs) == 0:
955 # No sockets to manage, but we wait for the timeout
955 # No sockets to manage, but we wait for the timeout
956 # or addition of a socket
956 # or addition of a socket
957 #
957 #
958 self.condition.acquire()
958 self.condition.acquire()
959 self.condition.wait(self.timeout)
959 self.condition.wait(self.timeout)
960 self.condition.release()
960 self.condition.release()
961 else:
961 else:
962 try:
962 try:
963 rr, wr, er = select.select(rs, [], [], self.timeout)
963 rr, wr, er = select.select(rs, [], [], self.timeout)
964 for sock in rr:
964 for sock in rr:
965 try:
965 try:
966 self.readers[sock].handle_read()
966 self.readers[sock].handle_read()
967 except Exception:
967 except Exception:
968 if not globals()[b'_GLOBAL_DONE']:
968 if not globals()['_GLOBAL_DONE']:
969 traceback.print_exc()
969 traceback.print_exc()
970 except Exception:
970 except Exception:
971 pass
971 pass
972
972
973 def getReaders(self):
973 def getReaders(self):
974 self.condition.acquire()
974 self.condition.acquire()
975 result = self.readers.keys()
975 result = self.readers.keys()
976 self.condition.release()
976 self.condition.release()
977 return result
977 return result
978
978
979 def addReader(self, reader, socket):
979 def addReader(self, reader, socket):
980 self.condition.acquire()
980 self.condition.acquire()
981 self.readers[socket] = reader
981 self.readers[socket] = reader
982 self.condition.notify()
982 self.condition.notify()
983 self.condition.release()
983 self.condition.release()
984
984
985 def delReader(self, socket):
985 def delReader(self, socket):
986 self.condition.acquire()
986 self.condition.acquire()
987 del self.readers[socket]
987 del self.readers[socket]
988 self.condition.notify()
988 self.condition.notify()
989 self.condition.release()
989 self.condition.release()
990
990
991 def notify(self):
991 def notify(self):
992 self.condition.acquire()
992 self.condition.acquire()
993 self.condition.notify()
993 self.condition.notify()
994 self.condition.release()
994 self.condition.release()
995
995
996
996
997 class Listener:
997 class Listener:
998 """A Listener is used by this module to listen on the multicast
998 """A Listener is used by this module to listen on the multicast
999 group to which DNS messages are sent, allowing the implementation
999 group to which DNS messages are sent, allowing the implementation
1000 to cache information as it arrives.
1000 to cache information as it arrives.
1001
1001
1002 It requires registration with an Engine object in order to have
1002 It requires registration with an Engine object in order to have
1003 the read() method called when a socket is available for reading."""
1003 the read() method called when a socket is available for reading."""
1004
1004
1005 def __init__(self, zeroconf):
1005 def __init__(self, zeroconf):
1006 self.zeroconf = zeroconf
1006 self.zeroconf = zeroconf
1007 self.zeroconf.engine.addReader(self, self.zeroconf.socket)
1007 self.zeroconf.engine.addReader(self, self.zeroconf.socket)
1008
1008
1009 def handle_read(self):
1009 def handle_read(self):
1010 sock = self.zeroconf.socket
1010 sock = self.zeroconf.socket
1011 try:
1011 try:
1012 data, (addr, port) = sock.recvfrom(_MAX_MSG_ABSOLUTE)
1012 data, (addr, port) = sock.recvfrom(_MAX_MSG_ABSOLUTE)
1013 except socket.error as e:
1013 except socket.error as e:
1014 if e.errno == errno.EBADF:
1014 if e.errno == errno.EBADF:
1015 # some other thread may close the socket
1015 # some other thread may close the socket
1016 return
1016 return
1017 else:
1017 else:
1018 raise
1018 raise
1019 self.data = data
1019 self.data = data
1020 msg = DNSIncoming(data)
1020 msg = DNSIncoming(data)
1021 if msg.isQuery():
1021 if msg.isQuery():
1022 # Always multicast responses
1022 # Always multicast responses
1023 #
1023 #
1024 if port == _MDNS_PORT:
1024 if port == _MDNS_PORT:
1025 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
1025 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
1026 # If it's not a multicast query, reply via unicast
1026 # If it's not a multicast query, reply via unicast
1027 # and multicast
1027 # and multicast
1028 #
1028 #
1029 elif port == _DNS_PORT:
1029 elif port == _DNS_PORT:
1030 self.zeroconf.handleQuery(msg, addr, port)
1030 self.zeroconf.handleQuery(msg, addr, port)
1031 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
1031 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
1032 else:
1032 else:
1033 self.zeroconf.handleResponse(msg)
1033 self.zeroconf.handleResponse(msg)
1034
1034
1035
1035
1036 class Reaper(threading.Thread):
1036 class Reaper(threading.Thread):
1037 """A Reaper is used by this module to remove cache entries that
1037 """A Reaper is used by this module to remove cache entries that
1038 have expired."""
1038 have expired."""
1039
1039
1040 def __init__(self, zeroconf):
1040 def __init__(self, zeroconf):
1041 threading.Thread.__init__(self)
1041 threading.Thread.__init__(self)
1042 self.zeroconf = zeroconf
1042 self.zeroconf = zeroconf
1043 self.start()
1043 self.start()
1044
1044
1045 def run(self):
1045 def run(self):
1046 while True:
1046 while True:
1047 self.zeroconf.wait(10 * 1000)
1047 self.zeroconf.wait(10 * 1000)
1048 if globals()[b'_GLOBAL_DONE']:
1048 if globals()['_GLOBAL_DONE']:
1049 return
1049 return
1050 now = currentTimeMillis()
1050 now = currentTimeMillis()
1051 for record in self.zeroconf.cache.entries():
1051 for record in self.zeroconf.cache.entries():
1052 if record.isExpired(now):
1052 if record.isExpired(now):
1053 self.zeroconf.updateRecord(now, record)
1053 self.zeroconf.updateRecord(now, record)
1054 self.zeroconf.cache.remove(record)
1054 self.zeroconf.cache.remove(record)
1055
1055
1056
1056
1057 class ServiceBrowser(threading.Thread):
1057 class ServiceBrowser(threading.Thread):
1058 """Used to browse for a service of a specific type.
1058 """Used to browse for a service of a specific type.
1059
1059
1060 The listener object will have its addService() and
1060 The listener object will have its addService() and
1061 removeService() methods called when this browser
1061 removeService() methods called when this browser
1062 discovers changes in the services availability."""
1062 discovers changes in the services availability."""
1063
1063
1064 def __init__(self, zeroconf, type, listener):
1064 def __init__(self, zeroconf, type, listener):
1065 """Creates a browser for a specific type"""
1065 """Creates a browser for a specific type"""
1066 threading.Thread.__init__(self)
1066 threading.Thread.__init__(self)
1067 self.zeroconf = zeroconf
1067 self.zeroconf = zeroconf
1068 self.type = type
1068 self.type = type
1069 self.listener = listener
1069 self.listener = listener
1070 self.services = {}
1070 self.services = {}
1071 self.nexttime = currentTimeMillis()
1071 self.nexttime = currentTimeMillis()
1072 self.delay = _BROWSER_TIME
1072 self.delay = _BROWSER_TIME
1073 self.list = []
1073 self.list = []
1074
1074
1075 self.done = 0
1075 self.done = 0
1076
1076
1077 self.zeroconf.addListener(
1077 self.zeroconf.addListener(
1078 self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN)
1078 self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN)
1079 )
1079 )
1080 self.start()
1080 self.start()
1081
1081
1082 def updateRecord(self, zeroconf, now, record):
1082 def updateRecord(self, zeroconf, now, record):
1083 """Callback invoked by Zeroconf when new information arrives.
1083 """Callback invoked by Zeroconf when new information arrives.
1084
1084
1085 Updates information required by browser in the Zeroconf cache."""
1085 Updates information required by browser in the Zeroconf cache."""
1086 if record.type == _TYPE_PTR and record.name == self.type:
1086 if record.type == _TYPE_PTR and record.name == self.type:
1087 expired = record.isExpired(now)
1087 expired = record.isExpired(now)
1088 try:
1088 try:
1089 oldrecord = self.services[record.alias.lower()]
1089 oldrecord = self.services[record.alias.lower()]
1090 if not expired:
1090 if not expired:
1091 oldrecord.resetTTL(record)
1091 oldrecord.resetTTL(record)
1092 else:
1092 else:
1093 del self.services[record.alias.lower()]
1093 del self.services[record.alias.lower()]
1094 callback = lambda x: self.listener.removeService(
1094 callback = lambda x: self.listener.removeService(
1095 x, self.type, record.alias
1095 x, self.type, record.alias
1096 )
1096 )
1097 self.list.append(callback)
1097 self.list.append(callback)
1098 return
1098 return
1099 except Exception:
1099 except Exception:
1100 if not expired:
1100 if not expired:
1101 self.services[record.alias.lower()] = record
1101 self.services[record.alias.lower()] = record
1102 callback = lambda x: self.listener.addService(
1102 callback = lambda x: self.listener.addService(
1103 x, self.type, record.alias
1103 x, self.type, record.alias
1104 )
1104 )
1105 self.list.append(callback)
1105 self.list.append(callback)
1106
1106
1107 expires = record.getExpirationTime(75)
1107 expires = record.getExpirationTime(75)
1108 if expires < self.nexttime:
1108 if expires < self.nexttime:
1109 self.nexttime = expires
1109 self.nexttime = expires
1110
1110
1111 def cancel(self):
1111 def cancel(self):
1112 self.done = 1
1112 self.done = 1
1113 self.zeroconf.notifyAll()
1113 self.zeroconf.notifyAll()
1114
1114
1115 def run(self):
1115 def run(self):
1116 while True:
1116 while True:
1117 event = None
1117 event = None
1118 now = currentTimeMillis()
1118 now = currentTimeMillis()
1119 if len(self.list) == 0 and self.nexttime > now:
1119 if len(self.list) == 0 and self.nexttime > now:
1120 self.zeroconf.wait(self.nexttime - now)
1120 self.zeroconf.wait(self.nexttime - now)
1121 if globals()[b'_GLOBAL_DONE'] or self.done:
1121 if globals()['_GLOBAL_DONE'] or self.done:
1122 return
1122 return
1123 now = currentTimeMillis()
1123 now = currentTimeMillis()
1124
1124
1125 if self.nexttime <= now:
1125 if self.nexttime <= now:
1126 out = DNSOutgoing(_FLAGS_QR_QUERY)
1126 out = DNSOutgoing(_FLAGS_QR_QUERY)
1127 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
1127 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
1128 for record in self.services.values():
1128 for record in self.services.values():
1129 if not record.isExpired(now):
1129 if not record.isExpired(now):
1130 out.addAnswerAtTime(record, now)
1130 out.addAnswerAtTime(record, now)
1131 self.zeroconf.send(out)
1131 self.zeroconf.send(out)
1132 self.nexttime = now + self.delay
1132 self.nexttime = now + self.delay
1133 self.delay = min(20 * 1000, self.delay * 2)
1133 self.delay = min(20 * 1000, self.delay * 2)
1134
1134
1135 if len(self.list) > 0:
1135 if len(self.list) > 0:
1136 event = self.list.pop(0)
1136 event = self.list.pop(0)
1137
1137
1138 if event is not None:
1138 if event is not None:
1139 event(self.zeroconf)
1139 event(self.zeroconf)
1140
1140
1141
1141
1142 class ServiceInfo:
1142 class ServiceInfo:
1143 """Service information"""
1143 """Service information"""
1144
1144
1145 def __init__(
1145 def __init__(
1146 self,
1146 self,
1147 type,
1147 type,
1148 name,
1148 name,
1149 address=None,
1149 address=None,
1150 port=None,
1150 port=None,
1151 weight=0,
1151 weight=0,
1152 priority=0,
1152 priority=0,
1153 properties=None,
1153 properties=None,
1154 server=None,
1154 server=None,
1155 ):
1155 ):
1156 """Create a service description.
1156 """Create a service description.
1157
1157
1158 type: fully qualified service type name
1158 type: fully qualified service type name
1159 name: fully qualified service name
1159 name: fully qualified service name
1160 address: IP address as unsigned short, network byte order
1160 address: IP address as unsigned short, network byte order
1161 port: port that the service runs on
1161 port: port that the service runs on
1162 weight: weight of the service
1162 weight: weight of the service
1163 priority: priority of the service
1163 priority: priority of the service
1164 properties: dictionary of properties (or a string holding the bytes for
1164 properties: dictionary of properties (or a string holding the bytes for
1165 the text field)
1165 the text field)
1166 server: fully qualified name for service host (defaults to name)"""
1166 server: fully qualified name for service host (defaults to name)"""
1167
1167
1168 if not name.endswith(type):
1168 if not name.endswith(type):
1169 raise BadTypeInNameException
1169 raise BadTypeInNameException
1170 self.type = type
1170 self.type = type
1171 self.name = name
1171 self.name = name
1172 self.address = address
1172 self.address = address
1173 self.port = port
1173 self.port = port
1174 self.weight = weight
1174 self.weight = weight
1175 self.priority = priority
1175 self.priority = priority
1176 if server:
1176 if server:
1177 self.server = server
1177 self.server = server
1178 else:
1178 else:
1179 self.server = name
1179 self.server = name
1180 self.setProperties(properties)
1180 self.setProperties(properties)
1181
1181
1182 def setProperties(self, properties):
1182 def setProperties(self, properties):
1183 """Sets properties and text of this info from a dictionary"""
1183 """Sets properties and text of this info from a dictionary"""
1184 if isinstance(properties, dict):
1184 if isinstance(properties, dict):
1185 self.properties = properties
1185 self.properties = properties
1186 list = []
1186 list = []
1187 result = b''
1187 result = b''
1188 for key in properties:
1188 for key in properties:
1189 value = properties[key]
1189 value = properties[key]
1190 if value is None:
1190 if value is None:
1191 suffix = b''
1191 suffix = b''
1192 elif isinstance(value, str):
1192 elif isinstance(value, str):
1193 suffix = value
1193 suffix = value
1194 elif isinstance(value, int):
1194 elif isinstance(value, int):
1195 if value:
1195 if value:
1196 suffix = b'true'
1196 suffix = b'true'
1197 else:
1197 else:
1198 suffix = b'false'
1198 suffix = b'false'
1199 else:
1199 else:
1200 suffix = b''
1200 suffix = b''
1201 list.append(b'='.join((key, suffix)))
1201 list.append(b'='.join((key, suffix)))
1202 for item in list:
1202 for item in list:
1203 result = b''.join(
1203 result = b''.join(
1204 (
1204 (
1205 result,
1205 result,
1206 struct.pack(b'!c', pycompat.bytechr(len(item))),
1206 struct.pack(b'!c', pycompat.bytechr(len(item))),
1207 item,
1207 item,
1208 )
1208 )
1209 )
1209 )
1210 self.text = result
1210 self.text = result
1211 else:
1211 else:
1212 self.text = properties
1212 self.text = properties
1213
1213
1214 def setText(self, text):
1214 def setText(self, text):
1215 """Sets properties and text given a text field"""
1215 """Sets properties and text given a text field"""
1216 self.text = text
1216 self.text = text
1217 try:
1217 try:
1218 result = {}
1218 result = {}
1219 end = len(text)
1219 end = len(text)
1220 index = 0
1220 index = 0
1221 strs = []
1221 strs = []
1222 while index < end:
1222 while index < end:
1223 length = ord(text[index])
1223 length = ord(text[index])
1224 index += 1
1224 index += 1
1225 strs.append(text[index : index + length])
1225 strs.append(text[index : index + length])
1226 index += length
1226 index += length
1227
1227
1228 for s in strs:
1228 for s in strs:
1229 eindex = s.find(b'=')
1229 eindex = s.find(b'=')
1230 if eindex == -1:
1230 if eindex == -1:
1231 # No equals sign at all
1231 # No equals sign at all
1232 key = s
1232 key = s
1233 value = 0
1233 value = 0
1234 else:
1234 else:
1235 key = s[:eindex]
1235 key = s[:eindex]
1236 value = s[eindex + 1 :]
1236 value = s[eindex + 1 :]
1237 if value == b'true':
1237 if value == b'true':
1238 value = 1
1238 value = 1
1239 elif value == b'false' or not value:
1239 elif value == b'false' or not value:
1240 value = 0
1240 value = 0
1241
1241
1242 # Only update non-existent properties
1242 # Only update non-existent properties
1243 if key and result.get(key) is None:
1243 if key and result.get(key) is None:
1244 result[key] = value
1244 result[key] = value
1245
1245
1246 self.properties = result
1246 self.properties = result
1247 except Exception:
1247 except Exception:
1248 traceback.print_exc()
1248 traceback.print_exc()
1249 self.properties = None
1249 self.properties = None
1250
1250
1251 def getType(self):
1251 def getType(self):
1252 """Type accessor"""
1252 """Type accessor"""
1253 return self.type
1253 return self.type
1254
1254
1255 def getName(self):
1255 def getName(self):
1256 """Name accessor"""
1256 """Name accessor"""
1257 if self.type is not None and self.name.endswith(b"." + self.type):
1257 if self.type is not None and self.name.endswith(b"." + self.type):
1258 return self.name[: len(self.name) - len(self.type) - 1]
1258 return self.name[: len(self.name) - len(self.type) - 1]
1259 return self.name
1259 return self.name
1260
1260
1261 def getAddress(self):
1261 def getAddress(self):
1262 """Address accessor"""
1262 """Address accessor"""
1263 return self.address
1263 return self.address
1264
1264
1265 def getPort(self):
1265 def getPort(self):
1266 """Port accessor"""
1266 """Port accessor"""
1267 return self.port
1267 return self.port
1268
1268
1269 def getPriority(self):
1269 def getPriority(self):
1270 """Priority accessor"""
1270 """Priority accessor"""
1271 return self.priority
1271 return self.priority
1272
1272
1273 def getWeight(self):
1273 def getWeight(self):
1274 """Weight accessor"""
1274 """Weight accessor"""
1275 return self.weight
1275 return self.weight
1276
1276
1277 def getProperties(self):
1277 def getProperties(self):
1278 """Properties accessor"""
1278 """Properties accessor"""
1279 return self.properties
1279 return self.properties
1280
1280
1281 def getText(self):
1281 def getText(self):
1282 """Text accessor"""
1282 """Text accessor"""
1283 return self.text
1283 return self.text
1284
1284
1285 def getServer(self):
1285 def getServer(self):
1286 """Server accessor"""
1286 """Server accessor"""
1287 return self.server
1287 return self.server
1288
1288
1289 def updateRecord(self, zeroconf, now, record):
1289 def updateRecord(self, zeroconf, now, record):
1290 """Updates service information from a DNS record"""
1290 """Updates service information from a DNS record"""
1291 if record is not None and not record.isExpired(now):
1291 if record is not None and not record.isExpired(now):
1292 if record.type == _TYPE_A:
1292 if record.type == _TYPE_A:
1293 # if record.name == self.name:
1293 # if record.name == self.name:
1294 if record.name == self.server:
1294 if record.name == self.server:
1295 self.address = record.address
1295 self.address = record.address
1296 elif record.type == _TYPE_SRV:
1296 elif record.type == _TYPE_SRV:
1297 if record.name == self.name:
1297 if record.name == self.name:
1298 self.server = record.server
1298 self.server = record.server
1299 self.port = record.port
1299 self.port = record.port
1300 self.weight = record.weight
1300 self.weight = record.weight
1301 self.priority = record.priority
1301 self.priority = record.priority
1302 # self.address = None
1302 # self.address = None
1303 self.updateRecord(
1303 self.updateRecord(
1304 zeroconf,
1304 zeroconf,
1305 now,
1305 now,
1306 zeroconf.cache.getByDetails(
1306 zeroconf.cache.getByDetails(
1307 self.server, _TYPE_A, _CLASS_IN
1307 self.server, _TYPE_A, _CLASS_IN
1308 ),
1308 ),
1309 )
1309 )
1310 elif record.type == _TYPE_TXT:
1310 elif record.type == _TYPE_TXT:
1311 if record.name == self.name:
1311 if record.name == self.name:
1312 self.setText(record.text)
1312 self.setText(record.text)
1313
1313
1314 def request(self, zeroconf, timeout):
1314 def request(self, zeroconf, timeout):
1315 """Returns true if the service could be discovered on the
1315 """Returns true if the service could be discovered on the
1316 network, and updates this object with details discovered.
1316 network, and updates this object with details discovered.
1317 """
1317 """
1318 now = currentTimeMillis()
1318 now = currentTimeMillis()
1319 delay = _LISTENER_TIME
1319 delay = _LISTENER_TIME
1320 next = now + delay
1320 next = now + delay
1321 last = now + timeout
1321 last = now + timeout
1322 result = False
1322 result = False
1323 try:
1323 try:
1324 zeroconf.addListener(
1324 zeroconf.addListener(
1325 self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN)
1325 self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN)
1326 )
1326 )
1327 while (
1327 while (
1328 self.server is None or self.address is None or self.text is None
1328 self.server is None or self.address is None or self.text is None
1329 ):
1329 ):
1330 if last <= now:
1330 if last <= now:
1331 return 0
1331 return 0
1332 if next <= now:
1332 if next <= now:
1333 out = DNSOutgoing(_FLAGS_QR_QUERY)
1333 out = DNSOutgoing(_FLAGS_QR_QUERY)
1334 out.addQuestion(
1334 out.addQuestion(
1335 DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN)
1335 DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN)
1336 )
1336 )
1337 out.addAnswerAtTime(
1337 out.addAnswerAtTime(
1338 zeroconf.cache.getByDetails(
1338 zeroconf.cache.getByDetails(
1339 self.name, _TYPE_SRV, _CLASS_IN
1339 self.name, _TYPE_SRV, _CLASS_IN
1340 ),
1340 ),
1341 now,
1341 now,
1342 )
1342 )
1343 out.addQuestion(
1343 out.addQuestion(
1344 DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN)
1344 DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN)
1345 )
1345 )
1346 out.addAnswerAtTime(
1346 out.addAnswerAtTime(
1347 zeroconf.cache.getByDetails(
1347 zeroconf.cache.getByDetails(
1348 self.name, _TYPE_TXT, _CLASS_IN
1348 self.name, _TYPE_TXT, _CLASS_IN
1349 ),
1349 ),
1350 now,
1350 now,
1351 )
1351 )
1352 if self.server is not None:
1352 if self.server is not None:
1353 out.addQuestion(
1353 out.addQuestion(
1354 DNSQuestion(self.server, _TYPE_A, _CLASS_IN)
1354 DNSQuestion(self.server, _TYPE_A, _CLASS_IN)
1355 )
1355 )
1356 out.addAnswerAtTime(
1356 out.addAnswerAtTime(
1357 zeroconf.cache.getByDetails(
1357 zeroconf.cache.getByDetails(
1358 self.server, _TYPE_A, _CLASS_IN
1358 self.server, _TYPE_A, _CLASS_IN
1359 ),
1359 ),
1360 now,
1360 now,
1361 )
1361 )
1362 zeroconf.send(out)
1362 zeroconf.send(out)
1363 next = now + delay
1363 next = now + delay
1364 delay = delay * 2
1364 delay = delay * 2
1365
1365
1366 zeroconf.wait(min(next, last) - now)
1366 zeroconf.wait(min(next, last) - now)
1367 now = currentTimeMillis()
1367 now = currentTimeMillis()
1368 result = True
1368 result = True
1369 finally:
1369 finally:
1370 zeroconf.removeListener(self)
1370 zeroconf.removeListener(self)
1371
1371
1372 return result
1372 return result
1373
1373
1374 def __eq__(self, other):
1374 def __eq__(self, other):
1375 """Tests equality of service name"""
1375 """Tests equality of service name"""
1376 if isinstance(other, ServiceInfo):
1376 if isinstance(other, ServiceInfo):
1377 return other.name == self.name
1377 return other.name == self.name
1378 return 0
1378 return 0
1379
1379
1380 def __ne__(self, other):
1380 def __ne__(self, other):
1381 """Non-equality test"""
1381 """Non-equality test"""
1382 return not self.__eq__(other)
1382 return not self.__eq__(other)
1383
1383
1384 def __repr__(self):
1384 def __repr__(self):
1385 """String representation"""
1385 """String representation"""
1386 result = b"service[%s,%s:%s," % (
1386 result = b"service[%s,%s:%s," % (
1387 self.name,
1387 self.name,
1388 socket.inet_ntoa(self.getAddress()),
1388 socket.inet_ntoa(self.getAddress()),
1389 self.port,
1389 self.port,
1390 )
1390 )
1391 if self.text is None:
1391 if self.text is None:
1392 result += b"None"
1392 result += b"None"
1393 else:
1393 else:
1394 if len(self.text) < 20:
1394 if len(self.text) < 20:
1395 result += self.text
1395 result += self.text
1396 else:
1396 else:
1397 result += self.text[:17] + b"..."
1397 result += self.text[:17] + b"..."
1398 result += b"]"
1398 result += b"]"
1399 return result
1399 return result
1400
1400
1401
1401
1402 class Zeroconf:
1402 class Zeroconf:
1403 """Implementation of Zeroconf Multicast DNS Service Discovery
1403 """Implementation of Zeroconf Multicast DNS Service Discovery
1404
1404
1405 Supports registration, unregistration, queries and browsing.
1405 Supports registration, unregistration, queries and browsing.
1406 """
1406 """
1407
1407
1408 def __init__(self, bindaddress=None):
1408 def __init__(self, bindaddress=None):
1409 """Creates an instance of the Zeroconf class, establishing
1409 """Creates an instance of the Zeroconf class, establishing
1410 multicast communications, listening and reaping threads."""
1410 multicast communications, listening and reaping threads."""
1411 globals()[b'_GLOBAL_DONE'] = 0
1411 globals()['_GLOBAL_DONE'] = 0
1412 if bindaddress is None:
1412 if bindaddress is None:
1413 self.intf = socket.gethostbyname(socket.gethostname())
1413 self.intf = socket.gethostbyname(socket.gethostname())
1414 else:
1414 else:
1415 self.intf = bindaddress
1415 self.intf = bindaddress
1416 self.group = (b'', _MDNS_PORT)
1416 self.group = (b'', _MDNS_PORT)
1417 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1417 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1418 try:
1418 try:
1419 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1419 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1420 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1420 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1421 except Exception:
1421 except Exception:
1422 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1422 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1423 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1423 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1424 # Volume 2"), but some BSD-derived systems require
1424 # Volume 2"), but some BSD-derived systems require
1425 # SO_REUSEPORT to be specified explicitly. Also, not all
1425 # SO_REUSEPORT to be specified explicitly. Also, not all
1426 # versions of Python have SO_REUSEPORT available. So
1426 # versions of Python have SO_REUSEPORT available. So
1427 # if you're on a BSD-based system, and haven't upgraded
1427 # if you're on a BSD-based system, and haven't upgraded
1428 # to Python 2.3 yet, you may find this library doesn't
1428 # to Python 2.3 yet, you may find this library doesn't
1429 # work as expected.
1429 # work as expected.
1430 #
1430 #
1431 pass
1431 pass
1432 self.socket.setsockopt(_SOL_IP, socket.IP_MULTICAST_TTL, b"\xff")
1432 self.socket.setsockopt(_SOL_IP, socket.IP_MULTICAST_TTL, b"\xff")
1433 self.socket.setsockopt(_SOL_IP, socket.IP_MULTICAST_LOOP, b"\x01")
1433 self.socket.setsockopt(_SOL_IP, socket.IP_MULTICAST_LOOP, b"\x01")
1434 try:
1434 try:
1435 self.socket.bind(self.group)
1435 self.socket.bind(self.group)
1436 except Exception:
1436 except Exception:
1437 # Some versions of linux raise an exception even though
1437 # Some versions of linux raise an exception even though
1438 # SO_REUSEADDR and SO_REUSEPORT have been set, so ignore it
1438 # SO_REUSEADDR and SO_REUSEPORT have been set, so ignore it
1439 pass
1439 pass
1440 self.socket.setsockopt(
1440 self.socket.setsockopt(
1441 _SOL_IP,
1441 _SOL_IP,
1442 socket.IP_ADD_MEMBERSHIP,
1442 socket.IP_ADD_MEMBERSHIP,
1443 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'),
1443 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'),
1444 )
1444 )
1445
1445
1446 self.listeners = []
1446 self.listeners = []
1447 self.browsers = []
1447 self.browsers = []
1448 self.services = {}
1448 self.services = {}
1449 self.servicetypes = {}
1449 self.servicetypes = {}
1450
1450
1451 self.cache = DNSCache()
1451 self.cache = DNSCache()
1452
1452
1453 self.condition = threading.Condition()
1453 self.condition = threading.Condition()
1454
1454
1455 self.engine = Engine(self)
1455 self.engine = Engine(self)
1456 self.listener = Listener(self)
1456 self.listener = Listener(self)
1457 self.reaper = Reaper(self)
1457 self.reaper = Reaper(self)
1458
1458
1459 def isLoopback(self):
1459 def isLoopback(self):
1460 return self.intf.startswith(b"127.0.0.1")
1460 return self.intf.startswith(b"127.0.0.1")
1461
1461
1462 def isLinklocal(self):
1462 def isLinklocal(self):
1463 return self.intf.startswith(b"169.254.")
1463 return self.intf.startswith(b"169.254.")
1464
1464
1465 def wait(self, timeout):
1465 def wait(self, timeout):
1466 """Calling thread waits for a given number of milliseconds or
1466 """Calling thread waits for a given number of milliseconds or
1467 until notified."""
1467 until notified."""
1468 self.condition.acquire()
1468 self.condition.acquire()
1469 self.condition.wait(timeout / 1000)
1469 self.condition.wait(timeout / 1000)
1470 self.condition.release()
1470 self.condition.release()
1471
1471
1472 def notifyAll(self):
1472 def notifyAll(self):
1473 """Notifies all waiting threads"""
1473 """Notifies all waiting threads"""
1474 self.condition.acquire()
1474 self.condition.acquire()
1475 self.condition.notify_all()
1475 self.condition.notify_all()
1476 self.condition.release()
1476 self.condition.release()
1477
1477
1478 def getServiceInfo(self, type, name, timeout=3000):
1478 def getServiceInfo(self, type, name, timeout=3000):
1479 """Returns network's service information for a particular
1479 """Returns network's service information for a particular
1480 name and type, or None if no service matches by the timeout,
1480 name and type, or None if no service matches by the timeout,
1481 which defaults to 3 seconds."""
1481 which defaults to 3 seconds."""
1482 info = ServiceInfo(type, name)
1482 info = ServiceInfo(type, name)
1483 if info.request(self, timeout):
1483 if info.request(self, timeout):
1484 return info
1484 return info
1485 return None
1485 return None
1486
1486
1487 def addServiceListener(self, type, listener):
1487 def addServiceListener(self, type, listener):
1488 """Adds a listener for a particular service type. This object
1488 """Adds a listener for a particular service type. This object
1489 will then have its updateRecord method called when information
1489 will then have its updateRecord method called when information
1490 arrives for that type."""
1490 arrives for that type."""
1491 self.removeServiceListener(listener)
1491 self.removeServiceListener(listener)
1492 self.browsers.append(ServiceBrowser(self, type, listener))
1492 self.browsers.append(ServiceBrowser(self, type, listener))
1493
1493
1494 def removeServiceListener(self, listener):
1494 def removeServiceListener(self, listener):
1495 """Removes a listener from the set that is currently listening."""
1495 """Removes a listener from the set that is currently listening."""
1496 for browser in self.browsers:
1496 for browser in self.browsers:
1497 if browser.listener == listener:
1497 if browser.listener == listener:
1498 browser.cancel()
1498 browser.cancel()
1499 del browser
1499 del browser
1500
1500
1501 def registerService(self, info, ttl=_DNS_TTL):
1501 def registerService(self, info, ttl=_DNS_TTL):
1502 """Registers service information to the network with a default TTL
1502 """Registers service information to the network with a default TTL
1503 of 60 seconds. Zeroconf will then respond to requests for
1503 of 60 seconds. Zeroconf will then respond to requests for
1504 information for that service. The name of the service may be
1504 information for that service. The name of the service may be
1505 changed if needed to make it unique on the network."""
1505 changed if needed to make it unique on the network."""
1506 self.checkService(info)
1506 self.checkService(info)
1507 self.services[info.name.lower()] = info
1507 self.services[info.name.lower()] = info
1508 if info.type in self.servicetypes:
1508 if info.type in self.servicetypes:
1509 self.servicetypes[info.type] += 1
1509 self.servicetypes[info.type] += 1
1510 else:
1510 else:
1511 self.servicetypes[info.type] = 1
1511 self.servicetypes[info.type] = 1
1512 now = currentTimeMillis()
1512 now = currentTimeMillis()
1513 nexttime = now
1513 nexttime = now
1514 i = 0
1514 i = 0
1515 while i < 3:
1515 while i < 3:
1516 if now < nexttime:
1516 if now < nexttime:
1517 self.wait(nexttime - now)
1517 self.wait(nexttime - now)
1518 now = currentTimeMillis()
1518 now = currentTimeMillis()
1519 continue
1519 continue
1520 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1520 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1521 out.addAnswerAtTime(
1521 out.addAnswerAtTime(
1522 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0
1522 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0
1523 )
1523 )
1524 out.addAnswerAtTime(
1524 out.addAnswerAtTime(
1525 DNSService(
1525 DNSService(
1526 info.name,
1526 info.name,
1527 _TYPE_SRV,
1527 _TYPE_SRV,
1528 _CLASS_IN,
1528 _CLASS_IN,
1529 ttl,
1529 ttl,
1530 info.priority,
1530 info.priority,
1531 info.weight,
1531 info.weight,
1532 info.port,
1532 info.port,
1533 info.server,
1533 info.server,
1534 ),
1534 ),
1535 0,
1535 0,
1536 )
1536 )
1537 out.addAnswerAtTime(
1537 out.addAnswerAtTime(
1538 DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0
1538 DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0
1539 )
1539 )
1540 if info.address:
1540 if info.address:
1541 out.addAnswerAtTime(
1541 out.addAnswerAtTime(
1542 DNSAddress(
1542 DNSAddress(
1543 info.server, _TYPE_A, _CLASS_IN, ttl, info.address
1543 info.server, _TYPE_A, _CLASS_IN, ttl, info.address
1544 ),
1544 ),
1545 0,
1545 0,
1546 )
1546 )
1547 self.send(out)
1547 self.send(out)
1548 i += 1
1548 i += 1
1549 nexttime += _REGISTER_TIME
1549 nexttime += _REGISTER_TIME
1550
1550
1551 def unregisterService(self, info):
1551 def unregisterService(self, info):
1552 """Unregister a service."""
1552 """Unregister a service."""
1553 try:
1553 try:
1554 del self.services[info.name.lower()]
1554 del self.services[info.name.lower()]
1555 if self.servicetypes[info.type] > 1:
1555 if self.servicetypes[info.type] > 1:
1556 self.servicetypes[info.type] -= 1
1556 self.servicetypes[info.type] -= 1
1557 else:
1557 else:
1558 del self.servicetypes[info.type]
1558 del self.servicetypes[info.type]
1559 except KeyError:
1559 except KeyError:
1560 pass
1560 pass
1561 now = currentTimeMillis()
1561 now = currentTimeMillis()
1562 nexttime = now
1562 nexttime = now
1563 i = 0
1563 i = 0
1564 while i < 3:
1564 while i < 3:
1565 if now < nexttime:
1565 if now < nexttime:
1566 self.wait(nexttime - now)
1566 self.wait(nexttime - now)
1567 now = currentTimeMillis()
1567 now = currentTimeMillis()
1568 continue
1568 continue
1569 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1569 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1570 out.addAnswerAtTime(
1570 out.addAnswerAtTime(
1571 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0
1571 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0
1572 )
1572 )
1573 out.addAnswerAtTime(
1573 out.addAnswerAtTime(
1574 DNSService(
1574 DNSService(
1575 info.name,
1575 info.name,
1576 _TYPE_SRV,
1576 _TYPE_SRV,
1577 _CLASS_IN,
1577 _CLASS_IN,
1578 0,
1578 0,
1579 info.priority,
1579 info.priority,
1580 info.weight,
1580 info.weight,
1581 info.port,
1581 info.port,
1582 info.name,
1582 info.name,
1583 ),
1583 ),
1584 0,
1584 0,
1585 )
1585 )
1586 out.addAnswerAtTime(
1586 out.addAnswerAtTime(
1587 DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0
1587 DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0
1588 )
1588 )
1589 if info.address:
1589 if info.address:
1590 out.addAnswerAtTime(
1590 out.addAnswerAtTime(
1591 DNSAddress(
1591 DNSAddress(
1592 info.server, _TYPE_A, _CLASS_IN, 0, info.address
1592 info.server, _TYPE_A, _CLASS_IN, 0, info.address
1593 ),
1593 ),
1594 0,
1594 0,
1595 )
1595 )
1596 self.send(out)
1596 self.send(out)
1597 i += 1
1597 i += 1
1598 nexttime += _UNREGISTER_TIME
1598 nexttime += _UNREGISTER_TIME
1599
1599
1600 def unregisterAllServices(self):
1600 def unregisterAllServices(self):
1601 """Unregister all registered services."""
1601 """Unregister all registered services."""
1602 if len(self.services) > 0:
1602 if len(self.services) > 0:
1603 now = currentTimeMillis()
1603 now = currentTimeMillis()
1604 nexttime = now
1604 nexttime = now
1605 i = 0
1605 i = 0
1606 while i < 3:
1606 while i < 3:
1607 if now < nexttime:
1607 if now < nexttime:
1608 self.wait(nexttime - now)
1608 self.wait(nexttime - now)
1609 now = currentTimeMillis()
1609 now = currentTimeMillis()
1610 continue
1610 continue
1611 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1611 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1612 for info in self.services.values():
1612 for info in self.services.values():
1613 out.addAnswerAtTime(
1613 out.addAnswerAtTime(
1614 DNSPointer(
1614 DNSPointer(
1615 info.type, _TYPE_PTR, _CLASS_IN, 0, info.name
1615 info.type, _TYPE_PTR, _CLASS_IN, 0, info.name
1616 ),
1616 ),
1617 0,
1617 0,
1618 )
1618 )
1619 out.addAnswerAtTime(
1619 out.addAnswerAtTime(
1620 DNSService(
1620 DNSService(
1621 info.name,
1621 info.name,
1622 _TYPE_SRV,
1622 _TYPE_SRV,
1623 _CLASS_IN,
1623 _CLASS_IN,
1624 0,
1624 0,
1625 info.priority,
1625 info.priority,
1626 info.weight,
1626 info.weight,
1627 info.port,
1627 info.port,
1628 info.server,
1628 info.server,
1629 ),
1629 ),
1630 0,
1630 0,
1631 )
1631 )
1632 out.addAnswerAtTime(
1632 out.addAnswerAtTime(
1633 DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text),
1633 DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text),
1634 0,
1634 0,
1635 )
1635 )
1636 if info.address:
1636 if info.address:
1637 out.addAnswerAtTime(
1637 out.addAnswerAtTime(
1638 DNSAddress(
1638 DNSAddress(
1639 info.server, _TYPE_A, _CLASS_IN, 0, info.address
1639 info.server, _TYPE_A, _CLASS_IN, 0, info.address
1640 ),
1640 ),
1641 0,
1641 0,
1642 )
1642 )
1643 self.send(out)
1643 self.send(out)
1644 i += 1
1644 i += 1
1645 nexttime += _UNREGISTER_TIME
1645 nexttime += _UNREGISTER_TIME
1646
1646
1647 def checkService(self, info):
1647 def checkService(self, info):
1648 """Checks the network for a unique service name, modifying the
1648 """Checks the network for a unique service name, modifying the
1649 ServiceInfo passed in if it is not unique."""
1649 ServiceInfo passed in if it is not unique."""
1650 now = currentTimeMillis()
1650 now = currentTimeMillis()
1651 nexttime = now
1651 nexttime = now
1652 i = 0
1652 i = 0
1653 while i < 3:
1653 while i < 3:
1654 for record in self.cache.entriesWithName(info.type):
1654 for record in self.cache.entriesWithName(info.type):
1655 if (
1655 if (
1656 record.type == _TYPE_PTR
1656 record.type == _TYPE_PTR
1657 and not record.isExpired(now)
1657 and not record.isExpired(now)
1658 and record.alias == info.name
1658 and record.alias == info.name
1659 ):
1659 ):
1660 if info.name.find(b'.') < 0:
1660 if info.name.find(b'.') < 0:
1661 info.name = b"%s.[%s:%d].%s" % (
1661 info.name = b"%s.[%s:%d].%s" % (
1662 info.name,
1662 info.name,
1663 info.address,
1663 info.address,
1664 info.port,
1664 info.port,
1665 info.type,
1665 info.type,
1666 )
1666 )
1667 self.checkService(info)
1667 self.checkService(info)
1668 return
1668 return
1669 raise NonUniqueNameException
1669 raise NonUniqueNameException
1670 if now < nexttime:
1670 if now < nexttime:
1671 self.wait(nexttime - now)
1671 self.wait(nexttime - now)
1672 now = currentTimeMillis()
1672 now = currentTimeMillis()
1673 continue
1673 continue
1674 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1674 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1675 self.debug = out
1675 self.debug = out
1676 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1676 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1677 out.addAuthoritativeAnswer(
1677 out.addAuthoritativeAnswer(
1678 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name)
1678 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name)
1679 )
1679 )
1680 self.send(out)
1680 self.send(out)
1681 i += 1
1681 i += 1
1682 nexttime += _CHECK_TIME
1682 nexttime += _CHECK_TIME
1683
1683
1684 def addListener(self, listener, question):
1684 def addListener(self, listener, question):
1685 """Adds a listener for a given question. The listener will have
1685 """Adds a listener for a given question. The listener will have
1686 its updateRecord method called when information is available to
1686 its updateRecord method called when information is available to
1687 answer the question."""
1687 answer the question."""
1688 now = currentTimeMillis()
1688 now = currentTimeMillis()
1689 self.listeners.append(listener)
1689 self.listeners.append(listener)
1690 if question is not None:
1690 if question is not None:
1691 for record in self.cache.entriesWithName(question.name):
1691 for record in self.cache.entriesWithName(question.name):
1692 if question.answeredBy(record) and not record.isExpired(now):
1692 if question.answeredBy(record) and not record.isExpired(now):
1693 listener.updateRecord(self, now, record)
1693 listener.updateRecord(self, now, record)
1694 self.notifyAll()
1694 self.notifyAll()
1695
1695
1696 def removeListener(self, listener):
1696 def removeListener(self, listener):
1697 """Removes a listener."""
1697 """Removes a listener."""
1698 try:
1698 try:
1699 self.listeners.remove(listener)
1699 self.listeners.remove(listener)
1700 self.notifyAll()
1700 self.notifyAll()
1701 except Exception:
1701 except Exception:
1702 pass
1702 pass
1703
1703
1704 def updateRecord(self, now, rec):
1704 def updateRecord(self, now, rec):
1705 """Used to notify listeners of new information that has updated
1705 """Used to notify listeners of new information that has updated
1706 a record."""
1706 a record."""
1707 for listener in self.listeners:
1707 for listener in self.listeners:
1708 listener.updateRecord(self, now, rec)
1708 listener.updateRecord(self, now, rec)
1709 self.notifyAll()
1709 self.notifyAll()
1710
1710
1711 def handleResponse(self, msg):
1711 def handleResponse(self, msg):
1712 """Deal with incoming response packets. All answers
1712 """Deal with incoming response packets. All answers
1713 are held in the cache, and listeners are notified."""
1713 are held in the cache, and listeners are notified."""
1714 now = currentTimeMillis()
1714 now = currentTimeMillis()
1715 for record in msg.answers:
1715 for record in msg.answers:
1716 expired = record.isExpired(now)
1716 expired = record.isExpired(now)
1717 if record in self.cache.entries():
1717 if record in self.cache.entries():
1718 if expired:
1718 if expired:
1719 self.cache.remove(record)
1719 self.cache.remove(record)
1720 else:
1720 else:
1721 entry = self.cache.get(record)
1721 entry = self.cache.get(record)
1722 if entry is not None:
1722 if entry is not None:
1723 entry.resetTTL(record)
1723 entry.resetTTL(record)
1724 record = entry
1724 record = entry
1725 else:
1725 else:
1726 self.cache.add(record)
1726 self.cache.add(record)
1727
1727
1728 self.updateRecord(now, record)
1728 self.updateRecord(now, record)
1729
1729
1730 def handleQuery(self, msg, addr, port):
1730 def handleQuery(self, msg, addr, port):
1731 """Deal with incoming query packets. Provides a response if
1731 """Deal with incoming query packets. Provides a response if
1732 possible."""
1732 possible."""
1733 out = None
1733 out = None
1734
1734
1735 # Support unicast client responses
1735 # Support unicast client responses
1736 #
1736 #
1737 if port != _MDNS_PORT:
1737 if port != _MDNS_PORT:
1738 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
1738 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
1739 for question in msg.questions:
1739 for question in msg.questions:
1740 out.addQuestion(question)
1740 out.addQuestion(question)
1741
1741
1742 for question in msg.questions:
1742 for question in msg.questions:
1743 if question.type == _TYPE_PTR:
1743 if question.type == _TYPE_PTR:
1744 if question.name == b"_services._dns-sd._udp.local.":
1744 if question.name == b"_services._dns-sd._udp.local.":
1745 for stype in self.servicetypes.keys():
1745 for stype in self.servicetypes.keys():
1746 if out is None:
1746 if out is None:
1747 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1747 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1748 out.addAnswer(
1748 out.addAnswer(
1749 msg,
1749 msg,
1750 DNSPointer(
1750 DNSPointer(
1751 b"_services._dns-sd._udp.local.",
1751 b"_services._dns-sd._udp.local.",
1752 _TYPE_PTR,
1752 _TYPE_PTR,
1753 _CLASS_IN,
1753 _CLASS_IN,
1754 _DNS_TTL,
1754 _DNS_TTL,
1755 stype,
1755 stype,
1756 ),
1756 ),
1757 )
1757 )
1758 for service in self.services.values():
1758 for service in self.services.values():
1759 if question.name == service.type:
1759 if question.name == service.type:
1760 if out is None:
1760 if out is None:
1761 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1761 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1762 out.addAnswer(
1762 out.addAnswer(
1763 msg,
1763 msg,
1764 DNSPointer(
1764 DNSPointer(
1765 service.type,
1765 service.type,
1766 _TYPE_PTR,
1766 _TYPE_PTR,
1767 _CLASS_IN,
1767 _CLASS_IN,
1768 _DNS_TTL,
1768 _DNS_TTL,
1769 service.name,
1769 service.name,
1770 ),
1770 ),
1771 )
1771 )
1772 else:
1772 else:
1773 try:
1773 try:
1774 if out is None:
1774 if out is None:
1775 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1775 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1776
1776
1777 # Answer A record queries for any service addresses we know
1777 # Answer A record queries for any service addresses we know
1778 if question.type == _TYPE_A or question.type == _TYPE_ANY:
1778 if question.type == _TYPE_A or question.type == _TYPE_ANY:
1779 for service in self.services.values():
1779 for service in self.services.values():
1780 if service.server == question.name.lower():
1780 if service.server == question.name.lower():
1781 out.addAnswer(
1781 out.addAnswer(
1782 msg,
1782 msg,
1783 DNSAddress(
1783 DNSAddress(
1784 question.name,
1784 question.name,
1785 _TYPE_A,
1785 _TYPE_A,
1786 _CLASS_IN | _CLASS_UNIQUE,
1786 _CLASS_IN | _CLASS_UNIQUE,
1787 _DNS_TTL,
1787 _DNS_TTL,
1788 service.address,
1788 service.address,
1789 ),
1789 ),
1790 )
1790 )
1791
1791
1792 service = self.services.get(question.name.lower(), None)
1792 service = self.services.get(question.name.lower(), None)
1793 if not service:
1793 if not service:
1794 continue
1794 continue
1795
1795
1796 if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
1796 if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
1797 out.addAnswer(
1797 out.addAnswer(
1798 msg,
1798 msg,
1799 DNSService(
1799 DNSService(
1800 question.name,
1800 question.name,
1801 _TYPE_SRV,
1801 _TYPE_SRV,
1802 _CLASS_IN | _CLASS_UNIQUE,
1802 _CLASS_IN | _CLASS_UNIQUE,
1803 _DNS_TTL,
1803 _DNS_TTL,
1804 service.priority,
1804 service.priority,
1805 service.weight,
1805 service.weight,
1806 service.port,
1806 service.port,
1807 service.server,
1807 service.server,
1808 ),
1808 ),
1809 )
1809 )
1810 if question.type == _TYPE_TXT or question.type == _TYPE_ANY:
1810 if question.type == _TYPE_TXT or question.type == _TYPE_ANY:
1811 out.addAnswer(
1811 out.addAnswer(
1812 msg,
1812 msg,
1813 DNSText(
1813 DNSText(
1814 question.name,
1814 question.name,
1815 _TYPE_TXT,
1815 _TYPE_TXT,
1816 _CLASS_IN | _CLASS_UNIQUE,
1816 _CLASS_IN | _CLASS_UNIQUE,
1817 _DNS_TTL,
1817 _DNS_TTL,
1818 service.text,
1818 service.text,
1819 ),
1819 ),
1820 )
1820 )
1821 if question.type == _TYPE_SRV:
1821 if question.type == _TYPE_SRV:
1822 out.addAdditionalAnswer(
1822 out.addAdditionalAnswer(
1823 DNSAddress(
1823 DNSAddress(
1824 service.server,
1824 service.server,
1825 _TYPE_A,
1825 _TYPE_A,
1826 _CLASS_IN | _CLASS_UNIQUE,
1826 _CLASS_IN | _CLASS_UNIQUE,
1827 _DNS_TTL,
1827 _DNS_TTL,
1828 service.address,
1828 service.address,
1829 )
1829 )
1830 )
1830 )
1831 except Exception:
1831 except Exception:
1832 traceback.print_exc()
1832 traceback.print_exc()
1833
1833
1834 if out is not None and out.answers:
1834 if out is not None and out.answers:
1835 out.id = msg.id
1835 out.id = msg.id
1836 self.send(out, addr, port)
1836 self.send(out, addr, port)
1837
1837
1838 def send(self, out, addr=_MDNS_ADDR, port=_MDNS_PORT):
1838 def send(self, out, addr=_MDNS_ADDR, port=_MDNS_PORT):
1839 """Sends an outgoing packet."""
1839 """Sends an outgoing packet."""
1840 # This is a quick test to see if we can parse the packets we generate
1840 # This is a quick test to see if we can parse the packets we generate
1841 # temp = DNSIncoming(out.packet())
1841 # temp = DNSIncoming(out.packet())
1842 try:
1842 try:
1843 self.socket.sendto(out.packet(), 0, (addr, port))
1843 self.socket.sendto(out.packet(), 0, (addr, port))
1844 except Exception:
1844 except Exception:
1845 # Ignore this, it may be a temporary loss of network connection
1845 # Ignore this, it may be a temporary loss of network connection
1846 pass
1846 pass
1847
1847
1848 def close(self):
1848 def close(self):
1849 """Ends the background threads, and prevent this instance from
1849 """Ends the background threads, and prevent this instance from
1850 servicing further queries."""
1850 servicing further queries."""
1851 if globals()[b'_GLOBAL_DONE'] == 0:
1851 if globals()['_GLOBAL_DONE'] == 0:
1852 globals()[b'_GLOBAL_DONE'] = 1
1852 globals()['_GLOBAL_DONE'] = 1
1853 self.notifyAll()
1853 self.notifyAll()
1854 self.engine.notify()
1854 self.engine.notify()
1855 self.unregisterAllServices()
1855 self.unregisterAllServices()
1856 self.socket.setsockopt(
1856 self.socket.setsockopt(
1857 _SOL_IP,
1857 _SOL_IP,
1858 socket.IP_DROP_MEMBERSHIP,
1858 socket.IP_DROP_MEMBERSHIP,
1859 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'),
1859 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'),
1860 )
1860 )
1861 self.socket.close()
1861 self.socket.close()
1862
1862
1863
1863
1864 # Test a few module features, including service registration, service
1864 # Test a few module features, including service registration, service
1865 # query (for Zoe), and service unregistration.
1865 # query (for Zoe), and service unregistration.
1866
1866
1867 if __name__ == '__main__':
1867 if __name__ == '__main__':
1868 print(b"Multicast DNS Service Discovery for Python, version", __version__)
1868 print(b"Multicast DNS Service Discovery for Python, version", __version__)
1869 r = Zeroconf()
1869 r = Zeroconf()
1870 print(b"1. Testing registration of a service...")
1870 print(b"1. Testing registration of a service...")
1871 desc = {b'version': b'0.10', b'a': b'test value', b'b': b'another value'}
1871 desc = {b'version': b'0.10', b'a': b'test value', b'b': b'another value'}
1872 info = ServiceInfo(
1872 info = ServiceInfo(
1873 b"_http._tcp.local.",
1873 b"_http._tcp.local.",
1874 b"My Service Name._http._tcp.local.",
1874 b"My Service Name._http._tcp.local.",
1875 socket.inet_aton("127.0.0.1"),
1875 socket.inet_aton("127.0.0.1"),
1876 1234,
1876 1234,
1877 0,
1877 0,
1878 0,
1878 0,
1879 desc,
1879 desc,
1880 )
1880 )
1881 print(b" Registering service...")
1881 print(b" Registering service...")
1882 r.registerService(info)
1882 r.registerService(info)
1883 print(b" Registration done.")
1883 print(b" Registration done.")
1884 print(b"2. Testing query of service information...")
1884 print(b"2. Testing query of service information...")
1885 print(
1885 print(
1886 b" Getting ZOE service:",
1886 b" Getting ZOE service:",
1887 str(r.getServiceInfo(b"_http._tcp.local.", b"ZOE._http._tcp.local.")),
1887 str(r.getServiceInfo(b"_http._tcp.local.", b"ZOE._http._tcp.local.")),
1888 )
1888 )
1889 print(b" Query done.")
1889 print(b" Query done.")
1890 print(b"3. Testing query of own service...")
1890 print(b"3. Testing query of own service...")
1891 print(
1891 print(
1892 b" Getting self:",
1892 b" Getting self:",
1893 str(
1893 str(
1894 r.getServiceInfo(
1894 r.getServiceInfo(
1895 b"_http._tcp.local.", b"My Service Name._http._tcp.local."
1895 b"_http._tcp.local.", b"My Service Name._http._tcp.local."
1896 )
1896 )
1897 ),
1897 ),
1898 )
1898 )
1899 print(b" Query done.")
1899 print(b" Query done.")
1900 print(b"4. Testing unregister of service information...")
1900 print(b"4. Testing unregister of service information...")
1901 r.unregisterService(info)
1901 r.unregisterService(info)
1902 print(b" Unregister done.")
1902 print(b" Unregister done.")
1903 r.close()
1903 r.close()
General Comments 0
You need to be logged in to leave comments. Login now