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 return unicode(self
.data
[offset
:offset
+len], 'utf-8', 'replace')
577 """Reads a domain name from the packet"""
584 len = ord(self
.data
[off
])
590 result
= ''.join((result
, self
.readUTF(off
, len) + '.'))
595 off
= ((len & 0x3F) << 8) |
ord(self
.data
[off
])
597 raise "Bad domain name (circular) at " + str(off
)
600 raise "Bad domain name at " + str(off
)
610 class DNSOutgoing(object):
611 """Object representation of an outgoing packet"""
613 def __init__(self
, flags
, multicast
= 1):
616 self
.multicast
= multicast
624 self
.authorities
= []
625 self
.additionals
= []
627 def addQuestion(self
, record
):
628 """Adds a question"""
629 self
.questions
.append(record
)
631 def addAnswer(self
, inp
, record
):
633 if not record
.suppressedBy(inp
):
634 self
.addAnswerAtTime(record
, 0)
636 def addAnswerAtTime(self
, record
, now
):
637 """Adds an answer if if does not expire by a certain time"""
638 if record
is not None:
639 if now
== 0 or not record
.isExpired(now
):
640 self
.answers
.append((record
, now
))
642 def addAuthorativeAnswer(self
, record
):
643 """Adds an authoritative answer"""
644 self
.authorities
.append(record
)
646 def addAdditionalAnswer(self
, record
):
647 """Adds an additional answer"""
648 self
.additionals
.append(record
)
650 def writeByte(self
, value
):
651 """Writes a single byte to the packet"""
653 self
.data
.append(struct
.pack(format
, chr(value
)))
656 def insertShort(self
, index
, value
):
657 """Inserts an unsigned short in a certain position in the packet"""
659 self
.data
.insert(index
, struct
.pack(format
, value
))
662 def writeShort(self
, value
):
663 """Writes an unsigned short to the packet"""
665 self
.data
.append(struct
.pack(format
, value
))
668 def writeInt(self
, value
):
669 """Writes an unsigned integer to the packet"""
671 self
.data
.append(struct
.pack(format
, int(value
)))
674 def writeString(self
, value
, length
):
675 """Writes a string to the packet"""
676 format
= '!' + str(length
) + 's'
677 self
.data
.append(struct
.pack(format
, value
))
680 def writeUTF(self
, s
):
681 """Writes a UTF-8 string of a given length to the packet"""
682 utfstr
= s
.encode('utf-8')
685 raise NamePartTooLongException
686 self
.writeByte(length
)
687 self
.writeString(utfstr
, length
)
689 def writeName(self
, name
):
690 """Writes a domain name to the packet"""
693 # Find existing instance of this name in packet
695 index
= self
.names
[name
]
697 # No record of this name already, so write it
698 # out as normal, recording the location of the name
699 # for future pointers to it.
701 self
.names
[name
] = self
.size
702 parts
= name
.split('.')
710 # An index was found, so write a pointer to it
712 self
.writeByte((index
>> 8) |
0xC0)
713 self
.writeByte(index
)
715 def writeQuestion(self
, question
):
716 """Writes a question to the packet"""
717 self
.writeName(question
.name
)
718 self
.writeShort(question
.type)
719 self
.writeShort(question
.clazz
)
721 def writeRecord(self
, record
, now
):
722 """Writes a record (answer, authoritative answer, additional) to
724 self
.writeName(record
.name
)
725 self
.writeShort(record
.type)
726 if record
.unique
and self
.multicast
:
727 self
.writeShort(record
.clazz | _CLASS_UNIQUE
)
729 self
.writeShort(record
.clazz
)
731 self
.writeInt(record
.ttl
)
733 self
.writeInt(record
.getRemainingTTL(now
))
734 index
= len(self
.data
)
735 # Adjust size for the short we will write before this record
741 length
= len(''.join(self
.data
[index
:]))
742 self
.insertShort(index
, length
) # Here is the short we adjusted for
745 """Returns a string containing the packet's bytes
747 No further parts should be added to the packet once this
749 if not self
.finished
:
751 for question
in self
.questions
:
752 self
.writeQuestion(question
)
753 for answer
, time
in self
.answers
:
754 self
.writeRecord(answer
, time
)
755 for authority
in self
.authorities
:
756 self
.writeRecord(authority
, 0)
757 for additional
in self
.additionals
:
758 self
.writeRecord(additional
, 0)
760 self
.insertShort(0, len(self
.additionals
))
761 self
.insertShort(0, len(self
.authorities
))
762 self
.insertShort(0, len(self
.answers
))
763 self
.insertShort(0, len(self
.questions
))
764 self
.insertShort(0, self
.flags
)
766 self
.insertShort(0, 0)
768 self
.insertShort(0, self
.id)
769 return ''.join(self
.data
)
772 class DNSCache(object):
773 """A cache of DNS entries"""
778 def add(self
, entry
):
781 list = self
.cache
[entry
.key
]
783 list = self
.cache
[entry
.key
] = []
786 def remove(self
, entry
):
787 """Removes an entry"""
789 list = self
.cache
[entry
.key
]
794 def get(self
, entry
):
795 """Gets an entry by key. Will return None if there is no
798 list = self
.cache
[entry
.key
]
799 return list[list.index(entry
)]
803 def getByDetails(self
, name
, type, clazz
):
804 """Gets an entry by details. Will return None if there is
805 no matching entry."""
806 entry
= DNSEntry(name
, type, clazz
)
807 return self
.get(entry
)
809 def entriesWithName(self
, name
):
810 """Returns a list of entries whose key matches the name."""
812 return self
.cache
[name
]
817 """Returns a list of all entries"""
818 def add(x
, y
): return x
+y
820 return reduce(add
, self
.cache
.values())
825 class Engine(threading
.Thread
):
826 """An engine wraps read access to sockets, allowing objects that
827 need to receive data from sockets to be called back when the
830 A reader needs a handle_read() method, which is called when the socket
831 it is interested in is ready for reading.
833 Writers are not implemented here, because we only send short
837 def __init__(self
, zeroconf
):
838 threading
.Thread
.__init
__(self
)
839 self
.zeroconf
= zeroconf
840 self
.readers
= {} # maps socket to reader
842 self
.condition
= threading
.Condition()
846 while not globals()['_GLOBAL_DONE']:
847 rs
= self
.getReaders()
849 # No sockets to manage, but we wait for the timeout
850 # or addition of a socket
852 self
.condition
.acquire()
853 self
.condition
.wait(self
.timeout
)
854 self
.condition
.release()
857 rr
, wr
, er
= select
.select(rs
, [], [], self
.timeout
)
860 self
.readers
[socket
].handle_read()
862 traceback
.print_exc()
866 def getReaders(self
):
868 self
.condition
.acquire()
869 result
= self
.readers
.keys()
870 self
.condition
.release()
873 def addReader(self
, reader
, socket
):
874 self
.condition
.acquire()
875 self
.readers
[socket
] = reader
876 self
.condition
.notify()
877 self
.condition
.release()
879 def delReader(self
, socket
):
880 self
.condition
.acquire()
881 del(self
.readers
[socket
])
882 self
.condition
.notify()
883 self
.condition
.release()
886 self
.condition
.acquire()
887 self
.condition
.notify()
888 self
.condition
.release()
890 class Listener(object):
891 """A Listener is used by this module to listen on the multicast
892 group to which DNS messages are sent, allowing the implementation
893 to cache information as it arrives.
895 It requires registration with an Engine object in order to have
896 the read() method called when a socket is availble for reading."""
898 def __init__(self
, zeroconf
):
899 self
.zeroconf
= zeroconf
900 self
.zeroconf
.engine
.addReader(self
, self
.zeroconf
.socket
)
902 def handle_read(self
):
904 data
, (addr
, port
) = self
.zeroconf
.socket
.recvfrom(_MAX_MSG_ABSOLUTE
)
905 except socket
.error
, e
:
906 # If the socket was closed by another thread -- which happens
907 # regularly on shutdown -- an EBADF exception is thrown here.
909 if e
[0] == socket
.EBADF
:
914 msg
= DNSIncoming(data
)
916 # Always multicast responses
918 if port
== _MDNS_PORT
:
919 self
.zeroconf
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
920 # If it's not a multicast query, reply via unicast
923 elif port
== _DNS_PORT
:
924 self
.zeroconf
.handleQuery(msg
, addr
, port
)
925 self
.zeroconf
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
927 self
.zeroconf
.handleResponse(msg
)
930 class Reaper(threading
.Thread
):
931 """A Reaper is used by this module to remove cache entries that
934 def __init__(self
, zeroconf
):
935 threading
.Thread
.__init
__(self
)
936 self
.zeroconf
= zeroconf
941 self
.zeroconf
.wait(10 * 1000)
942 if globals()['_GLOBAL_DONE']:
944 now
= currentTimeMillis()
945 for record
in self
.zeroconf
.cache
.entries():
946 if record
.isExpired(now
):
947 self
.zeroconf
.updateRecord(now
, record
)
948 self
.zeroconf
.cache
.remove(record
)
951 class ServiceBrowser(threading
.Thread
):
952 """Used to browse for a service of a specific type.
954 The listener object will have its addService() and
955 removeService() methods called when this browser
956 discovers changes in the services availability."""
958 def __init__(self
, zeroconf
, type, listener
):
959 """Creates a browser for a specific type"""
960 threading
.Thread
.__init
__(self
)
961 self
.zeroconf
= zeroconf
963 self
.listener
= listener
965 self
.nextTime
= currentTimeMillis()
966 self
.delay
= _BROWSER_TIME
971 self
.zeroconf
.addListener(self
, DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
974 def updateRecord(self
, zeroconf
, now
, record
):
975 """Callback invoked by Zeroconf when new information arrives.
977 Updates information required by browser in the Zeroconf cache."""
978 if record
.type == _TYPE_PTR
and record
.name
== self
.type:
979 expired
= record
.isExpired(now
)
981 oldrecord
= self
.services
[record
.alias
.lower()]
983 oldrecord
.resetTTL(record
)
985 del(self
.services
[record
.alias
.lower()])
986 callback
= lambda x
: self
.listener
.removeService(x
, self
.type, record
.alias
)
987 self
.list.append(callback
)
991 self
.services
[record
.alias
.lower()] = record
992 callback
= lambda x
: self
.listener
.addService(x
, self
.type, record
.alias
)
993 self
.list.append(callback
)
995 expires
= record
.getExpirationTime(75)
996 if expires
< self
.nextTime
:
997 self
.nextTime
= expires
1001 self
.zeroconf
.notifyAll()
1006 now
= currentTimeMillis()
1007 if len(self
.list) == 0 and self
.nextTime
> now
:
1008 self
.zeroconf
.wait(self
.nextTime
- now
)
1009 if globals()['_GLOBAL_DONE'] or self
.done
:
1011 now
= currentTimeMillis()
1013 if self
.nextTime
<= now
:
1014 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
1015 out
.addQuestion(DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
1016 for record
in self
.services
.values():
1017 if not record
.isExpired(now
):
1018 out
.addAnswerAtTime(record
, now
)
1019 self
.zeroconf
.send(out
)
1020 self
.nextTime
= now
+ self
.delay
1021 self
.delay
= min(20 * 1000, self
.delay
* 2)
1023 if len(self
.list) > 0:
1024 event
= self
.list.pop(0)
1026 if event
is not None:
1027 event(self
.zeroconf
)
1030 class ServiceInfo(object):
1031 """Service information"""
1033 def __init__(self
, type, name
, address
=None, port
=None, weight
=0, priority
=0, properties
=None, server
=None):
1034 """Create a service description.
1036 type: fully qualified service type name
1037 name: fully qualified service name
1038 address: IP address as unsigned short, network byte order
1039 port: port that the service runs on
1040 weight: weight of the service
1041 priority: priority of the service
1042 properties: dictionary of properties (or a string holding the bytes for the text field)
1043 server: fully qualified name for service host (defaults to name)"""
1045 if not name
.endswith(type):
1046 raise BadTypeInNameException
1049 self
.address
= address
1051 self
.weight
= weight
1052 self
.priority
= priority
1054 self
.server
= server
1057 self
.setProperties(properties
)
1059 def setProperties(self
, properties
):
1060 """Sets properties and text of this info from a dictionary"""
1061 if isinstance(properties
, dict):
1062 self
.properties
= properties
1065 for key
in properties
:
1066 value
= properties
[key
]
1068 suffix
= ''.encode('utf-8')
1069 elif isinstance(value
, str):
1070 suffix
= value
.encode('utf-8')
1071 elif isinstance(value
, int):
1077 suffix
= ''.encode('utf-8')
1078 list.append('='.join((key
, suffix
)))
1080 result
= ''.join((result
, struct
.pack('!c', chr(len(item
))), item
))
1083 self
.text
= properties
1085 def setText(self
, text
):
1086 """Sets properties and text given a text field"""
1094 length
= ord(text
[index
])
1096 strs
.append(text
[index
:index
+length
])
1100 eindex
= s
.find('=')
1102 # No equals sign at all
1107 value
= s
[eindex
+1:]
1110 elif value
== 'false' or not value
:
1113 # Only update non-existent properties
1114 if key
and result
.get(key
) == None:
1117 self
.properties
= result
1119 traceback
.print_exc()
1120 self
.properties
= None
1128 if self
.type is not None and self
.name
.endswith("." + self
.type):
1129 return self
.name
[:len(self
.name
) - len(self
.type) - 1]
1132 def getAddress(self
):
1133 """Address accessor"""
1140 def getPriority(self
):
1141 """Pirority accessor"""
1142 return self
.priority
1144 def getWeight(self
):
1145 """Weight accessor"""
1148 def getProperties(self
):
1149 """Properties accessor"""
1150 return self
.properties
1156 def getServer(self
):
1157 """Server accessor"""
1160 def updateRecord(self
, zeroconf
, now
, record
):
1161 """Updates service information from a DNS record"""
1162 if record
is not None and not record
.isExpired(now
):
1163 if record
.type == _TYPE_A
:
1164 #if record.name == self.name:
1165 if record
.name
== self
.server
:
1166 self
.address
= record
.address
1167 elif record
.type == _TYPE_SRV
:
1168 if record
.name
== self
.name
:
1169 self
.server
= record
.server
1170 self
.port
= record
.port
1171 self
.weight
= record
.weight
1172 self
.priority
= record
.priority
1173 #self.address = None
1174 self
.updateRecord(zeroconf
, now
, zeroconf
.cache
.getByDetails(self
.server
, _TYPE_A
, _CLASS_IN
))
1175 elif record
.type == _TYPE_TXT
:
1176 if record
.name
== self
.name
:
1177 self
.setText(record
.text
)
1179 def request(self
, zeroconf
, timeout
):
1180 """Returns true if the service could be discovered on the
1181 network, and updates this object with details discovered.
1183 now
= currentTimeMillis()
1184 delay
= _LISTENER_TIME
1186 last
= now
+ timeout
1189 zeroconf
.addListener(self
, DNSQuestion(self
.name
, _TYPE_ANY
, _CLASS_IN
))
1190 while self
.server
is None or self
.address
is None or self
.text
is None:
1194 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
1195 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_SRV
, _CLASS_IN
))
1196 out
.addAnswerAtTime(zeroconf
.cache
.getByDetails(self
.name
, _TYPE_SRV
, _CLASS_IN
), now
)
1197 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_TXT
, _CLASS_IN
))
1198 out
.addAnswerAtTime(zeroconf
.cache
.getByDetails(self
.name
, _TYPE_TXT
, _CLASS_IN
), now
)
1199 if self
.server
is not None:
1200 out
.addQuestion(DNSQuestion(self
.server
, _TYPE_A
, _CLASS_IN
))
1201 out
.addAnswerAtTime(zeroconf
.cache
.getByDetails(self
.server
, _TYPE_A
, _CLASS_IN
), now
)
1206 zeroconf
.wait(min(next
, last
) - now
)
1207 now
= currentTimeMillis()
1210 zeroconf
.removeListener(self
)
1214 def __eq__(self
, other
):
1215 """Tests equality of service name"""
1216 if isinstance(other
, ServiceInfo
):
1217 return other
.name
== self
.name
1220 def __ne__(self
, other
):
1221 """Non-equality test"""
1222 return not self
.__eq
__(other
)
1225 """String representation"""
1226 result
= "service[%s,%s:%s," % (self
.name
, socket
.inet_ntoa(self
.getAddress()), self
.port
)
1227 if self
.text
is None:
1230 if len(self
.text
) < 20:
1233 result
+= self
.text
[:17] + "..."
1238 class Zeroconf(object):
1239 """Implementation of Zeroconf Multicast DNS Service Discovery
1241 Supports registration, unregistration, queries and browsing.
1243 def __init__(self
, bindaddress
=None):
1244 """Creates an instance of the Zeroconf class, establishing
1245 multicast communications, listening and reaping threads."""
1246 globals()['_GLOBAL_DONE'] = 0
1247 if bindaddress
is None:
1248 self
.intf
= socket
.gethostbyname(socket
.gethostname())
1250 self
.intf
= bindaddress
1251 self
.group
= ('', _MDNS_PORT
)
1252 self
.socket
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
1254 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
, 1)
1255 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEPORT
, 1)
1257 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1258 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1259 # Volume 2"), but some BSD-derived systems require
1260 # SO_REUSEPORT to be specified explicity. Also, not all
1261 # versions of Python have SO_REUSEPORT available. So
1262 # if you're on a BSD-based system, and haven't upgraded
1263 # to Python 2.3 yet, you may find this library doesn't
1267 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_TTL
, 255)
1268 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_LOOP
, 1)
1270 self
.socket
.bind(self
.group
)
1272 # Some versions of linux raise an exception even though
1273 # the SO_REUSE* options have been set, so ignore it
1276 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1277 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_ADD_MEMBERSHIP
, socket
.inet_aton(_MDNS_ADDR
) + socket
.inet_aton('0.0.0.0'))
1282 self
.servicetypes
= {}
1284 self
.cache
= DNSCache()
1286 self
.condition
= threading
.Condition()
1288 self
.engine
= Engine(self
)
1289 self
.listener
= Listener(self
)
1290 self
.reaper
= Reaper(self
)
1292 def isLoopback(self
):
1293 return self
.intf
.startswith("127.0.0.1")
1295 def isLinklocal(self
):
1296 return self
.intf
.startswith("169.254.")
1298 def wait(self
, timeout
):
1299 """Calling thread waits for a given number of milliseconds or
1301 self
.condition
.acquire()
1302 self
.condition
.wait(timeout
/1000)
1303 self
.condition
.release()
1305 def notifyAll(self
):
1306 """Notifies all waiting threads"""
1307 self
.condition
.acquire()
1308 self
.condition
.notifyAll()
1309 self
.condition
.release()
1311 def getServiceInfo(self
, type, name
, timeout
=3000):
1312 """Returns network's service information for a particular
1313 name and type, or None if no service matches by the timeout,
1314 which defaults to 3 seconds."""
1315 info
= ServiceInfo(type, name
)
1316 if info
.request(self
, timeout
):
1320 def addServiceListener(self
, type, listener
):
1321 """Adds a listener for a particular service type. This object
1322 will then have its updateRecord method called when information
1323 arrives for that type."""
1324 self
.removeServiceListener(listener
)
1325 self
.browsers
.append(ServiceBrowser(self
, type, listener
))
1327 def removeServiceListener(self
, listener
):
1328 """Removes a listener from the set that is currently listening."""
1329 for browser
in self
.browsers
:
1330 if browser
.listener
== listener
:
1334 def registerService(self
, info
, ttl
=_DNS_TTL
):
1335 """Registers service information to the network with a default TTL
1336 of 60 seconds. Zeroconf will then respond to requests for
1337 information for that service. The name of the service may be
1338 changed if needed to make it unique on the network."""
1339 self
.checkService(info
)
1340 self
.services
[info
.name
.lower()] = info
1341 if info
.type in self
.servicetypes
:
1342 self
.servicetypes
[info
.type]+=1
1344 self
.servicetypes
[info
.type]=1
1345 now
= currentTimeMillis()
1350 self
.wait(nextTime
- now
)
1351 now
= currentTimeMillis()
1353 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1354 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, ttl
, info
.name
), 0)
1355 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
, _CLASS_IN
, ttl
, info
.priority
, info
.weight
, info
.port
, info
.server
), 0)
1356 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
, ttl
, info
.text
), 0)
1358 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, ttl
, info
.address
), 0)
1361 nextTime
+= _REGISTER_TIME
1363 def unregisterService(self
, info
):
1364 """Unregister a service."""
1366 del(self
.services
[info
.name
.lower()])
1367 if self
.servicetypes
[info
.type]>1:
1368 self
.servicetypes
[info
.type]-=1
1370 del self
.servicetypes
[info
.type]
1373 now
= currentTimeMillis()
1378 self
.wait(nextTime
- now
)
1379 now
= currentTimeMillis()
1381 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1382 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, 0, info
.name
), 0)
1383 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
, _CLASS_IN
, 0, info
.priority
, info
.weight
, info
.port
, info
.name
), 0)
1384 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
, 0, info
.text
), 0)
1386 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, 0, info
.address
), 0)
1389 nextTime
+= _UNREGISTER_TIME
1391 def unregisterAllServices(self
):
1392 """Unregister all registered services."""
1393 if len(self
.services
) > 0:
1394 now
= currentTimeMillis()
1399 self
.wait(nextTime
- now
)
1400 now
= currentTimeMillis()
1402 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1403 for info
in self
.services
.values():
1404 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, 0, info
.name
), 0)
1405 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
, _CLASS_IN
, 0, info
.priority
, info
.weight
, info
.port
, info
.server
), 0)
1406 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
, 0, info
.text
), 0)
1408 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, 0, info
.address
), 0)
1411 nextTime
+= _UNREGISTER_TIME
1413 def checkService(self
, info
):
1414 """Checks the network for a unique service name, modifying the
1415 ServiceInfo passed in if it is not unique."""
1416 now
= currentTimeMillis()
1420 for record
in self
.cache
.entriesWithName(info
.type):
1421 if record
.type == _TYPE_PTR
and not record
.isExpired(now
) and record
.alias
== info
.name
:
1422 if (info
.name
.find('.') < 0):
1423 info
.name
= info
.name
+ ".[" + info
.address
+ ":" + info
.port
+ "]." + info
.type
1424 self
.checkService(info
)
1426 raise NonUniqueNameException
1428 self
.wait(nextTime
- now
)
1429 now
= currentTimeMillis()
1431 out
= DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA
)
1433 out
.addQuestion(DNSQuestion(info
.type, _TYPE_PTR
, _CLASS_IN
))
1434 out
.addAuthorativeAnswer(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, info
.name
))
1437 nextTime
+= _CHECK_TIME
1439 def addListener(self
, listener
, question
):
1440 """Adds a listener for a given question. The listener will have
1441 its updateRecord method called when information is available to
1442 answer the question."""
1443 now
= currentTimeMillis()
1444 self
.listeners
.append(listener
)
1445 if question
is not None:
1446 for record
in self
.cache
.entriesWithName(question
.name
):
1447 if question
.answeredBy(record
) and not record
.isExpired(now
):
1448 listener
.updateRecord(self
, now
, record
)
1451 def removeListener(self
, listener
):
1452 """Removes a listener."""
1454 self
.listeners
.remove(listener
)
1459 def updateRecord(self
, now
, rec
):
1460 """Used to notify listeners of new information that has updated
1462 for listener
in self
.listeners
:
1463 listener
.updateRecord(self
, now
, rec
)
1466 def handleResponse(self
, msg
):
1467 """Deal with incoming response packets. All answers
1468 are held in the cache, and listeners are notified."""
1469 now
= currentTimeMillis()
1470 for record
in msg
.answers
:
1471 expired
= record
.isExpired(now
)
1472 if record
in self
.cache
.entries():
1474 self
.cache
.remove(record
)
1476 entry
= self
.cache
.get(record
)
1477 if entry
is not None:
1478 entry
.resetTTL(record
)
1481 self
.cache
.add(record
)
1483 self
.updateRecord(now
, record
)
1485 def handleQuery(self
, msg
, addr
, port
):
1486 """Deal with incoming query packets. Provides a response if
1490 # Support unicast client responses
1492 if port
!= _MDNS_PORT
:
1493 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
, 0)
1494 for question
in msg
.questions
:
1495 out
.addQuestion(question
)
1497 for question
in msg
.questions
:
1498 if question
.type == _TYPE_PTR
:
1499 if question
.name
== "_services._dns-sd._udp.local.":
1500 for stype
in self
.servicetypes
.keys():
1502 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1503 out
.addAnswer(msg
, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, stype
))
1504 for service
in self
.services
.values():
1505 if question
.name
== service
.type:
1507 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1508 out
.addAnswer(msg
, DNSPointer(service
.type, _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, service
.name
))
1512 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1514 # Answer A record queries for any service addresses we know
1515 if question
.type == _TYPE_A
or question
.type == _TYPE_ANY
:
1516 for service
in self
.services
.values():
1517 if service
.server
== question
.name
.lower():
1518 out
.addAnswer(msg
, DNSAddress(question
.name
, _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.address
))
1520 service
= self
.services
.get(question
.name
.lower(), None)
1521 if not service
: continue
1523 if question
.type == _TYPE_SRV
or question
.type == _TYPE_ANY
:
1524 out
.addAnswer(msg
, DNSService(question
.name
, _TYPE_SRV
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.priority
, service
.weight
, service
.port
, service
.server
))
1525 if question
.type == _TYPE_TXT
or question
.type == _TYPE_ANY
:
1526 out
.addAnswer(msg
, DNSText(question
.name
, _TYPE_TXT
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.text
))
1527 if question
.type == _TYPE_SRV
:
1528 out
.addAdditionalAnswer(DNSAddress(service
.server
, _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.address
))
1530 traceback
.print_exc()
1532 if out
is not None and out
.answers
:
1534 self
.send(out
, addr
, port
)
1536 def send(self
, out
, addr
= _MDNS_ADDR
, port
= _MDNS_PORT
):
1537 """Sends an outgoing packet."""
1538 # This is a quick test to see if we can parse the packets we generate
1539 #temp = DNSIncoming(out.packet())
1541 bytes_sent
= self
.socket
.sendto(out
.packet(), 0, (addr
, port
))
1543 # Ignore this, it may be a temporary loss of network connection
1547 """Ends the background threads, and prevent this instance from
1548 servicing further queries."""
1549 if globals()['_GLOBAL_DONE'] == 0:
1550 globals()['_GLOBAL_DONE'] = 1
1552 self
.engine
.notify()
1553 self
.unregisterAllServices()
1554 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_DROP_MEMBERSHIP
, socket
.inet_aton(_MDNS_ADDR
) + socket
.inet_aton('0.0.0.0'))
1557 # Test a few module features, including service registration, service
1558 # query (for Zoe), and service unregistration.
1560 if __name__
== '__main__':
1561 print "Multicast DNS Service Discovery for Python, version", __version__
1563 print "1. Testing registration of a service..."
1564 desc
= {'version':'0.10','a':'test value', 'b':'another value'}
1565 info
= ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket
.inet_aton("127.0.0.1"), 1234, 0, 0, desc
)
1566 print " Registering service..."
1567 r
.registerService(info
)
1568 print " Registration done."
1569 print "2. Testing query of service information..."
1570 print " Getting ZOE service:", str(r
.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1571 print " Query done."
1572 print "3. Testing query of own service..."
1573 print " Getting self:", str(r
.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local."))
1574 print " Query done."
1575 print "4. Testing unregister of service information..."
1576 r
.unregisterService(info
)
1577 print " Unregister done."