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
37 tested against Mac OS X 10.3.2's mDNSResponder
38 corrections to removal of list entries for service browser"""
40 """0.10 update - Jonathon Paisley contributed these corrections:
41 always multicast replies, even when query is unicast
42 correct a pointer encoding problem
43 can now write records in any order
44 traceback shown on failure
45 better TXT record parsing
46 server is now separate from name
47 can cancel a service browser
49 modified some unit tests to accommodate these changes"""
51 """0.09 update - remove all records on service unregistration
52 fix DOS security problem with readName"""
54 """0.08 update - changed licensing to LGPL"""
56 """0.07 update - faster shutdown on engine
57 pointer encoding of outgoing names
58 ServiceBrowser now works
61 """0.06 update - small improvements with unit tests
62 added defined exception types
64 fixed hostname/interface problem
65 fixed socket timeout problem
66 fixed addServiceListener() typo bug
67 using select() for socket reads
68 tested on Debian unstable with Python 2.2.2"""
70 """0.05 update - ensure case insensitivty on domain names
71 support for unicast DNS queries"""
73 """0.04 update - added some unit tests
74 added __ne__ adjuncts where required
75 ensure names end in '.local.'
76 timeout on receiving socket for clean shutdown"""
78 __author__
= "Paul Scott-Murphy"
79 __email__
= "paul at scott dash murphy dot com"
89 __all__
= ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
95 # Some timing constants
97 _UNREGISTER_TIME
= 125
105 _MDNS_ADDR
= '224.0.0.251'
108 _DNS_TTL
= 60 * 60; # one hour default TTL
110 _MAX_MSG_TYPICAL
= 1460 # unused
111 _MAX_MSG_ABSOLUTE
= 8972
113 _FLAGS_QR_MASK
= 0x8000 # query response mask
114 _FLAGS_QR_QUERY
= 0x0000 # query
115 _FLAGS_QR_RESPONSE
= 0x8000 # response
117 _FLAGS_AA
= 0x0400 # Authorative answer
118 _FLAGS_TC
= 0x0200 # Truncated
119 _FLAGS_RD
= 0x0100 # Recursion desired
120 _FLAGS_RA
= 0x8000 # Recursion available
122 _FLAGS_Z
= 0x0040 # Zero
123 _FLAGS_AD
= 0x0020 # Authentic data
124 _FLAGS_CD
= 0x0010 # Checking disabled
133 _CLASS_UNIQUE
= 0x8000
155 # Mapping constants to names
157 _CLASSES
= { _CLASS_IN
: "in",
161 _CLASS_NONE
: "none",
164 _TYPES
= { _TYPE_A
: "a",
168 _TYPE_CNAME
: "cname",
176 _TYPE_HINFO
: "hinfo",
177 _TYPE_MINFO
: "minfo",
180 _TYPE_AAAA
: "quada",
186 def currentTimeMillis():
187 """Current system time in milliseconds"""
188 return time
.time() * 1000
192 class NonLocalNameException(Exception):
195 class NonUniqueNameException(Exception):
198 class NamePartTooLongException(Exception):
201 class AbstractMethodException(Exception):
204 class BadTypeInNameException(Exception):
207 # implementation classes
209 class DNSEntry(object):
212 def __init__(self
, name
, type, clazz
):
213 self
.key
= name
.lower()
216 self
.clazz
= clazz
& _CLASS_MASK
217 self
.unique
= (clazz
& _CLASS_UNIQUE
) != 0
219 def __eq__(self
, other
):
220 """Equality test on name, type, and class"""
221 return (isinstance(other
, DNSEntry
) and
222 self
.name
== other
.name
and
223 self
.type == other
.type and
224 self
.clazz
== other
.clazz
)
226 def __ne__(self
, other
):
227 """Non-equality test"""
228 return not self
.__eq
__(other
)
230 def getClazz(self
, clazz
):
233 return _CLASSES
[clazz
]
235 return "?(%s)" % (clazz
)
237 def getType(self
, type):
242 return "?(%s)" % (type)
244 def toString(self
, hdr
, other
):
245 """String representation with additional information"""
246 result
= "%s[%s,%s" % (hdr
, self
.getType(self
.type),
247 self
.getClazz(self
.clazz
))
253 if other
is not None:
254 result
+= ",%s]" % (other
)
259 class DNSQuestion(DNSEntry
):
260 """A DNS question entry"""
262 def __init__(self
, name
, type, clazz
):
263 #if not name.endswith(".local."):
264 # raise NonLocalNameException
265 DNSEntry
.__init
__(self
, name
, type, clazz
)
267 def answeredBy(self
, rec
):
268 """Returns true if the question is answered by the record"""
269 return (self
.clazz
== rec
.clazz
and
270 (self
.type == rec
.type or self
.type == _TYPE_ANY
) and
271 self
.name
== rec
.name
)
274 """String representation"""
275 return DNSEntry
.toString(self
, "question", None)
278 class DNSRecord(DNSEntry
):
279 """A DNS record - like a DNS entry, but has a TTL"""
281 def __init__(self
, name
, type, clazz
, ttl
):
282 DNSEntry
.__init
__(self
, name
, type, clazz
)
284 self
.created
= currentTimeMillis()
286 def __eq__(self
, other
):
287 """Tests equality as per DNSRecord"""
288 return isinstance(other
, DNSRecord
) and DNSEntry
.__eq
__(self
, other
)
290 def suppressedBy(self
, msg
):
291 """Returns true if any answer in a message can suffice for the
292 information held in this record."""
293 for record
in msg
.answers
:
294 if self
.suppressedByAnswer(record
):
298 def suppressedByAnswer(self
, other
):
299 """Returns true if another record has same name, type and class,
300 and if its TTL is at least half of this record's."""
301 return self
== other
and other
.ttl
> (self
.ttl
/ 2)
303 def getExpirationTime(self
, percent
):
304 """Returns the time at which this record will have expired
305 by a certain percentage."""
306 return self
.created
+ (percent
* self
.ttl
* 10)
308 def getRemainingTTL(self
, now
):
309 """Returns the remaining TTL in seconds."""
310 return max(0, (self
.getExpirationTime(100) - now
) / 1000)
312 def isExpired(self
, now
):
313 """Returns true if this record has expired."""
314 return self
.getExpirationTime(100) <= now
316 def isStale(self
, now
):
317 """Returns true if this record is at least half way expired."""
318 return self
.getExpirationTime(50) <= now
320 def resetTTL(self
, other
):
321 """Sets this record's TTL and created time to that of
323 self
.created
= other
.created
326 def write(self
, out
):
327 """Abstract method"""
328 raise AbstractMethodException
330 def toString(self
, other
):
331 """String representation with addtional information"""
332 arg
= "%s/%s,%s" % (self
.ttl
,
333 self
.getRemainingTTL(currentTimeMillis()), other
)
334 return DNSEntry
.toString(self
, "record", arg
)
336 class DNSAddress(DNSRecord
):
337 """A DNS address record"""
339 def __init__(self
, name
, type, clazz
, ttl
, address
):
340 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
341 self
.address
= address
343 def write(self
, out
):
344 """Used in constructing an outgoing packet"""
345 out
.writeString(self
.address
)
347 def __eq__(self
, other
):
348 """Tests equality on address"""
349 return isinstance(other
, DNSAddress
) and 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
)
369 out
.writeString(self
.oso
)
371 def __eq__(self
, other
):
372 """Tests equality on cpu and os"""
373 return (isinstance(other
, DNSHinfo
) and
374 self
.cpu
== other
.cpu
and self
.os
== other
.os
)
377 """String representation"""
378 return self
.cpu
+ " " + self
.os
380 class DNSPointer(DNSRecord
):
381 """A DNS pointer record"""
383 def __init__(self
, name
, type, clazz
, ttl
, alias
):
384 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
387 def write(self
, out
):
388 """Used in constructing an outgoing packet"""
389 out
.writeName(self
.alias
)
391 def __eq__(self
, other
):
392 """Tests equality on alias"""
393 return isinstance(other
, DNSPointer
) and self
.alias
== other
.alias
396 """String representation"""
397 return self
.toString(self
.alias
)
399 class DNSText(DNSRecord
):
400 """A DNS text record"""
402 def __init__(self
, name
, type, clazz
, ttl
, text
):
403 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
406 def write(self
, out
):
407 """Used in constructing an outgoing packet"""
408 out
.writeString(self
.text
)
410 def __eq__(self
, other
):
411 """Tests equality on text"""
412 return isinstance(other
, DNSText
) and self
.text
== other
.text
415 """String representation"""
416 if len(self
.text
) > 10:
417 return self
.toString(self
.text
[:7] + "...")
419 return self
.toString(self
.text
)
421 class DNSService(DNSRecord
):
422 """A DNS service record"""
424 def __init__(self
, name
, type, clazz
, ttl
, priority
, weight
, port
, server
):
425 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
426 self
.priority
= priority
431 def write(self
, out
):
432 """Used in constructing an outgoing packet"""
433 out
.writeShort(self
.priority
)
434 out
.writeShort(self
.weight
)
435 out
.writeShort(self
.port
)
436 out
.writeName(self
.server
)
438 def __eq__(self
, other
):
439 """Tests equality on priority, weight, port and server"""
440 return (isinstance(other
, DNSService
) and
441 self
.priority
== other
.priority
and
442 self
.weight
== other
.weight
and
443 self
.port
== other
.port
and
444 self
.server
== other
.server
)
447 """String representation"""
448 return self
.toString("%s:%s" % (self
.server
, self
.port
))
450 class DNSIncoming(object):
451 """Object representation of an incoming DNS packet"""
453 def __init__(self
, data
):
454 """Constructor from string holding bytes of packet"""
459 self
.numQuestions
= 0
461 self
.numAuthorities
= 0
462 self
.numAdditionals
= 0
468 def unpack(self
, format
):
469 length
= struct
.calcsize(format
)
470 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
471 self
.offset
+= length
474 def readHeader(self
):
475 """Reads header portion of packet"""
476 (self
.id, self
.flags
, self
.numQuestions
, self
.numAnswers
,
477 self
.numAuthorities
, self
.numAdditionals
) = self
.unpack('!HHHHHH')
479 def readQuestions(self
):
480 """Reads questions section of packet"""
481 for i
in xrange(self
.numQuestions
):
482 name
= self
.readName()
483 type, clazz
= self
.unpack('!HH')
485 question
= DNSQuestion(name
, type, clazz
)
486 self
.questions
.append(question
)
489 """Reads an integer from the packet"""
490 return self
.unpack('!I')[0]
492 def readCharacterString(self
):
493 """Reads a character string from the packet"""
494 length
= ord(self
.data
[self
.offset
])
496 return self
.readString(length
)
498 def readString(self
, length
):
499 """Reads a string of a given length from the packet"""
500 info
= self
.data
[self
.offset
:self
.offset
+length
]
501 self
.offset
+= length
504 def readUnsignedShort(self
):
505 """Reads an unsigned short from the packet"""
506 return self
.unpack('!H')[0]
508 def readOthers(self
):
509 """Reads the answers, authorities and additionals section of the
511 n
= self
.numAnswers
+ self
.numAuthorities
+ self
.numAdditionals
513 domain
= self
.readName()
514 type, clazz
, ttl
, length
= self
.unpack('!HHiH')
518 rec
= DNSAddress(domain
, type, clazz
, ttl
, self
.readString(4))
519 elif type == _TYPE_CNAME
or type == _TYPE_PTR
:
520 rec
= DNSPointer(domain
, type, clazz
, ttl
, self
.readName())
521 elif type == _TYPE_TXT
:
522 rec
= DNSText(domain
, type, clazz
, ttl
, self
.readString(length
))
523 elif type == _TYPE_SRV
:
524 rec
= DNSService(domain
, type, clazz
, ttl
,
525 self
.readUnsignedShort(), self
.readUnsignedShort(),
526 self
.readUnsignedShort(), self
.readName())
527 elif type == _TYPE_HINFO
:
528 rec
= DNSHinfo(domain
, type, clazz
, ttl
,
529 self
.readCharacterString(), self
.readCharacterString())
530 elif type == _TYPE_AAAA
:
531 rec
= DNSAddress(domain
, type, clazz
, ttl
, self
.readString(16))
533 # Try to ignore types we don't know about
534 # Skip the payload for the resource record so the next
535 # records can be parsed correctly
536 self
.offset
+= length
539 self
.answers
.append(rec
)
542 """Returns true if this is a query"""
543 return (self
.flags
& _FLAGS_QR_MASK
) == _FLAGS_QR_QUERY
545 def isResponse(self
):
546 """Returns true if this is a response"""
547 return (self
.flags
& _FLAGS_QR_MASK
) == _FLAGS_QR_RESPONSE
549 def readUTF(self
, offset
, len):
550 """Reads a UTF-8 string of a given length from the packet"""
551 return unicode(self
.data
[offset
:offset
+len], 'utf-8', 'replace')
554 """Reads a domain name from the packet"""
561 len = ord(self
.data
[off
])
567 result
= ''.join((result
, self
.readUTF(off
, len) + '.'))
572 off
= ((len & 0x3F) << 8) |
ord(self
.data
[off
])
574 raise "Bad domain name (circular) at " + str(off
)
577 raise "Bad domain name at " + str(off
)
587 class DNSOutgoing(object):
588 """Object representation of an outgoing packet"""
590 def __init__(self
, flags
, multicast
=True):
591 self
.finished
= False
593 self
.multicast
= multicast
601 self
.authorities
= []
602 self
.additionals
= []
604 def addQuestion(self
, record
):
605 """Adds a question"""
606 self
.questions
.append(record
)
608 def addAnswer(self
, inp
, record
):
610 if not record
.suppressedBy(inp
):
611 self
.addAnswerAtTime(record
, 0)
613 def addAnswerAtTime(self
, record
, now
):
614 """Adds an answer if if does not expire by a certain time"""
615 if record
is not None:
616 if now
== 0 or not record
.isExpired(now
):
617 self
.answers
.append((record
, now
))
619 def addAuthorativeAnswer(self
, record
):
620 """Adds an authoritative answer"""
621 self
.authorities
.append(record
)
623 def addAdditionalAnswer(self
, record
):
624 """Adds an additional answer"""
625 self
.additionals
.append(record
)
627 def pack(self
, format
, value
):
628 self
.data
.append(struct
.pack(format
, value
))
629 self
.size
+= struct
.calcsize(format
)
631 def writeByte(self
, value
):
632 """Writes a single byte to the packet"""
633 self
.pack('!c', chr(value
))
635 def insertShort(self
, index
, value
):
636 """Inserts an unsigned short in a certain position in the packet"""
637 self
.data
.insert(index
, struct
.pack('!H', value
))
640 def writeShort(self
, value
):
641 """Writes an unsigned short to the packet"""
642 self
.pack('!H', value
)
644 def writeInt(self
, value
):
645 """Writes an unsigned integer to the packet"""
646 self
.pack('!I', int(value
))
648 def writeString(self
, value
):
649 """Writes a string to the packet"""
650 self
.data
.append(value
)
651 self
.size
+= len(value
)
653 def writeUTF(self
, s
):
654 """Writes a UTF-8 string of a given length to the packet"""
655 utfstr
= s
.encode('utf-8')
658 raise NamePartTooLongException
659 self
.writeByte(length
)
660 self
.writeString(utfstr
)
662 def writeName(self
, name
):
663 """Writes a domain name to the packet"""
666 # Find existing instance of this name in packet
668 index
= self
.names
[name
]
670 # No record of this name already, so write it
671 # out as normal, recording the location of the name
672 # for future pointers to it.
674 self
.names
[name
] = self
.size
675 parts
= name
.split('.')
683 # An index was found, so write a pointer to it
685 self
.writeByte((index
>> 8) |
0xC0)
686 self
.writeByte(index
)
688 def writeQuestion(self
, question
):
689 """Writes a question to the packet"""
690 self
.writeName(question
.name
)
691 self
.writeShort(question
.type)
692 self
.writeShort(question
.clazz
)
694 def writeRecord(self
, record
, now
):
695 """Writes a record (answer, authoritative answer, additional) to
697 self
.writeName(record
.name
)
698 self
.writeShort(record
.type)
699 if record
.unique
and self
.multicast
:
700 self
.writeShort(record
.clazz | _CLASS_UNIQUE
)
702 self
.writeShort(record
.clazz
)
704 self
.writeInt(record
.ttl
)
706 self
.writeInt(record
.getRemainingTTL(now
))
707 index
= len(self
.data
)
708 # Adjust size for the short we will write before this record
714 length
= len(''.join(self
.data
[index
:]))
715 self
.insertShort(index
, length
) # Here is the short we adjusted for
718 """Returns a string containing the packet's bytes
720 No further parts should be added to the packet once this
722 if not self
.finished
:
724 for question
in self
.questions
:
725 self
.writeQuestion(question
)
726 for answer
, time
in self
.answers
:
727 self
.writeRecord(answer
, time
)
728 for authority
in self
.authorities
:
729 self
.writeRecord(authority
, 0)
730 for additional
in self
.additionals
:
731 self
.writeRecord(additional
, 0)
733 self
.insertShort(0, len(self
.additionals
))
734 self
.insertShort(0, len(self
.authorities
))
735 self
.insertShort(0, len(self
.answers
))
736 self
.insertShort(0, len(self
.questions
))
737 self
.insertShort(0, self
.flags
)
739 self
.insertShort(0, 0)
741 self
.insertShort(0, self
.id)
742 return ''.join(self
.data
)
745 class DNSCache(object):
746 """A cache of DNS entries"""
751 def add(self
, entry
):
754 list = self
.cache
[entry
.key
]
756 list = self
.cache
[entry
.key
] = []
759 def remove(self
, entry
):
760 """Removes an entry"""
762 list = self
.cache
[entry
.key
]
767 def get(self
, entry
):
768 """Gets an entry by key. Will return None if there is no
771 list = self
.cache
[entry
.key
]
772 return list[list.index(entry
)]
776 def getByDetails(self
, name
, type, clazz
):
777 """Gets an entry by details. Will return None if there is
778 no matching entry."""
779 entry
= DNSEntry(name
, type, clazz
)
780 return self
.get(entry
)
782 def entriesWithName(self
, name
):
783 """Returns a list of entries whose key matches the name."""
785 return self
.cache
[name
]
790 """Returns a list of all entries"""
791 def add(x
, y
): return x
+y
793 return reduce(add
, self
.cache
.values())
798 class Engine(threading
.Thread
):
799 """An engine wraps read access to sockets, allowing objects that
800 need to receive data from sockets to be called back when the
803 A reader needs a handle_read() method, which is called when the socket
804 it is interested in is ready for reading.
806 Writers are not implemented here, because we only send short
810 def __init__(self
, zc
):
811 threading
.Thread
.__init
__(self
)
813 self
.readers
= {} # maps socket to reader
815 self
.condition
= threading
.Condition()
819 while not _GLOBAL_DONE
:
820 rs
= self
.getReaders()
822 # No sockets to manage, but we wait for the timeout
823 # or addition of a socket
825 self
.condition
.acquire()
826 self
.condition
.wait(self
.timeout
)
827 self
.condition
.release()
830 rr
, wr
, er
= select
.select(rs
, [], [], self
.timeout
)
833 self
.readers
[socket
].handle_read()
835 traceback
.print_exc()
839 def getReaders(self
):
841 self
.condition
.acquire()
842 result
= self
.readers
.keys()
843 self
.condition
.release()
846 def addReader(self
, reader
, socket
):
847 self
.condition
.acquire()
848 self
.readers
[socket
] = reader
849 self
.condition
.notify()
850 self
.condition
.release()
852 def delReader(self
, socket
):
853 self
.condition
.acquire()
854 del(self
.readers
[socket
])
855 self
.condition
.notify()
856 self
.condition
.release()
859 self
.condition
.acquire()
860 self
.condition
.notify()
861 self
.condition
.release()
863 class Listener(object):
864 """A Listener is used by this module to listen on the multicast
865 group to which DNS messages are sent, allowing the implementation
866 to cache information as it arrives.
868 It requires registration with an Engine object in order to have
869 the read() method called when a socket is availble for reading."""
871 def __init__(self
, zc
):
873 self
.zc
.engine
.addReader(self
, self
.zc
.socket
)
875 def handle_read(self
):
877 data
, (addr
, port
) = self
.zc
.socket
.recvfrom(_MAX_MSG_ABSOLUTE
)
878 except socket
.error
, e
:
879 # If the socket was closed by another thread -- which happens
880 # regularly on shutdown -- an EBADF exception is thrown here.
882 if e
[0] == socket
.EBADF
:
887 msg
= DNSIncoming(data
)
889 # Always multicast responses
891 if port
== _MDNS_PORT
:
892 self
.zc
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
893 # If it's not a multicast query, reply via unicast
896 elif port
== _DNS_PORT
:
897 self
.zc
.handleQuery(msg
, addr
, port
)
898 self
.zc
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
900 self
.zc
.handleResponse(msg
)
903 class Reaper(threading
.Thread
):
904 """A Reaper is used by this module to remove cache entries that
907 def __init__(self
, zc
):
908 threading
.Thread
.__init
__(self
)
914 self
.zc
.wait(10 * 1000)
917 now
= currentTimeMillis()
918 for record
in self
.zc
.cache
.entries():
919 if record
.isExpired(now
):
920 self
.zc
.updateRecord(now
, record
)
921 self
.zc
.cache
.remove(record
)
924 class ServiceBrowser(threading
.Thread
):
925 """Used to browse for a service of a specific type.
927 The listener object will have its addService() and
928 removeService() methods called when this browser
929 discovers changes in the services availability."""
931 def __init__(self
, zc
, type, listener
):
932 """Creates a browser for a specific type"""
933 threading
.Thread
.__init
__(self
)
936 self
.listener
= listener
938 self
.nextTime
= currentTimeMillis()
939 self
.delay
= _BROWSER_TIME
944 self
.zc
.addListener(self
, DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
947 def updateRecord(self
, zc
, now
, record
):
948 """Callback invoked by Zeroconf when new information arrives.
950 Updates information required by browser in the Zeroconf cache."""
951 if record
.type == _TYPE_PTR
and record
.name
== self
.type:
952 expired
= record
.isExpired(now
)
954 oldrecord
= self
.services
[record
.alias
.lower()]
956 oldrecord
.resetTTL(record
)
958 del(self
.services
[record
.alias
.lower()])
959 callback
= lambda x
: self
.listener
.removeService(x
,
960 self
.type, record
.alias
)
961 self
.list.append(callback
)
965 self
.services
[record
.alias
.lower()] = record
966 callback
= lambda x
: self
.listener
.addService(x
,
967 self
.type, record
.alias
)
968 self
.list.append(callback
)
970 expires
= record
.getExpirationTime(75)
971 if expires
< self
.nextTime
:
972 self
.nextTime
= expires
981 now
= currentTimeMillis()
982 if len(self
.list) == 0 and self
.nextTime
> now
:
983 self
.zc
.wait(self
.nextTime
- now
)
984 if _GLOBAL_DONE
or self
.done
:
986 now
= currentTimeMillis()
988 if self
.nextTime
<= now
:
989 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
990 out
.addQuestion(DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
991 for record
in self
.services
.values():
992 if not record
.isExpired(now
):
993 out
.addAnswerAtTime(record
, now
)
995 self
.nextTime
= now
+ self
.delay
996 self
.delay
= min(20 * 1000, self
.delay
* 2)
998 if len(self
.list) > 0:
999 event
= self
.list.pop(0)
1001 if event
is not None:
1005 class ServiceInfo(object):
1006 """Service information"""
1008 def __init__(self
, type, name
, address
=None, port
=None, weight
=0,
1009 priority
=0, properties
=None, server
=None):
1010 """Create a service description.
1012 type: fully qualified service type name
1013 name: fully qualified service name
1014 address: IP address as unsigned short, network byte order
1015 port: port that the service runs on
1016 weight: weight of the service
1017 priority: priority of the service
1018 properties: dictionary of properties (or a string holding the
1019 bytes for the text field)
1020 server: fully qualified name for service host (defaults to name)"""
1022 if not name
.endswith(type):
1023 raise BadTypeInNameException
1026 self
.address
= address
1028 self
.weight
= weight
1029 self
.priority
= priority
1031 self
.server
= server
1034 self
.setProperties(properties
)
1036 def setProperties(self
, properties
):
1037 """Sets properties and text of this info from a dictionary"""
1038 if isinstance(properties
, dict):
1039 self
.properties
= properties
1042 for key
in properties
:
1043 value
= properties
[key
]
1045 suffix
= ''.encode('utf-8')
1046 elif isinstance(value
, str):
1047 suffix
= value
.encode('utf-8')
1048 elif isinstance(value
, int):
1054 suffix
= ''.encode('utf-8')
1055 list.append('='.join((key
, suffix
)))
1057 result
= ''.join((result
, chr(len(item
)), item
))
1060 self
.text
= properties
1062 def setText(self
, text
):
1063 """Sets properties and text given a text field"""
1071 length
= ord(text
[index
])
1073 strs
.append(text
[index
:index
+length
])
1078 key
, value
= s
.split('=', 1)
1081 elif value
== 'false' or not value
:
1084 # No equals sign at all
1088 # Only update non-existent properties
1089 if key
and result
.get(key
) == None:
1092 self
.properties
= result
1094 traceback
.print_exc()
1095 self
.properties
= None
1103 if self
.type is not None and self
.name
.endswith("." + self
.type):
1104 return self
.name
[:len(self
.name
) - len(self
.type) - 1]
1107 def getAddress(self
):
1108 """Address accessor"""
1115 def getPriority(self
):
1116 """Pirority accessor"""
1117 return self
.priority
1119 def getWeight(self
):
1120 """Weight accessor"""
1123 def getProperties(self
):
1124 """Properties accessor"""
1125 return self
.properties
1131 def getServer(self
):
1132 """Server accessor"""
1135 def updateRecord(self
, zc
, now
, record
):
1136 """Updates service information from a DNS record"""
1137 if record
is not None and not record
.isExpired(now
):
1138 if record
.type == _TYPE_A
:
1139 #if record.name == self.name:
1140 if record
.name
== self
.server
:
1141 self
.address
= record
.address
1142 elif record
.type == _TYPE_SRV
:
1143 if record
.name
== self
.name
:
1144 self
.server
= record
.server
1145 self
.port
= record
.port
1146 self
.weight
= record
.weight
1147 self
.priority
= record
.priority
1148 #self.address = None
1149 self
.updateRecord(zc
, now
,
1150 zc
.cache
.getByDetails(self
.server
, _TYPE_A
, _CLASS_IN
))
1151 elif record
.type == _TYPE_TXT
:
1152 if record
.name
== self
.name
:
1153 self
.setText(record
.text
)
1155 def request(self
, zc
, timeout
):
1156 """Returns true if the service could be discovered on the
1157 network, and updates this object with details discovered.
1159 now
= currentTimeMillis()
1160 delay
= _LISTENER_TIME
1162 last
= now
+ timeout
1165 zc
.addListener(self
, DNSQuestion(self
.name
, _TYPE_ANY
, _CLASS_IN
))
1166 while (self
.server
is None or self
.address
is None or
1171 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
1172 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_SRV
,
1174 out
.addAnswerAtTime(zc
.cache
.getByDetails(self
.name
,
1175 _TYPE_SRV
, _CLASS_IN
), now
)
1176 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_TXT
,
1178 out
.addAnswerAtTime(zc
.cache
.getByDetails(self
.name
,
1179 _TYPE_TXT
, _CLASS_IN
), now
)
1180 if self
.server
is not None:
1181 out
.addQuestion(DNSQuestion(self
.server
,
1182 _TYPE_A
, _CLASS_IN
))
1183 out
.addAnswerAtTime(zc
.cache
.getByDetails(self
.server
,
1184 _TYPE_A
, _CLASS_IN
), now
)
1189 zc
.wait(min(next
, last
) - now
)
1190 now
= currentTimeMillis()
1193 zc
.removeListener(self
)
1197 def __eq__(self
, other
):
1198 """Tests equality of service name"""
1199 if isinstance(other
, ServiceInfo
):
1200 return other
.name
== self
.name
1203 def __ne__(self
, other
):
1204 """Non-equality test"""
1205 return not self
.__eq
__(other
)
1208 """String representation"""
1209 result
= "service[%s,%s:%s," % (self
.name
,
1210 socket
.inet_ntoa(self
.getAddress()), self
.port
)
1211 if self
.text
is None:
1214 if len(self
.text
) < 20:
1217 result
+= self
.text
[:17] + "..."
1222 class Zeroconf(object):
1223 """Implementation of Zeroconf Multicast DNS Service Discovery
1225 Supports registration, unregistration, queries and browsing.
1227 def __init__(self
, bindaddress
=None):
1228 """Creates an instance of the Zeroconf class, establishing
1229 multicast communications, listening and reaping threads."""
1231 _GLOBAL_DONE
= False
1232 if bindaddress
is None:
1233 self
.intf
= socket
.gethostbyname(socket
.gethostname())
1235 self
.intf
= bindaddress
1236 self
.group
= ('', _MDNS_PORT
)
1237 self
.socket
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
1239 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
, 1)
1240 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEPORT
, 1)
1242 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1243 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1244 # Volume 2"), but some BSD-derived systems require
1245 # SO_REUSEPORT to be specified explicity. Also, not all
1246 # versions of Python have SO_REUSEPORT available. So
1247 # if you're on a BSD-based system, and haven't upgraded
1248 # to Python 2.3 yet, you may find this library doesn't
1252 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_TTL
, 255)
1253 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_LOOP
, 1)
1255 self
.socket
.bind(self
.group
)
1257 # Some versions of linux raise an exception even though
1258 # the SO_REUSE* options have been set, so ignore it
1261 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF,
1262 # socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1263 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_ADD_MEMBERSHIP
,
1264 socket
.inet_aton(_MDNS_ADDR
) + socket
.inet_aton('0.0.0.0'))
1269 self
.servicetypes
= {}
1271 self
.cache
= DNSCache()
1273 self
.condition
= threading
.Condition()
1275 self
.engine
= Engine(self
)
1276 self
.listener
= Listener(self
)
1277 self
.reaper
= Reaper(self
)
1279 def isLoopback(self
):
1280 return self
.intf
.startswith("127.0.0.1")
1282 def isLinklocal(self
):
1283 return self
.intf
.startswith("169.254.")
1285 def wait(self
, timeout
):
1286 """Calling thread waits for a given number of milliseconds or
1288 self
.condition
.acquire()
1289 self
.condition
.wait(timeout
/1000)
1290 self
.condition
.release()
1292 def notifyAll(self
):
1293 """Notifies all waiting threads"""
1294 self
.condition
.acquire()
1295 self
.condition
.notifyAll()
1296 self
.condition
.release()
1298 def getServiceInfo(self
, type, name
, timeout
=3000):
1299 """Returns network's service information for a particular
1300 name and type, or None if no service matches by the timeout,
1301 which defaults to 3 seconds."""
1302 info
= ServiceInfo(type, name
)
1303 if info
.request(self
, timeout
):
1307 def addServiceListener(self
, type, listener
):
1308 """Adds a listener for a particular service type. This object
1309 will then have its updateRecord method called when information
1310 arrives for that type."""
1311 self
.removeServiceListener(listener
)
1312 self
.browsers
.append(ServiceBrowser(self
, type, listener
))
1314 def removeServiceListener(self
, listener
):
1315 """Removes a listener from the set that is currently listening."""
1316 for browser
in self
.browsers
:
1317 if browser
.listener
== listener
:
1321 def registerService(self
, info
, ttl
=_DNS_TTL
):
1322 """Registers service information to the network with a default TTL
1323 of 60 seconds. Zeroconf will then respond to requests for
1324 information for that service. The name of the service may be
1325 changed if needed to make it unique on the network."""
1326 self
.checkService(info
)
1327 self
.services
[info
.name
.lower()] = info
1328 if info
.type in self
.servicetypes
:
1329 self
.servicetypes
[info
.type]+=1
1331 self
.servicetypes
[info
.type]=1
1332 now
= currentTimeMillis()
1337 self
.wait(nextTime
- now
)
1338 now
= currentTimeMillis()
1340 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1341 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
,
1342 _CLASS_IN
, ttl
, info
.name
), 0)
1343 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
,
1344 _CLASS_IN
, ttl
, info
.priority
, info
.weight
, info
.port
,
1346 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
,
1349 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
,
1350 _CLASS_IN
, ttl
, info
.address
), 0)
1353 nextTime
+= _REGISTER_TIME
1355 def unregisterService(self
, info
):
1356 """Unregister a service."""
1358 del(self
.services
[info
.name
.lower()])
1359 if self
.servicetypes
[info
.type]>1:
1360 self
.servicetypes
[info
.type]-=1
1362 del self
.servicetypes
[info
.type]
1365 now
= currentTimeMillis()
1370 self
.wait(nextTime
- now
)
1371 now
= currentTimeMillis()
1373 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1374 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
,
1375 _CLASS_IN
, 0, info
.name
), 0)
1376 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
,
1377 _CLASS_IN
, 0, info
.priority
, info
.weight
, info
.port
,
1379 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
,
1382 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
,
1383 _CLASS_IN
, 0, info
.address
), 0)
1386 nextTime
+= _UNREGISTER_TIME
1388 def unregisterAllServices(self
):
1389 """Unregister all registered services."""
1390 if len(self
.services
) > 0:
1391 now
= currentTimeMillis()
1396 self
.wait(nextTime
- now
)
1397 now
= currentTimeMillis()
1399 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1400 for info
in self
.services
.values():
1401 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
,
1402 _CLASS_IN
, 0, info
.name
), 0)
1403 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
,
1404 _CLASS_IN
, 0, info
.priority
, info
.weight
,
1405 info
.port
, info
.server
), 0)
1406 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
,
1407 _CLASS_IN
, 0, info
.text
), 0)
1409 out
.addAnswerAtTime(DNSAddress(info
.server
,
1410 _TYPE_A
, _CLASS_IN
, 0, info
.address
), 0)
1413 nextTime
+= _UNREGISTER_TIME
1415 def checkService(self
, info
):
1416 """Checks the network for a unique service name, modifying the
1417 ServiceInfo passed in if it is not unique."""
1418 now
= currentTimeMillis()
1422 for record
in self
.cache
.entriesWithName(info
.type):
1423 if (record
.type == _TYPE_PTR
and
1424 not record
.isExpired(now
) and
1425 record
.alias
== info
.name
):
1426 if info
.name
.find('.') < 0:
1427 info
.name
= '%s.[%s:%s].%s' % (info
.name
,
1428 info
.address
, info
.port
, info
.type)
1430 self
.checkService(info
)
1432 raise NonUniqueNameException
1434 self
.wait(nextTime
- now
)
1435 now
= currentTimeMillis()
1437 out
= DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA
)
1439 out
.addQuestion(DNSQuestion(info
.type, _TYPE_PTR
, _CLASS_IN
))
1440 out
.addAuthorativeAnswer(DNSPointer(info
.type, _TYPE_PTR
,
1441 _CLASS_IN
, _DNS_TTL
, info
.name
))
1444 nextTime
+= _CHECK_TIME
1446 def addListener(self
, listener
, question
):
1447 """Adds a listener for a given question. The listener will have
1448 its updateRecord method called when information is available to
1449 answer the question."""
1450 now
= currentTimeMillis()
1451 self
.listeners
.append(listener
)
1452 if question
is not None:
1453 for record
in self
.cache
.entriesWithName(question
.name
):
1454 if question
.answeredBy(record
) and not record
.isExpired(now
):
1455 listener
.updateRecord(self
, now
, record
)
1458 def removeListener(self
, listener
):
1459 """Removes a listener."""
1461 self
.listeners
.remove(listener
)
1466 def updateRecord(self
, now
, rec
):
1467 """Used to notify listeners of new information that has updated
1469 for listener
in self
.listeners
:
1470 listener
.updateRecord(self
, now
, rec
)
1473 def handleResponse(self
, msg
):
1474 """Deal with incoming response packets. All answers
1475 are held in the cache, and listeners are notified."""
1476 now
= currentTimeMillis()
1477 for record
in msg
.answers
:
1478 expired
= record
.isExpired(now
)
1479 if record
in self
.cache
.entries():
1481 self
.cache
.remove(record
)
1483 entry
= self
.cache
.get(record
)
1484 if entry
is not None:
1485 entry
.resetTTL(record
)
1488 self
.cache
.add(record
)
1490 self
.updateRecord(now
, record
)
1492 def handleQuery(self
, msg
, addr
, port
):
1493 """Deal with incoming query packets. Provides a response if
1497 # Support unicast client responses
1499 if port
!= _MDNS_PORT
:
1500 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
, False)
1501 for question
in msg
.questions
:
1502 out
.addQuestion(question
)
1504 for question
in msg
.questions
:
1505 if question
.type == _TYPE_PTR
:
1506 if question
.name
== "_services._dns-sd._udp.local.":
1507 for stype
in self
.servicetypes
.keys():
1509 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1511 DNSPointer("_services._dns-sd._udp.local.",
1512 _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, stype
))
1513 for service
in self
.services
.values():
1514 if question
.name
== service
.type:
1516 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1518 DNSPointer(service
.type, _TYPE_PTR
,
1519 _CLASS_IN
, _DNS_TTL
, service
.name
))
1523 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1525 # Answer A record queries for any service addresses we know
1526 if question
.type in (_TYPE_A
, _TYPE_ANY
):
1527 for service
in self
.services
.values():
1528 if service
.server
== question
.name
.lower():
1529 out
.addAnswer(msg
, DNSAddress(question
.name
,
1530 _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
,
1531 _DNS_TTL
, service
.address
))
1533 service
= self
.services
.get(question
.name
.lower(), None)
1534 if not service
: continue
1536 if question
.type in (_TYPE_SRV
, _TYPE_ANY
):
1537 out
.addAnswer(msg
, DNSService(question
.name
,
1538 _TYPE_SRV
, _CLASS_IN | _CLASS_UNIQUE
,
1539 _DNS_TTL
, service
.priority
, service
.weight
,
1540 service
.port
, service
.server
))
1541 if question
.type in (_TYPE_TXT
, _TYPE_ANY
):
1542 out
.addAnswer(msg
, DNSText(question
.name
,
1543 _TYPE_TXT
, _CLASS_IN | _CLASS_UNIQUE
,
1544 _DNS_TTL
, service
.text
))
1545 if question
.type == _TYPE_SRV
:
1546 out
.addAdditionalAnswer(DNSAddress(service
.server
,
1547 _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
,
1548 _DNS_TTL
, service
.address
))
1550 traceback
.print_exc()
1552 if out
is not None and out
.answers
:
1554 self
.send(out
, addr
, port
)
1556 def send(self
, out
, addr
= _MDNS_ADDR
, port
= _MDNS_PORT
):
1557 """Sends an outgoing packet."""
1558 # This is a quick test to see if we can parse the packets we generate
1559 #temp = DNSIncoming(out.packet())
1561 bytes_sent
= self
.socket
.sendto(out
.packet(), 0, (addr
, port
))
1563 # Ignore this, it may be a temporary loss of network connection
1567 """Ends the background threads, and prevent this instance from
1568 servicing further queries."""
1570 if not _GLOBAL_DONE
:
1573 self
.engine
.notify()
1574 self
.unregisterAllServices()
1575 self
.socket
.setsockopt(socket
.SOL_IP
,
1576 socket
.IP_DROP_MEMBERSHIP
,
1577 socket
.inet_aton(_MDNS_ADDR
) +
1578 socket
.inet_aton('0.0.0.0'))
1581 # Test a few module features, including service registration, service
1582 # query (for Zoe), and service unregistration.
1584 if __name__
== '__main__':
1585 print "Multicast DNS Service Discovery for Python, version", __version__
1587 print "1. Testing registration of a service..."
1588 desc
= {'version':'0.10','a':'test value', 'b':'another value'}
1589 info
= ServiceInfo("_http._tcp.local.",
1590 "My Service Name._http._tcp.local.",
1591 socket
.inet_aton("127.0.0.1"), 1234, 0, 0, desc
)
1592 print " Registering service..."
1593 r
.registerService(info
)
1594 print " Registration done."
1595 print "2. Testing query of service information..."
1596 print " Getting ZOE service:",
1597 print str(r
.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1598 print " Query done."
1599 print "3. Testing query of own service..."
1600 print " Getting self:",
1601 print str(r
.getServiceInfo("_http._tcp.local.",
1602 "My Service Name._http._tcp.local."))
1603 print " Query done."
1604 print "4. Testing unregister of service information..."
1605 r
.unregisterService(info
)
1606 print " Unregister done."