mimetype.guess_type() returns a tuple, and it might be (None, None).
[pyTivo/TheBayer.git] / Zeroconf.py
blobe6c227a8739c442a636afe9ceac1de4a3f3bce67
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 return unicode(self.data[offset:offset+len], 'utf-8', 'replace')
576 def readName(self):
577 """Reads a domain name from the packet"""
578 result = ''
579 off = self.offset
580 next = -1
581 first = off
583 while 1:
584 len = ord(self.data[off])
585 off += 1
586 if len == 0:
587 break
588 t = len & 0xC0
589 if t == 0x00:
590 result = ''.join((result, self.readUTF(off, len) + '.'))
591 off += len
592 elif t == 0xC0:
593 if next < 0:
594 next = off + 1
595 off = ((len & 0x3F) << 8) | ord(self.data[off])
596 if off >= first:
597 raise "Bad domain name (circular) at " + str(off)
598 first = off
599 else:
600 raise "Bad domain name at " + str(off)
602 if next >= 0:
603 self.offset = next
604 else:
605 self.offset = off
607 return result
610 class DNSOutgoing(object):
611 """Object representation of an outgoing packet"""
613 def __init__(self, flags, multicast = 1):
614 self.finished = 0
615 self.id = 0
616 self.multicast = multicast
617 self.flags = flags
618 self.names = {}
619 self.data = []
620 self.size = 12
622 self.questions = []
623 self.answers = []
624 self.authorities = []
625 self.additionals = []
627 def addQuestion(self, record):
628 """Adds a question"""
629 self.questions.append(record)
631 def addAnswer(self, inp, record):
632 """Adds an answer"""
633 if not record.suppressedBy(inp):
634 self.addAnswerAtTime(record, 0)
636 def addAnswerAtTime(self, record, now):
637 """Adds an answer if if does not expire by a certain time"""
638 if record is not None:
639 if now == 0 or not record.isExpired(now):
640 self.answers.append((record, now))
642 def addAuthorativeAnswer(self, record):
643 """Adds an authoritative answer"""
644 self.authorities.append(record)
646 def addAdditionalAnswer(self, record):
647 """Adds an additional answer"""
648 self.additionals.append(record)
650 def writeByte(self, value):
651 """Writes a single byte to the packet"""
652 format = '!c'
653 self.data.append(struct.pack(format, chr(value)))
654 self.size += 1
656 def insertShort(self, index, value):
657 """Inserts an unsigned short in a certain position in the packet"""
658 format = '!H'
659 self.data.insert(index, struct.pack(format, value))
660 self.size += 2
662 def writeShort(self, value):
663 """Writes an unsigned short to the packet"""
664 format = '!H'
665 self.data.append(struct.pack(format, value))
666 self.size += 2
668 def writeInt(self, value):
669 """Writes an unsigned integer to the packet"""
670 format = '!I'
671 self.data.append(struct.pack(format, int(value)))
672 self.size += 4
674 def writeString(self, value, length):
675 """Writes a string to the packet"""
676 format = '!' + str(length) + 's'
677 self.data.append(struct.pack(format, value))
678 self.size += length
680 def writeUTF(self, s):
681 """Writes a UTF-8 string of a given length to the packet"""
682 utfstr = s.encode('utf-8')
683 length = len(utfstr)
684 if length > 64:
685 raise NamePartTooLongException
686 self.writeByte(length)
687 self.writeString(utfstr, length)
689 def writeName(self, name):
690 """Writes a domain name to the packet"""
692 try:
693 # Find existing instance of this name in packet
695 index = self.names[name]
696 except KeyError:
697 # No record of this name already, so write it
698 # out as normal, recording the location of the name
699 # for future pointers to it.
701 self.names[name] = self.size
702 parts = name.split('.')
703 if parts[-1] == '':
704 parts = parts[:-1]
705 for part in parts:
706 self.writeUTF(part)
707 self.writeByte(0)
708 return
710 # An index was found, so write a pointer to it
712 self.writeByte((index >> 8) | 0xC0)
713 self.writeByte(index)
715 def writeQuestion(self, question):
716 """Writes a question to the packet"""
717 self.writeName(question.name)
718 self.writeShort(question.type)
719 self.writeShort(question.clazz)
721 def writeRecord(self, record, now):
722 """Writes a record (answer, authoritative answer, additional) to
723 the packet"""
724 self.writeName(record.name)
725 self.writeShort(record.type)
726 if record.unique and self.multicast:
727 self.writeShort(record.clazz | _CLASS_UNIQUE)
728 else:
729 self.writeShort(record.clazz)
730 if now == 0:
731 self.writeInt(record.ttl)
732 else:
733 self.writeInt(record.getRemainingTTL(now))
734 index = len(self.data)
735 # Adjust size for the short we will write before this record
737 self.size += 2
738 record.write(self)
739 self.size -= 2
741 length = len(''.join(self.data[index:]))
742 self.insertShort(index, length) # Here is the short we adjusted for
744 def packet(self):
745 """Returns a string containing the packet's bytes
747 No further parts should be added to the packet once this
748 is done."""
749 if not self.finished:
750 self.finished = 1
751 for question in self.questions:
752 self.writeQuestion(question)
753 for answer, time in self.answers:
754 self.writeRecord(answer, time)
755 for authority in self.authorities:
756 self.writeRecord(authority, 0)
757 for additional in self.additionals:
758 self.writeRecord(additional, 0)
760 self.insertShort(0, len(self.additionals))
761 self.insertShort(0, len(self.authorities))
762 self.insertShort(0, len(self.answers))
763 self.insertShort(0, len(self.questions))
764 self.insertShort(0, self.flags)
765 if self.multicast:
766 self.insertShort(0, 0)
767 else:
768 self.insertShort(0, self.id)
769 return ''.join(self.data)
772 class DNSCache(object):
773 """A cache of DNS entries"""
775 def __init__(self):
776 self.cache = {}
778 def add(self, entry):
779 """Adds an entry"""
780 try:
781 list = self.cache[entry.key]
782 except:
783 list = self.cache[entry.key] = []
784 list.append(entry)
786 def remove(self, entry):
787 """Removes an entry"""
788 try:
789 list = self.cache[entry.key]
790 list.remove(entry)
791 except:
792 pass
794 def get(self, entry):
795 """Gets an entry by key. Will return None if there is no
796 matching entry."""
797 try:
798 list = self.cache[entry.key]
799 return list[list.index(entry)]
800 except:
801 return None
803 def getByDetails(self, name, type, clazz):
804 """Gets an entry by details. Will return None if there is
805 no matching entry."""
806 entry = DNSEntry(name, type, clazz)
807 return self.get(entry)
809 def entriesWithName(self, name):
810 """Returns a list of entries whose key matches the name."""
811 try:
812 return self.cache[name]
813 except:
814 return []
816 def entries(self):
817 """Returns a list of all entries"""
818 def add(x, y): return x+y
819 try:
820 return reduce(add, self.cache.values())
821 except:
822 return []
825 class Engine(threading.Thread):
826 """An engine wraps read access to sockets, allowing objects that
827 need to receive data from sockets to be called back when the
828 sockets are ready.
830 A reader needs a handle_read() method, which is called when the socket
831 it is interested in is ready for reading.
833 Writers are not implemented here, because we only send short
834 packets.
837 def __init__(self, zeroconf):
838 threading.Thread.__init__(self)
839 self.zeroconf = zeroconf
840 self.readers = {} # maps socket to reader
841 self.timeout = 5
842 self.condition = threading.Condition()
843 self.start()
845 def run(self):
846 while not globals()['_GLOBAL_DONE']:
847 rs = self.getReaders()
848 if len(rs) == 0:
849 # No sockets to manage, but we wait for the timeout
850 # or addition of a socket
852 self.condition.acquire()
853 self.condition.wait(self.timeout)
854 self.condition.release()
855 else:
856 try:
857 rr, wr, er = select.select(rs, [], [], self.timeout)
858 for socket in rr:
859 try:
860 self.readers[socket].handle_read()
861 except:
862 traceback.print_exc()
863 except:
864 pass
866 def getReaders(self):
867 result = []
868 self.condition.acquire()
869 result = self.readers.keys()
870 self.condition.release()
871 return result
873 def addReader(self, reader, socket):
874 self.condition.acquire()
875 self.readers[socket] = reader
876 self.condition.notify()
877 self.condition.release()
879 def delReader(self, socket):
880 self.condition.acquire()
881 del(self.readers[socket])
882 self.condition.notify()
883 self.condition.release()
885 def notify(self):
886 self.condition.acquire()
887 self.condition.notify()
888 self.condition.release()
890 class Listener(object):
891 """A Listener is used by this module to listen on the multicast
892 group to which DNS messages are sent, allowing the implementation
893 to cache information as it arrives.
895 It requires registration with an Engine object in order to have
896 the read() method called when a socket is availble for reading."""
898 def __init__(self, zeroconf):
899 self.zeroconf = zeroconf
900 self.zeroconf.engine.addReader(self, self.zeroconf.socket)
902 def handle_read(self):
903 try:
904 data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE)
905 except socket.error, e:
906 # If the socket was closed by another thread -- which happens
907 # regularly on shutdown -- an EBADF exception is thrown here.
908 # Ignore it.
909 if e[0] == socket.EBADF:
910 return
911 else:
912 raise e
913 self.data = data
914 msg = DNSIncoming(data)
915 if msg.isQuery():
916 # Always multicast responses
918 if port == _MDNS_PORT:
919 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
920 # If it's not a multicast query, reply via unicast
921 # and multicast
923 elif port == _DNS_PORT:
924 self.zeroconf.handleQuery(msg, addr, port)
925 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
926 else:
927 self.zeroconf.handleResponse(msg)
930 class Reaper(threading.Thread):
931 """A Reaper is used by this module to remove cache entries that
932 have expired."""
934 def __init__(self, zeroconf):
935 threading.Thread.__init__(self)
936 self.zeroconf = zeroconf
937 self.start()
939 def run(self):
940 while 1:
941 self.zeroconf.wait(10 * 1000)
942 if globals()['_GLOBAL_DONE']:
943 return
944 now = currentTimeMillis()
945 for record in self.zeroconf.cache.entries():
946 if record.isExpired(now):
947 self.zeroconf.updateRecord(now, record)
948 self.zeroconf.cache.remove(record)
951 class ServiceBrowser(threading.Thread):
952 """Used to browse for a service of a specific type.
954 The listener object will have its addService() and
955 removeService() methods called when this browser
956 discovers changes in the services availability."""
958 def __init__(self, zeroconf, type, listener):
959 """Creates a browser for a specific type"""
960 threading.Thread.__init__(self)
961 self.zeroconf = zeroconf
962 self.type = type
963 self.listener = listener
964 self.services = {}
965 self.nextTime = currentTimeMillis()
966 self.delay = _BROWSER_TIME
967 self.list = []
969 self.done = 0
971 self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
972 self.start()
974 def updateRecord(self, zeroconf, now, record):
975 """Callback invoked by Zeroconf when new information arrives.
977 Updates information required by browser in the Zeroconf cache."""
978 if record.type == _TYPE_PTR and record.name == self.type:
979 expired = record.isExpired(now)
980 try:
981 oldrecord = self.services[record.alias.lower()]
982 if not expired:
983 oldrecord.resetTTL(record)
984 else:
985 del(self.services[record.alias.lower()])
986 callback = lambda x: self.listener.removeService(x, self.type, record.alias)
987 self.list.append(callback)
988 return
989 except:
990 if not expired:
991 self.services[record.alias.lower()] = record
992 callback = lambda x: self.listener.addService(x, self.type, record.alias)
993 self.list.append(callback)
995 expires = record.getExpirationTime(75)
996 if expires < self.nextTime:
997 self.nextTime = expires
999 def cancel(self):
1000 self.done = 1
1001 self.zeroconf.notifyAll()
1003 def run(self):
1004 while 1:
1005 event = None
1006 now = currentTimeMillis()
1007 if len(self.list) == 0 and self.nextTime > now:
1008 self.zeroconf.wait(self.nextTime - now)
1009 if globals()['_GLOBAL_DONE'] or self.done:
1010 return
1011 now = currentTimeMillis()
1013 if self.nextTime <= now:
1014 out = DNSOutgoing(_FLAGS_QR_QUERY)
1015 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
1016 for record in self.services.values():
1017 if not record.isExpired(now):
1018 out.addAnswerAtTime(record, now)
1019 self.zeroconf.send(out)
1020 self.nextTime = now + self.delay
1021 self.delay = min(20 * 1000, self.delay * 2)
1023 if len(self.list) > 0:
1024 event = self.list.pop(0)
1026 if event is not None:
1027 event(self.zeroconf)
1030 class ServiceInfo(object):
1031 """Service information"""
1033 def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None):
1034 """Create a service description.
1036 type: fully qualified service type name
1037 name: fully qualified service name
1038 address: IP address as unsigned short, network byte order
1039 port: port that the service runs on
1040 weight: weight of the service
1041 priority: priority of the service
1042 properties: dictionary of properties (or a string holding the bytes for the text field)
1043 server: fully qualified name for service host (defaults to name)"""
1045 if not name.endswith(type):
1046 raise BadTypeInNameException
1047 self.type = type
1048 self.name = name
1049 self.address = address
1050 self.port = port
1051 self.weight = weight
1052 self.priority = priority
1053 if server:
1054 self.server = server
1055 else:
1056 self.server = name
1057 self.setProperties(properties)
1059 def setProperties(self, properties):
1060 """Sets properties and text of this info from a dictionary"""
1061 if isinstance(properties, dict):
1062 self.properties = properties
1063 list = []
1064 result = ''
1065 for key in properties:
1066 value = properties[key]
1067 if value is None:
1068 suffix = ''.encode('utf-8')
1069 elif isinstance(value, str):
1070 suffix = value.encode('utf-8')
1071 elif isinstance(value, int):
1072 if value:
1073 suffix = 'true'
1074 else:
1075 suffix = 'false'
1076 else:
1077 suffix = ''.encode('utf-8')
1078 list.append('='.join((key, suffix)))
1079 for item in list:
1080 result = ''.join((result, struct.pack('!c', chr(len(item))), item))
1081 self.text = result
1082 else:
1083 self.text = properties
1085 def setText(self, text):
1086 """Sets properties and text given a text field"""
1087 self.text = text
1088 try:
1089 result = {}
1090 end = len(text)
1091 index = 0
1092 strs = []
1093 while index < end:
1094 length = ord(text[index])
1095 index += 1
1096 strs.append(text[index:index+length])
1097 index += length
1099 for s in strs:
1100 eindex = s.find('=')
1101 if eindex == -1:
1102 # No equals sign at all
1103 key = s
1104 value = 0
1105 else:
1106 key = s[:eindex]
1107 value = s[eindex+1:]
1108 if value == 'true':
1109 value = 1
1110 elif value == 'false' or not value:
1111 value = 0
1113 # Only update non-existent properties
1114 if key and result.get(key) == None:
1115 result[key] = value
1117 self.properties = result
1118 except:
1119 traceback.print_exc()
1120 self.properties = None
1122 def getType(self):
1123 """Type accessor"""
1124 return self.type
1126 def getName(self):
1127 """Name accessor"""
1128 if self.type is not None and self.name.endswith("." + self.type):
1129 return self.name[:len(self.name) - len(self.type) - 1]
1130 return self.name
1132 def getAddress(self):
1133 """Address accessor"""
1134 return self.address
1136 def getPort(self):
1137 """Port accessor"""
1138 return self.port
1140 def getPriority(self):
1141 """Pirority accessor"""
1142 return self.priority
1144 def getWeight(self):
1145 """Weight accessor"""
1146 return self.weight
1148 def getProperties(self):
1149 """Properties accessor"""
1150 return self.properties
1152 def getText(self):
1153 """Text accessor"""
1154 return self.text
1156 def getServer(self):
1157 """Server accessor"""
1158 return self.server
1160 def updateRecord(self, zeroconf, now, record):
1161 """Updates service information from a DNS record"""
1162 if record is not None and not record.isExpired(now):
1163 if record.type == _TYPE_A:
1164 #if record.name == self.name:
1165 if record.name == self.server:
1166 self.address = record.address
1167 elif record.type == _TYPE_SRV:
1168 if record.name == self.name:
1169 self.server = record.server
1170 self.port = record.port
1171 self.weight = record.weight
1172 self.priority = record.priority
1173 #self.address = None
1174 self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
1175 elif record.type == _TYPE_TXT:
1176 if record.name == self.name:
1177 self.setText(record.text)
1179 def request(self, zeroconf, timeout):
1180 """Returns true if the service could be discovered on the
1181 network, and updates this object with details discovered.
1183 now = currentTimeMillis()
1184 delay = _LISTENER_TIME
1185 next = now + delay
1186 last = now + timeout
1187 result = 0
1188 try:
1189 zeroconf.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
1190 while self.server is None or self.address is None or self.text is None:
1191 if last <= now:
1192 return 0
1193 if next <= now:
1194 out = DNSOutgoing(_FLAGS_QR_QUERY)
1195 out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
1196 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_SRV, _CLASS_IN), now)
1197 out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
1198 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_TXT, _CLASS_IN), now)
1199 if self.server is not None:
1200 out.addQuestion(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
1201 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN), now)
1202 zeroconf.send(out)
1203 next = now + delay
1204 delay = delay * 2
1206 zeroconf.wait(min(next, last) - now)
1207 now = currentTimeMillis()
1208 result = 1
1209 finally:
1210 zeroconf.removeListener(self)
1212 return result
1214 def __eq__(self, other):
1215 """Tests equality of service name"""
1216 if isinstance(other, ServiceInfo):
1217 return other.name == self.name
1218 return 0
1220 def __ne__(self, other):
1221 """Non-equality test"""
1222 return not self.__eq__(other)
1224 def __repr__(self):
1225 """String representation"""
1226 result = "service[%s,%s:%s," % (self.name, socket.inet_ntoa(self.getAddress()), self.port)
1227 if self.text is None:
1228 result += "None"
1229 else:
1230 if len(self.text) < 20:
1231 result += self.text
1232 else:
1233 result += self.text[:17] + "..."
1234 result += "]"
1235 return result
1238 class Zeroconf(object):
1239 """Implementation of Zeroconf Multicast DNS Service Discovery
1241 Supports registration, unregistration, queries and browsing.
1243 def __init__(self, bindaddress=None):
1244 """Creates an instance of the Zeroconf class, establishing
1245 multicast communications, listening and reaping threads."""
1246 globals()['_GLOBAL_DONE'] = 0
1247 if bindaddress is None:
1248 self.intf = socket.gethostbyname(socket.gethostname())
1249 else:
1250 self.intf = bindaddress
1251 self.group = ('', _MDNS_PORT)
1252 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1253 try:
1254 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1255 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1256 except:
1257 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1258 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1259 # Volume 2"), but some BSD-derived systems require
1260 # SO_REUSEPORT to be specified explicity. Also, not all
1261 # versions of Python have SO_REUSEPORT available. So
1262 # if you're on a BSD-based system, and haven't upgraded
1263 # to Python 2.3 yet, you may find this library doesn't
1264 # work as expected.
1266 pass
1267 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
1268 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
1269 try:
1270 self.socket.bind(self.group)
1271 except:
1272 # Some versions of linux raise an exception even though
1273 # the SO_REUSE* options have been set, so ignore it
1275 pass
1276 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1277 self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1279 self.listeners = []
1280 self.browsers = []
1281 self.services = {}
1282 self.servicetypes = {}
1284 self.cache = DNSCache()
1286 self.condition = threading.Condition()
1288 self.engine = Engine(self)
1289 self.listener = Listener(self)
1290 self.reaper = Reaper(self)
1292 def isLoopback(self):
1293 return self.intf.startswith("127.0.0.1")
1295 def isLinklocal(self):
1296 return self.intf.startswith("169.254.")
1298 def wait(self, timeout):
1299 """Calling thread waits for a given number of milliseconds or
1300 until notified."""
1301 self.condition.acquire()
1302 self.condition.wait(timeout/1000)
1303 self.condition.release()
1305 def notifyAll(self):
1306 """Notifies all waiting threads"""
1307 self.condition.acquire()
1308 self.condition.notifyAll()
1309 self.condition.release()
1311 def getServiceInfo(self, type, name, timeout=3000):
1312 """Returns network's service information for a particular
1313 name and type, or None if no service matches by the timeout,
1314 which defaults to 3 seconds."""
1315 info = ServiceInfo(type, name)
1316 if info.request(self, timeout):
1317 return info
1318 return None
1320 def addServiceListener(self, type, listener):
1321 """Adds a listener for a particular service type. This object
1322 will then have its updateRecord method called when information
1323 arrives for that type."""
1324 self.removeServiceListener(listener)
1325 self.browsers.append(ServiceBrowser(self, type, listener))
1327 def removeServiceListener(self, listener):
1328 """Removes a listener from the set that is currently listening."""
1329 for browser in self.browsers:
1330 if browser.listener == listener:
1331 browser.cancel()
1332 del(browser)
1334 def registerService(self, info, ttl=_DNS_TTL):
1335 """Registers service information to the network with a default TTL
1336 of 60 seconds. Zeroconf will then respond to requests for
1337 information for that service. The name of the service may be
1338 changed if needed to make it unique on the network."""
1339 self.checkService(info)
1340 self.services[info.name.lower()] = info
1341 if info.type in self.servicetypes:
1342 self.servicetypes[info.type]+=1
1343 else:
1344 self.servicetypes[info.type]=1
1345 now = currentTimeMillis()
1346 nextTime = now
1347 i = 0
1348 while i < 3:
1349 if now < nextTime:
1350 self.wait(nextTime - now)
1351 now = currentTimeMillis()
1352 continue
1353 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1354 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0)
1355 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, ttl, info.priority, info.weight, info.port, info.server), 0)
1356 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0)
1357 if info.address:
1358 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, ttl, info.address), 0)
1359 self.send(out)
1360 i += 1
1361 nextTime += _REGISTER_TIME
1363 def unregisterService(self, info):
1364 """Unregister a service."""
1365 try:
1366 del(self.services[info.name.lower()])
1367 if self.servicetypes[info.type]>1:
1368 self.servicetypes[info.type]-=1
1369 else:
1370 del self.servicetypes[info.type]
1371 except:
1372 pass
1373 now = currentTimeMillis()
1374 nextTime = now
1375 i = 0
1376 while i < 3:
1377 if now < nextTime:
1378 self.wait(nextTime - now)
1379 now = currentTimeMillis()
1380 continue
1381 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1382 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
1383 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.name), 0)
1384 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1385 if info.address:
1386 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1387 self.send(out)
1388 i += 1
1389 nextTime += _UNREGISTER_TIME
1391 def unregisterAllServices(self):
1392 """Unregister all registered services."""
1393 if len(self.services) > 0:
1394 now = currentTimeMillis()
1395 nextTime = now
1396 i = 0
1397 while i < 3:
1398 if now < nextTime:
1399 self.wait(nextTime - now)
1400 now = currentTimeMillis()
1401 continue
1402 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1403 for info in self.services.values():
1404 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
1405 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.server), 0)
1406 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1407 if info.address:
1408 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1409 self.send(out)
1410 i += 1
1411 nextTime += _UNREGISTER_TIME
1413 def checkService(self, info):
1414 """Checks the network for a unique service name, modifying the
1415 ServiceInfo passed in if it is not unique."""
1416 now = currentTimeMillis()
1417 nextTime = now
1418 i = 0
1419 while i < 3:
1420 for record in self.cache.entriesWithName(info.type):
1421 if record.type == _TYPE_PTR and not record.isExpired(now) and record.alias == info.name:
1422 if (info.name.find('.') < 0):
1423 info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type
1424 self.checkService(info)
1425 return
1426 raise NonUniqueNameException
1427 if now < nextTime:
1428 self.wait(nextTime - now)
1429 now = currentTimeMillis()
1430 continue
1431 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1432 self.debug = out
1433 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1434 out.addAuthorativeAnswer(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name))
1435 self.send(out)
1436 i += 1
1437 nextTime += _CHECK_TIME
1439 def addListener(self, listener, question):
1440 """Adds a listener for a given question. The listener will have
1441 its updateRecord method called when information is available to
1442 answer the question."""
1443 now = currentTimeMillis()
1444 self.listeners.append(listener)
1445 if question is not None:
1446 for record in self.cache.entriesWithName(question.name):
1447 if question.answeredBy(record) and not record.isExpired(now):
1448 listener.updateRecord(self, now, record)
1449 self.notifyAll()
1451 def removeListener(self, listener):
1452 """Removes a listener."""
1453 try:
1454 self.listeners.remove(listener)
1455 self.notifyAll()
1456 except:
1457 pass
1459 def updateRecord(self, now, rec):
1460 """Used to notify listeners of new information that has updated
1461 a record."""
1462 for listener in self.listeners:
1463 listener.updateRecord(self, now, rec)
1464 self.notifyAll()
1466 def handleResponse(self, msg):
1467 """Deal with incoming response packets. All answers
1468 are held in the cache, and listeners are notified."""
1469 now = currentTimeMillis()
1470 for record in msg.answers:
1471 expired = record.isExpired(now)
1472 if record in self.cache.entries():
1473 if expired:
1474 self.cache.remove(record)
1475 else:
1476 entry = self.cache.get(record)
1477 if entry is not None:
1478 entry.resetTTL(record)
1479 record = entry
1480 else:
1481 self.cache.add(record)
1483 self.updateRecord(now, record)
1485 def handleQuery(self, msg, addr, port):
1486 """Deal with incoming query packets. Provides a response if
1487 possible."""
1488 out = None
1490 # Support unicast client responses
1492 if port != _MDNS_PORT:
1493 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
1494 for question in msg.questions:
1495 out.addQuestion(question)
1497 for question in msg.questions:
1498 if question.type == _TYPE_PTR:
1499 if question.name == "_services._dns-sd._udp.local.":
1500 for stype in self.servicetypes.keys():
1501 if out is None:
1502 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1503 out.addAnswer(msg, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
1504 for service in self.services.values():
1505 if question.name == service.type:
1506 if out is None:
1507 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1508 out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name))
1509 else:
1510 try:
1511 if out is None:
1512 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1514 # Answer A record queries for any service addresses we know
1515 if question.type == _TYPE_A or question.type == _TYPE_ANY:
1516 for service in self.services.values():
1517 if service.server == question.name.lower():
1518 out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1520 service = self.services.get(question.name.lower(), None)
1521 if not service: continue
1523 if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
1524 out.addAnswer(msg, DNSService(question.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.priority, service.weight, service.port, service.server))
1525 if question.type == _TYPE_TXT or question.type == _TYPE_ANY:
1526 out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text))
1527 if question.type == _TYPE_SRV:
1528 out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1529 except:
1530 traceback.print_exc()
1532 if out is not None and out.answers:
1533 out.id = msg.id
1534 self.send(out, addr, port)
1536 def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
1537 """Sends an outgoing packet."""
1538 # This is a quick test to see if we can parse the packets we generate
1539 #temp = DNSIncoming(out.packet())
1540 try:
1541 bytes_sent = self.socket.sendto(out.packet(), 0, (addr, port))
1542 except:
1543 # Ignore this, it may be a temporary loss of network connection
1544 pass
1546 def close(self):
1547 """Ends the background threads, and prevent this instance from
1548 servicing further queries."""
1549 if globals()['_GLOBAL_DONE'] == 0:
1550 globals()['_GLOBAL_DONE'] = 1
1551 self.notifyAll()
1552 self.engine.notify()
1553 self.unregisterAllServices()
1554 self.socket.setsockopt(socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1555 self.socket.close()
1557 # Test a few module features, including service registration, service
1558 # query (for Zoe), and service unregistration.
1560 if __name__ == '__main__':
1561 print "Multicast DNS Service Discovery for Python, version", __version__
1562 r = Zeroconf()
1563 print "1. Testing registration of a service..."
1564 desc = {'version':'0.10','a':'test value', 'b':'another value'}
1565 info = ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
1566 print " Registering service..."
1567 r.registerService(info)
1568 print " Registration done."
1569 print "2. Testing query of service information..."
1570 print " Getting ZOE service:", str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1571 print " Query done."
1572 print "3. Testing query of own service..."
1573 print " Getting self:", str(r.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local."))
1574 print " Query done."
1575 print "4. Testing unregister of service information..."
1576 r.unregisterService(info)
1577 print " Unregister done."
1578 r.close()