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