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
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
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
60 """0.06 update - small improvements with unit tests
61 added defined exception types
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"
88 __all__
= ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
92 globals()['_GLOBAL_DONE'] = 0
94 # Some timing constants
96 _UNREGISTER_TIME
= 125
104 _MDNS_ADDR
= '224.0.0.251'
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
132 _CLASS_UNIQUE
= 0x8000
154 # Mapping constants to names
156 _CLASSES
= { _CLASS_IN
: "in",
160 _CLASS_NONE
: "none",
163 _TYPES
= { _TYPE_A
: "a",
167 _TYPE_CNAME
: "cname",
175 _TYPE_HINFO
: "hinfo",
176 _TYPE_MINFO
: "minfo",
179 _TYPE_AAAA
: "quada",
185 def currentTimeMillis():
186 """Current system time in milliseconds"""
187 return time
.time() * 1000
191 class NonLocalNameException(Exception):
194 class NonUniqueNameException(Exception):
197 class NamePartTooLongException(Exception):
200 class AbstractMethodException(Exception):
203 class BadTypeInNameException(Exception):
206 # implementation classes
208 class DNSEntry(object):
211 def __init__(self
, name
, type, clazz
):
212 self
.key
= name
.lower()
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
224 def __ne__(self
, other
):
225 """Non-equality test"""
226 return not self
.__eq
__(other
)
228 def getClazz(self
, clazz
):
231 return _CLASSES
[clazz
]
233 return "?(%s)" % (clazz
)
235 def getType(self
, type):
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
))
250 if other
is not None:
251 result
+= ",%s]" % (other
)
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
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
)
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
)
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
):
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):
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
322 self
.created
= other
.created
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
352 """String representation"""
354 return socket
.inet_ntoa(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
)
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
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
)
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
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
)
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
420 """String representation"""
421 if len(self
.text
) > 10:
422 return self
.toString(self
.text
[:7] + "...")
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
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
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"""
462 self
.numQuestions
= 0
464 self
.numAuthorities
= 0
465 self
.numAdditionals
= 0
471 def readHeader(self
):
472 """Reads header portion of packet"""
474 length
= struct
.calcsize(format
)
475 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
476 self
.offset
+= length
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"""
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
)
498 """Reads an integer from the packet"""
500 length
= struct
.calcsize(format
)
501 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
502 self
.offset
+= length
505 def readCharacterString(self
):
506 """Reads a character string from the packet"""
507 length
= ord(self
.data
[self
.offset
])
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
519 def readUnsignedShort(self
):
520 """Reads an unsigned short from the packet"""
522 length
= struct
.calcsize(format
)
523 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
524 self
.offset
+= length
527 def readOthers(self
):
528 """Reads the answers, authorities and additionals section of the packet"""
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
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))
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
562 self
.answers
.append(rec
)
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')
578 """Reads a domain name from the packet"""
585 len = ord(self
.data
[off
])
591 result
= ''.join((result
, self
.readUTF(off
, len) + '.'))
596 off
= ((len & 0x3F) << 8) |
ord(self
.data
[off
])
598 raise "Bad domain name (circular) at " + str(off
)
601 raise "Bad domain name at " + str(off
)
611 class DNSOutgoing(object):
612 """Object representation of an outgoing packet"""
614 def __init__(self
, flags
, multicast
= 1):
617 self
.multicast
= multicast
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
):
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"""
654 self
.data
.append(struct
.pack(format
, chr(value
)))
657 def insertShort(self
, index
, value
):
658 """Inserts an unsigned short in a certain position in the packet"""
660 self
.data
.insert(index
, struct
.pack(format
, value
))
663 def writeShort(self
, value
):
664 """Writes an unsigned short to the packet"""
666 self
.data
.append(struct
.pack(format
, value
))
669 def writeInt(self
, value
):
670 """Writes an unsigned integer to the packet"""
672 self
.data
.append(struct
.pack(format
, int(value
)))
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
))
681 def writeUTF(self
, s
):
682 """Writes a UTF-8 string of a given length to the packet"""
683 utfstr
= s
.encode('utf-8')
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"""
694 # Find existing instance of this name in packet
696 index
= self
.names
[name
]
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('.')
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
725 self
.writeName(record
.name
)
726 self
.writeShort(record
.type)
727 if record
.unique
and self
.multicast
:
728 self
.writeShort(record
.clazz | _CLASS_UNIQUE
)
730 self
.writeShort(record
.clazz
)
732 self
.writeInt(record
.ttl
)
734 self
.writeInt(record
.getRemainingTTL(now
))
735 index
= len(self
.data
)
736 # Adjust size for the short we will write before this record
742 length
= len(''.join(self
.data
[index
:]))
743 self
.insertShort(index
, length
) # Here is the short we adjusted for
746 """Returns a string containing the packet's bytes
748 No further parts should be added to the packet once this
750 if not self
.finished
:
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
)
767 self
.insertShort(0, 0)
769 self
.insertShort(0, self
.id)
770 return ''.join(self
.data
)
773 class DNSCache(object):
774 """A cache of DNS entries"""
779 def add(self
, entry
):
782 list = self
.cache
[entry
.key
]
784 list = self
.cache
[entry
.key
] = []
787 def remove(self
, entry
):
788 """Removes an entry"""
790 list = self
.cache
[entry
.key
]
795 def get(self
, entry
):
796 """Gets an entry by key. Will return None if there is no
799 list = self
.cache
[entry
.key
]
800 return list[list.index(entry
)]
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."""
813 return self
.cache
[name
]
818 """Returns a list of all entries"""
819 def add(x
, y
): return x
+y
821 return reduce(add
, self
.cache
.values())
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
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
838 def __init__(self
, zeroconf
):
839 threading
.Thread
.__init
__(self
)
840 self
.zeroconf
= zeroconf
841 self
.readers
= {} # maps socket to reader
843 self
.condition
= threading
.Condition()
847 while not globals()['_GLOBAL_DONE']:
848 rs
= self
.getReaders()
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()
858 rr
, wr
, er
= select
.select(rs
, [], [], self
.timeout
)
861 self
.readers
[socket
].handle_read()
863 traceback
.print_exc()
867 def getReaders(self
):
869 self
.condition
.acquire()
870 result
= self
.readers
.keys()
871 self
.condition
.release()
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()
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
):
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.
910 if e
[0] == socket
.EBADF
:
915 msg
= DNSIncoming(data
)
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
924 elif port
== _DNS_PORT
:
925 self
.zeroconf
.handleQuery(msg
, addr
, port
)
926 self
.zeroconf
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
928 self
.zeroconf
.handleResponse(msg
)
931 class Reaper(threading
.Thread
):
932 """A Reaper is used by this module to remove cache entries that
935 def __init__(self
, zeroconf
):
936 threading
.Thread
.__init
__(self
)
937 self
.zeroconf
= zeroconf
942 self
.zeroconf
.wait(10 * 1000)
943 if globals()['_GLOBAL_DONE']:
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
964 self
.listener
= listener
966 self
.nextTime
= currentTimeMillis()
967 self
.delay
= _BROWSER_TIME
972 self
.zeroconf
.addListener(self
, DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
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
)
982 oldrecord
= self
.services
[record
.alias
.lower()]
984 oldrecord
.resetTTL(record
)
986 del(self
.services
[record
.alias
.lower()])
987 callback
= lambda x
: self
.listener
.removeService(x
, self
.type, record
.alias
)
988 self
.list.append(callback
)
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
1002 self
.zeroconf
.notifyAll()
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
:
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
1050 self
.address
= address
1052 self
.weight
= weight
1053 self
.priority
= priority
1055 self
.server
= server
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
1066 for key
in properties
:
1067 value
= properties
[key
]
1069 suffix
= ''.encode('utf-8')
1070 elif isinstance(value
, str):
1071 suffix
= value
.encode('utf-8')
1072 elif isinstance(value
, int):
1078 suffix
= ''.encode('utf-8')
1079 list.append('='.join((key
, suffix
)))
1081 result
= ''.join((result
, struct
.pack('!c', chr(len(item
))), item
))
1084 self
.text
= properties
1086 def setText(self
, text
):
1087 """Sets properties and text given a text field"""
1095 length
= ord(text
[index
])
1097 strs
.append(text
[index
:index
+length
])
1101 eindex
= s
.find('=')
1103 # No equals sign at all
1108 value
= s
[eindex
+1:]
1111 elif value
== 'false' or not value
:
1114 # Only update non-existent properties
1115 if key
and result
.get(key
) == None:
1118 self
.properties
= result
1120 traceback
.print_exc()
1121 self
.properties
= None
1129 if self
.type is not None and self
.name
.endswith("." + self
.type):
1130 return self
.name
[:len(self
.name
) - len(self
.type) - 1]
1133 def getAddress(self
):
1134 """Address accessor"""
1141 def getPriority(self
):
1142 """Pirority accessor"""
1143 return self
.priority
1145 def getWeight(self
):
1146 """Weight accessor"""
1149 def getProperties(self
):
1150 """Properties accessor"""
1151 return self
.properties
1157 def getServer(self
):
1158 """Server accessor"""
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
1187 last
= now
+ timeout
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:
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
)
1207 zeroconf
.wait(min(next
, last
) - now
)
1208 now
= currentTimeMillis()
1211 zeroconf
.removeListener(self
)
1215 def __eq__(self
, other
):
1216 """Tests equality of service name"""
1217 if isinstance(other
, ServiceInfo
):
1218 return other
.name
== self
.name
1221 def __ne__(self
, other
):
1222 """Non-equality test"""
1223 return not self
.__eq
__(other
)
1226 """String representation"""
1227 result
= "service[%s,%s:%s," % (self
.name
, socket
.inet_ntoa(self
.getAddress()), self
.port
)
1228 if self
.text
is None:
1231 if len(self
.text
) < 20:
1234 result
+= self
.text
[:17] + "..."
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())
1251 self
.intf
= bindaddress
1252 self
.group
= ('', _MDNS_PORT
)
1253 self
.socket
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
1255 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
, 1)
1256 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEPORT
, 1)
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
1268 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_TTL
, 255)
1269 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_LOOP
, 1)
1271 self
.socket
.bind(self
.group
)
1273 # Some versions of linux raise an exception even though
1274 # the SO_REUSE* options have been set, so ignore it
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'))
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
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
):
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
:
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
1345 self
.servicetypes
[info
.type]=1
1346 now
= currentTimeMillis()
1351 self
.wait(nextTime
- now
)
1352 now
= currentTimeMillis()
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)
1359 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, ttl
, info
.address
), 0)
1362 nextTime
+= _REGISTER_TIME
1364 def unregisterService(self
, info
):
1365 """Unregister a service."""
1367 del(self
.services
[info
.name
.lower()])
1368 if self
.servicetypes
[info
.type]>1:
1369 self
.servicetypes
[info
.type]-=1
1371 del self
.servicetypes
[info
.type]
1374 now
= currentTimeMillis()
1379 self
.wait(nextTime
- now
)
1380 now
= currentTimeMillis()
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)
1387 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, 0, info
.address
), 0)
1390 nextTime
+= _UNREGISTER_TIME
1392 def unregisterAllServices(self
):
1393 """Unregister all registered services."""
1394 if len(self
.services
) > 0:
1395 now
= currentTimeMillis()
1400 self
.wait(nextTime
- now
)
1401 now
= currentTimeMillis()
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)
1409 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, 0, info
.address
), 0)
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()
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
)
1427 raise NonUniqueNameException
1429 self
.wait(nextTime
- now
)
1430 now
= currentTimeMillis()
1432 out
= DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA
)
1434 out
.addQuestion(DNSQuestion(info
.type, _TYPE_PTR
, _CLASS_IN
))
1435 out
.addAuthorativeAnswer(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, info
.name
))
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
)
1452 def removeListener(self
, listener
):
1453 """Removes a listener."""
1455 self
.listeners
.remove(listener
)
1460 def updateRecord(self
, now
, rec
):
1461 """Used to notify listeners of new information that has updated
1463 for listener
in self
.listeners
:
1464 listener
.updateRecord(self
, now
, rec
)
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():
1475 self
.cache
.remove(record
)
1477 entry
= self
.cache
.get(record
)
1478 if entry
is not None:
1479 entry
.resetTTL(record
)
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
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():
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:
1508 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1509 out
.addAnswer(msg
, DNSPointer(service
.type, _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, service
.name
))
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
))
1531 traceback
.print_exc()
1533 if out
is not None and out
.answers
:
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())
1542 bytes_sent
= self
.socket
.sendto(out
.packet(), 0, (addr
, port
))
1544 # Ignore this, it may be a temporary loss of network connection
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
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'))
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__
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."