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:
1234 s
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
1235 s
.connect(('4.2.2.1', 123))
1236 self
.intf
= s
.getsockname()[0]
1238 self
.intf
= socket
.gethostbyname(socket
.gethostname())
1240 self
.intf
= bindaddress
1241 self
.group
= ('', _MDNS_PORT
)
1242 self
.socket
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
1244 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
, 1)
1245 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEPORT
, 1)
1247 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1248 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1249 # Volume 2"), but some BSD-derived systems require
1250 # SO_REUSEPORT to be specified explicity. Also, not all
1251 # versions of Python have SO_REUSEPORT available. So
1252 # if you're on a BSD-based system, and haven't upgraded
1253 # to Python 2.3 yet, you may find this library doesn't
1257 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_TTL
, 255)
1258 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_LOOP
, 1)
1260 self
.socket
.bind(self
.group
)
1262 # Some versions of linux raise an exception even though
1263 # the SO_REUSE* options have been set, so ignore it
1266 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF,
1267 # socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1268 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_ADD_MEMBERSHIP
,
1269 socket
.inet_aton(_MDNS_ADDR
) + socket
.inet_aton('0.0.0.0'))
1274 self
.servicetypes
= {}
1276 self
.cache
= DNSCache()
1278 self
.condition
= threading
.Condition()
1280 self
.engine
= Engine(self
)
1281 self
.listener
= Listener(self
)
1282 self
.reaper
= Reaper(self
)
1284 def isLoopback(self
):
1285 return self
.intf
.startswith("127.0.0.1")
1287 def isLinklocal(self
):
1288 return self
.intf
.startswith("169.254.")
1290 def wait(self
, timeout
):
1291 """Calling thread waits for a given number of milliseconds or
1293 self
.condition
.acquire()
1294 self
.condition
.wait(timeout
/1000)
1295 self
.condition
.release()
1297 def notifyAll(self
):
1298 """Notifies all waiting threads"""
1299 self
.condition
.acquire()
1300 self
.condition
.notifyAll()
1301 self
.condition
.release()
1303 def getServiceInfo(self
, type, name
, timeout
=3000):
1304 """Returns network's service information for a particular
1305 name and type, or None if no service matches by the timeout,
1306 which defaults to 3 seconds."""
1307 info
= ServiceInfo(type, name
)
1308 if info
.request(self
, timeout
):
1312 def addServiceListener(self
, type, listener
):
1313 """Adds a listener for a particular service type. This object
1314 will then have its updateRecord method called when information
1315 arrives for that type."""
1316 self
.removeServiceListener(listener
)
1317 self
.browsers
.append(ServiceBrowser(self
, type, listener
))
1319 def removeServiceListener(self
, listener
):
1320 """Removes a listener from the set that is currently listening."""
1321 for browser
in self
.browsers
:
1322 if browser
.listener
== listener
:
1326 def registerService(self
, info
, ttl
=_DNS_TTL
):
1327 """Registers service information to the network with a default TTL
1328 of 60 seconds. Zeroconf will then respond to requests for
1329 information for that service. The name of the service may be
1330 changed if needed to make it unique on the network."""
1331 self
.checkService(info
)
1332 self
.services
[info
.name
.lower()] = info
1333 if info
.type in self
.servicetypes
:
1334 self
.servicetypes
[info
.type]+=1
1336 self
.servicetypes
[info
.type]=1
1337 now
= currentTimeMillis()
1342 self
.wait(nextTime
- now
)
1343 now
= currentTimeMillis()
1345 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1346 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
,
1347 _CLASS_IN
, ttl
, info
.name
), 0)
1348 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
,
1349 _CLASS_IN
, ttl
, info
.priority
, info
.weight
, info
.port
,
1351 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
,
1354 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
,
1355 _CLASS_IN
, ttl
, info
.address
), 0)
1358 nextTime
+= _REGISTER_TIME
1360 def unregisterService(self
, info
):
1361 """Unregister a service."""
1363 del(self
.services
[info
.name
.lower()])
1364 if self
.servicetypes
[info
.type]>1:
1365 self
.servicetypes
[info
.type]-=1
1367 del self
.servicetypes
[info
.type]
1370 now
= currentTimeMillis()
1375 self
.wait(nextTime
- now
)
1376 now
= currentTimeMillis()
1378 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1379 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
,
1380 _CLASS_IN
, 0, info
.name
), 0)
1381 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
,
1382 _CLASS_IN
, 0, info
.priority
, info
.weight
, info
.port
,
1384 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
,
1387 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
,
1388 _CLASS_IN
, 0, info
.address
), 0)
1391 nextTime
+= _UNREGISTER_TIME
1393 def unregisterAllServices(self
):
1394 """Unregister all registered services."""
1395 if len(self
.services
) > 0:
1396 now
= currentTimeMillis()
1401 self
.wait(nextTime
- now
)
1402 now
= currentTimeMillis()
1404 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1405 for info
in self
.services
.values():
1406 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
,
1407 _CLASS_IN
, 0, info
.name
), 0)
1408 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
,
1409 _CLASS_IN
, 0, info
.priority
, info
.weight
,
1410 info
.port
, info
.server
), 0)
1411 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
,
1412 _CLASS_IN
, 0, info
.text
), 0)
1414 out
.addAnswerAtTime(DNSAddress(info
.server
,
1415 _TYPE_A
, _CLASS_IN
, 0, info
.address
), 0)
1418 nextTime
+= _UNREGISTER_TIME
1420 def checkService(self
, info
):
1421 """Checks the network for a unique service name, modifying the
1422 ServiceInfo passed in if it is not unique."""
1423 now
= currentTimeMillis()
1427 for record
in self
.cache
.entriesWithName(info
.type):
1428 if (record
.type == _TYPE_PTR
and
1429 not record
.isExpired(now
) and
1430 record
.alias
== info
.name
):
1431 if info
.name
.find('.') < 0:
1432 info
.name
= '%s.[%s:%s].%s' % (info
.name
,
1433 info
.address
, info
.port
, info
.type)
1435 self
.checkService(info
)
1437 raise NonUniqueNameException
1439 self
.wait(nextTime
- now
)
1440 now
= currentTimeMillis()
1442 out
= DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA
)
1444 out
.addQuestion(DNSQuestion(info
.type, _TYPE_PTR
, _CLASS_IN
))
1445 out
.addAuthorativeAnswer(DNSPointer(info
.type, _TYPE_PTR
,
1446 _CLASS_IN
, _DNS_TTL
, info
.name
))
1449 nextTime
+= _CHECK_TIME
1451 def addListener(self
, listener
, question
):
1452 """Adds a listener for a given question. The listener will have
1453 its updateRecord method called when information is available to
1454 answer the question."""
1455 now
= currentTimeMillis()
1456 self
.listeners
.append(listener
)
1457 if question
is not None:
1458 for record
in self
.cache
.entriesWithName(question
.name
):
1459 if question
.answeredBy(record
) and not record
.isExpired(now
):
1460 listener
.updateRecord(self
, now
, record
)
1463 def removeListener(self
, listener
):
1464 """Removes a listener."""
1466 self
.listeners
.remove(listener
)
1471 def updateRecord(self
, now
, rec
):
1472 """Used to notify listeners of new information that has updated
1474 for listener
in self
.listeners
:
1475 listener
.updateRecord(self
, now
, rec
)
1478 def handleResponse(self
, msg
):
1479 """Deal with incoming response packets. All answers
1480 are held in the cache, and listeners are notified."""
1481 now
= currentTimeMillis()
1482 for record
in msg
.answers
:
1483 expired
= record
.isExpired(now
)
1484 if record
in self
.cache
.entries():
1486 self
.cache
.remove(record
)
1488 entry
= self
.cache
.get(record
)
1489 if entry
is not None:
1490 entry
.resetTTL(record
)
1493 self
.cache
.add(record
)
1495 self
.updateRecord(now
, record
)
1497 def handleQuery(self
, msg
, addr
, port
):
1498 """Deal with incoming query packets. Provides a response if
1502 # Support unicast client responses
1504 if port
!= _MDNS_PORT
:
1505 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
, False)
1506 for question
in msg
.questions
:
1507 out
.addQuestion(question
)
1509 for question
in msg
.questions
:
1510 if question
.type == _TYPE_PTR
:
1511 if question
.name
== "_services._dns-sd._udp.local.":
1512 for stype
in self
.servicetypes
.keys():
1514 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1516 DNSPointer("_services._dns-sd._udp.local.",
1517 _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, stype
))
1518 for service
in self
.services
.values():
1519 if question
.name
== service
.type:
1521 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1523 DNSPointer(service
.type, _TYPE_PTR
,
1524 _CLASS_IN
, _DNS_TTL
, service
.name
))
1528 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1530 # Answer A record queries for any service addresses we know
1531 if question
.type in (_TYPE_A
, _TYPE_ANY
):
1532 for service
in self
.services
.values():
1533 if service
.server
== question
.name
.lower():
1534 out
.addAnswer(msg
, DNSAddress(question
.name
,
1535 _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
,
1536 _DNS_TTL
, service
.address
))
1538 service
= self
.services
.get(question
.name
.lower(), None)
1539 if not service
: continue
1541 if question
.type in (_TYPE_SRV
, _TYPE_ANY
):
1542 out
.addAnswer(msg
, DNSService(question
.name
,
1543 _TYPE_SRV
, _CLASS_IN | _CLASS_UNIQUE
,
1544 _DNS_TTL
, service
.priority
, service
.weight
,
1545 service
.port
, service
.server
))
1546 if question
.type in (_TYPE_TXT
, _TYPE_ANY
):
1547 out
.addAnswer(msg
, DNSText(question
.name
,
1548 _TYPE_TXT
, _CLASS_IN | _CLASS_UNIQUE
,
1549 _DNS_TTL
, service
.text
))
1550 if question
.type == _TYPE_SRV
:
1551 out
.addAdditionalAnswer(DNSAddress(service
.server
,
1552 _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
,
1553 _DNS_TTL
, service
.address
))
1555 traceback
.print_exc()
1557 if out
is not None and out
.answers
:
1559 self
.send(out
, addr
, port
)
1561 def send(self
, out
, addr
= _MDNS_ADDR
, port
= _MDNS_PORT
):
1562 """Sends an outgoing packet."""
1563 # This is a quick test to see if we can parse the packets we generate
1564 #temp = DNSIncoming(out.packet())
1566 bytes_sent
= self
.socket
.sendto(out
.packet(), 0, (addr
, port
))
1568 # Ignore this, it may be a temporary loss of network connection
1572 """Ends the background threads, and prevent this instance from
1573 servicing further queries."""
1575 if not _GLOBAL_DONE
:
1578 self
.engine
.notify()
1579 self
.unregisterAllServices()
1580 self
.socket
.setsockopt(socket
.SOL_IP
,
1581 socket
.IP_DROP_MEMBERSHIP
,
1582 socket
.inet_aton(_MDNS_ADDR
) +
1583 socket
.inet_aton('0.0.0.0'))
1586 # Test a few module features, including service registration, service
1587 # query (for Zoe), and service unregistration.
1589 if __name__
== '__main__':
1590 print "Multicast DNS Service Discovery for Python, version", __version__
1592 print "1. Testing registration of a service..."
1593 desc
= {'version':'0.10','a':'test value', 'b':'another value'}
1594 info
= ServiceInfo("_http._tcp.local.",
1595 "My Service Name._http._tcp.local.",
1596 socket
.inet_aton("127.0.0.1"), 1234, 0, 0, desc
)
1597 print " Registering service..."
1598 r
.registerService(info
)
1599 print " Registration done."
1600 print "2. Testing query of service information..."
1601 print " Getting ZOE service:",
1602 print str(r
.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1603 print " Query done."
1604 print "3. Testing query of own service..."
1605 print " Getting self:",
1606 print str(r
.getServiceInfo("_http._tcp.local.",
1607 "My Service Name._http._tcp.local."))
1608 print " Query done."
1609 print "4. Testing unregister of service information..."
1610 r
.unregisterService(info
)
1611 print " Unregister done."