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