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"
89 __all__
= ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
93 globals()['_GLOBAL_DONE'] = 0
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
= string
.lower(name
)
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 if isinstance(other
, DNSEntry
):
222 return self
.name
== other
.name
and self
.type == other
.type and self
.clazz
== other
.clazz
225 def __ne__(self
, other
):
226 """Non-equality test"""
227 return not self
.__eq
__(other
)
229 def getClazz(self
, clazz
):
232 return _CLASSES
[clazz
]
234 return "?(%s)" % (clazz
)
236 def getType(self
, type):
241 return "?(%s)" % (type)
243 def toString(self
, hdr
, other
):
244 """String representation with additional information"""
245 result
= "%s[%s,%s" % (hdr
, self
.getType(self
.type), self
.getClazz(self
.clazz
))
251 if other
is not None:
252 result
+= ",%s]" % (other
)
257 class DNSQuestion(DNSEntry
):
258 """A DNS question entry"""
260 def __init__(self
, name
, type, clazz
):
261 #if not name.endswith(".local."):
262 # raise NonLocalNameException
263 DNSEntry
.__init
__(self
, name
, type, clazz
)
265 def answeredBy(self
, rec
):
266 """Returns true if the question is answered by the record"""
267 return self
.clazz
== rec
.clazz
and (self
.type == rec
.type or self
.type == _TYPE_ANY
) and self
.name
== rec
.name
270 """String representation"""
271 return DNSEntry
.toString(self
, "question", None)
274 class DNSRecord(DNSEntry
):
275 """A DNS record - like a DNS entry, but has a TTL"""
277 def __init__(self
, name
, type, clazz
, ttl
):
278 DNSEntry
.__init
__(self
, name
, type, clazz
)
280 self
.created
= currentTimeMillis()
282 def __eq__(self
, other
):
283 """Tests equality as per DNSRecord"""
284 if isinstance(other
, DNSRecord
):
285 return DNSEntry
.__eq
__(self
, other
)
288 def suppressedBy(self
, msg
):
289 """Returns true if any answer in a message can suffice for the
290 information held in this record."""
291 for record
in msg
.answers
:
292 if self
.suppressedByAnswer(record
):
296 def suppressedByAnswer(self
, other
):
297 """Returns true if another record has same name, type and class,
298 and if its TTL is at least half of this record's."""
299 if 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
, self
.getRemainingTTL(currentTimeMillis()), other
)
333 return DNSEntry
.toString(self
, "record", arg
)
335 class DNSAddress(DNSRecord
):
336 """A DNS address record"""
338 def __init__(self
, name
, type, clazz
, ttl
, address
):
339 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
340 self
.address
= address
342 def write(self
, out
):
343 """Used in constructing an outgoing packet"""
344 out
.writeString(self
.address
, len(self
.address
))
346 def __eq__(self
, other
):
347 """Tests equality on address"""
348 if isinstance(other
, DNSAddress
):
349 return self
.address
== other
.address
353 """String representation"""
355 return socket
.inet_ntoa(self
.address
)
359 class DNSHinfo(DNSRecord
):
360 """A DNS host information record"""
362 def __init__(self
, name
, type, clazz
, ttl
, cpu
, os
):
363 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
367 def write(self
, out
):
368 """Used in constructing an outgoing packet"""
369 out
.writeString(self
.cpu
, len(self
.cpu
))
370 out
.writeString(self
.os
, len(self
.os
))
372 def __eq__(self
, other
):
373 """Tests equality on cpu and os"""
374 if isinstance(other
, DNSHinfo
):
375 return self
.cpu
== other
.cpu
and self
.os
== other
.os
379 """String representation"""
380 return self
.cpu
+ " " + self
.os
382 class DNSPointer(DNSRecord
):
383 """A DNS pointer record"""
385 def __init__(self
, name
, type, clazz
, ttl
, alias
):
386 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
389 def write(self
, out
):
390 """Used in constructing an outgoing packet"""
391 out
.writeName(self
.alias
)
393 def __eq__(self
, other
):
394 """Tests equality on alias"""
395 if isinstance(other
, DNSPointer
):
396 return self
.alias
== other
.alias
400 """String representation"""
401 return self
.toString(self
.alias
)
403 class DNSText(DNSRecord
):
404 """A DNS text record"""
406 def __init__(self
, name
, type, clazz
, ttl
, text
):
407 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
410 def write(self
, out
):
411 """Used in constructing an outgoing packet"""
412 out
.writeString(self
.text
, len(self
.text
))
414 def __eq__(self
, other
):
415 """Tests equality on text"""
416 if isinstance(other
, DNSText
):
417 return self
.text
== other
.text
421 """String representation"""
422 if len(self
.text
) > 10:
423 return self
.toString(self
.text
[:7] + "...")
425 return self
.toString(self
.text
)
427 class DNSService(DNSRecord
):
428 """A DNS service record"""
430 def __init__(self
, name
, type, clazz
, ttl
, priority
, weight
, port
, server
):
431 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
432 self
.priority
= priority
437 def write(self
, out
):
438 """Used in constructing an outgoing packet"""
439 out
.writeShort(self
.priority
)
440 out
.writeShort(self
.weight
)
441 out
.writeShort(self
.port
)
442 out
.writeName(self
.server
)
444 def __eq__(self
, other
):
445 """Tests equality on priority, weight, port and server"""
446 if isinstance(other
, DNSService
):
447 return self
.priority
== other
.priority
and self
.weight
== other
.weight
and self
.port
== other
.port
and self
.server
== other
.server
451 """String representation"""
452 return self
.toString("%s:%s" % (self
.server
, self
.port
))
454 class DNSIncoming(object):
455 """Object representation of an incoming DNS packet"""
457 def __init__(self
, data
):
458 """Constructor from string holding bytes of packet"""
463 self
.numQuestions
= 0
465 self
.numAuthorities
= 0
466 self
.numAdditionals
= 0
472 def readHeader(self
):
473 """Reads header portion of packet"""
475 length
= struct
.calcsize(format
)
476 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
477 self
.offset
+= length
481 self
.numQuestions
= info
[2]
482 self
.numAnswers
= info
[3]
483 self
.numAuthorities
= info
[4]
484 self
.numAdditionals
= info
[5]
486 def readQuestions(self
):
487 """Reads questions section of packet"""
489 length
= struct
.calcsize(format
)
490 for i
in range(0, self
.numQuestions
):
491 name
= self
.readName()
492 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
493 self
.offset
+= length
495 question
= DNSQuestion(name
, info
[0], info
[1])
496 self
.questions
.append(question
)
499 """Reads an integer from the packet"""
501 length
= struct
.calcsize(format
)
502 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
503 self
.offset
+= length
506 def readCharacterString(self
):
507 """Reads a character string from the packet"""
508 length
= ord(self
.data
[self
.offset
])
510 return self
.readString(length
)
512 def readString(self
, len):
513 """Reads a string of a given length from the packet"""
514 format
= '!' + str(len) + 's'
515 length
= struct
.calcsize(format
)
516 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
517 self
.offset
+= length
520 def readUnsignedShort(self
):
521 """Reads an unsigned short from the packet"""
523 length
= struct
.calcsize(format
)
524 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
525 self
.offset
+= length
528 def readOthers(self
):
529 """Reads the answers, authorities and additionals section of the packet"""
531 length
= struct
.calcsize(format
)
532 n
= self
.numAnswers
+ self
.numAuthorities
+ self
.numAdditionals
533 for i
in range(0, n
):
534 domain
= self
.readName()
535 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
536 self
.offset
+= length
539 if info
[0] == _TYPE_A
:
540 rec
= DNSAddress(domain
, info
[0], info
[1], info
[2], self
.readString(4))
541 elif info
[0] == _TYPE_CNAME
or info
[0] == _TYPE_PTR
:
542 rec
= DNSPointer(domain
, info
[0], info
[1], info
[2], self
.readName())
543 elif info
[0] == _TYPE_TXT
:
544 rec
= DNSText(domain
, info
[0], info
[1], info
[2], self
.readString(info
[3]))
545 elif info
[0] == _TYPE_SRV
:
546 rec
= DNSService(domain
, info
[0], info
[1], info
[2], self
.readUnsignedShort(), self
.readUnsignedShort(), self
.readUnsignedShort(), self
.readName())
547 elif info
[0] == _TYPE_HINFO
:
548 rec
= DNSHinfo(domain
, info
[0], info
[1], info
[2], self
.readCharacterString(), self
.readCharacterString())
549 elif info
[0] == _TYPE_AAAA
:
550 rec
= DNSAddress(domain
, info
[0], info
[1], info
[2], self
.readString(16))
552 # Try to ignore types we don't know about
553 # this may mean the rest of the name is
554 # unable to be parsed, and may show errors
555 # so this is left for debugging. New types
556 # encountered need to be parsed properly.
558 #print "UNKNOWN TYPE = " + str(info[0])
559 #raise BadTypeInNameException
563 self
.answers
.append(rec
)
566 """Returns true if this is a query"""
567 return (self
.flags
& _FLAGS_QR_MASK
) == _FLAGS_QR_QUERY
569 def isResponse(self
):
570 """Returns true if this is a response"""
571 return (self
.flags
& _FLAGS_QR_MASK
) == _FLAGS_QR_RESPONSE
573 def readUTF(self
, offset
, len):
574 """Reads a UTF-8 string of a given length from the packet"""
575 result
= self
.data
[offset
:offset
+len].decode('utf-8')
579 """Reads a domain name from the packet"""
586 len = ord(self
.data
[off
])
592 result
= ''.join((result
, self
.readUTF(off
, len) + '.'))
597 off
= ((len & 0x3F) << 8) |
ord(self
.data
[off
])
599 raise "Bad domain name (circular) at " + str(off
)
602 raise "Bad domain name at " + str(off
)
612 class DNSOutgoing(object):
613 """Object representation of an outgoing packet"""
615 def __init__(self
, flags
, multicast
= 1):
618 self
.multicast
= multicast
626 self
.authorities
= []
627 self
.additionals
= []
629 def addQuestion(self
, record
):
630 """Adds a question"""
631 self
.questions
.append(record
)
633 def addAnswer(self
, inp
, record
):
635 if not record
.suppressedBy(inp
):
636 self
.addAnswerAtTime(record
, 0)
638 def addAnswerAtTime(self
, record
, now
):
639 """Adds an answer if if does not expire by a certain time"""
640 if record
is not None:
641 if now
== 0 or not record
.isExpired(now
):
642 self
.answers
.append((record
, now
))
644 def addAuthorativeAnswer(self
, record
):
645 """Adds an authoritative answer"""
646 self
.authorities
.append(record
)
648 def addAdditionalAnswer(self
, record
):
649 """Adds an additional answer"""
650 self
.additionals
.append(record
)
652 def writeByte(self
, value
):
653 """Writes a single byte to the packet"""
655 self
.data
.append(struct
.pack(format
, chr(value
)))
658 def insertShort(self
, index
, value
):
659 """Inserts an unsigned short in a certain position in the packet"""
661 self
.data
.insert(index
, struct
.pack(format
, value
))
664 def writeShort(self
, value
):
665 """Writes an unsigned short to the packet"""
667 self
.data
.append(struct
.pack(format
, value
))
670 def writeInt(self
, value
):
671 """Writes an unsigned integer to the packet"""
673 self
.data
.append(struct
.pack(format
, int(value
)))
676 def writeString(self
, value
, length
):
677 """Writes a string to the packet"""
678 format
= '!' + str(length
) + 's'
679 self
.data
.append(struct
.pack(format
, value
))
682 def writeUTF(self
, s
):
683 """Writes a UTF-8 string of a given length to the packet"""
684 utfstr
= s
.encode('utf-8')
687 raise NamePartTooLongException
688 self
.writeByte(length
)
689 self
.writeString(utfstr
, length
)
691 def writeName(self
, name
):
692 """Writes a domain name to the packet"""
695 # Find existing instance of this name in packet
697 index
= self
.names
[name
]
699 # No record of this name already, so write it
700 # out as normal, recording the location of the name
701 # for future pointers to it.
703 self
.names
[name
] = self
.size
704 parts
= name
.split('.')
712 # An index was found, so write a pointer to it
714 self
.writeByte((index
>> 8) |
0xC0)
715 self
.writeByte(index
)
717 def writeQuestion(self
, question
):
718 """Writes a question to the packet"""
719 self
.writeName(question
.name
)
720 self
.writeShort(question
.type)
721 self
.writeShort(question
.clazz
)
723 def writeRecord(self
, record
, now
):
724 """Writes a record (answer, authoritative answer, additional) to
726 self
.writeName(record
.name
)
727 self
.writeShort(record
.type)
728 if record
.unique
and self
.multicast
:
729 self
.writeShort(record
.clazz | _CLASS_UNIQUE
)
731 self
.writeShort(record
.clazz
)
733 self
.writeInt(record
.ttl
)
735 self
.writeInt(record
.getRemainingTTL(now
))
736 index
= len(self
.data
)
737 # Adjust size for the short we will write before this record
743 length
= len(''.join(self
.data
[index
:]))
744 self
.insertShort(index
, length
) # Here is the short we adjusted for
747 """Returns a string containing the packet's bytes
749 No further parts should be added to the packet once this
751 if not self
.finished
:
753 for question
in self
.questions
:
754 self
.writeQuestion(question
)
755 for answer
, time
in self
.answers
:
756 self
.writeRecord(answer
, time
)
757 for authority
in self
.authorities
:
758 self
.writeRecord(authority
, 0)
759 for additional
in self
.additionals
:
760 self
.writeRecord(additional
, 0)
762 self
.insertShort(0, len(self
.additionals
))
763 self
.insertShort(0, len(self
.authorities
))
764 self
.insertShort(0, len(self
.answers
))
765 self
.insertShort(0, len(self
.questions
))
766 self
.insertShort(0, self
.flags
)
768 self
.insertShort(0, 0)
770 self
.insertShort(0, self
.id)
771 return ''.join(self
.data
)
774 class DNSCache(object):
775 """A cache of DNS entries"""
780 def add(self
, entry
):
783 list = self
.cache
[entry
.key
]
785 list = self
.cache
[entry
.key
] = []
788 def remove(self
, entry
):
789 """Removes an entry"""
791 list = self
.cache
[entry
.key
]
796 def get(self
, entry
):
797 """Gets an entry by key. Will return None if there is no
800 list = self
.cache
[entry
.key
]
801 return list[list.index(entry
)]
805 def getByDetails(self
, name
, type, clazz
):
806 """Gets an entry by details. Will return None if there is
807 no matching entry."""
808 entry
= DNSEntry(name
, type, clazz
)
809 return self
.get(entry
)
811 def entriesWithName(self
, name
):
812 """Returns a list of entries whose key matches the name."""
814 return self
.cache
[name
]
819 """Returns a list of all entries"""
820 def add(x
, y
): return x
+y
822 return reduce(add
, self
.cache
.values())
827 class Engine(threading
.Thread
):
828 """An engine wraps read access to sockets, allowing objects that
829 need to receive data from sockets to be called back when the
832 A reader needs a handle_read() method, which is called when the socket
833 it is interested in is ready for reading.
835 Writers are not implemented here, because we only send short
839 def __init__(self
, zeroconf
):
840 threading
.Thread
.__init
__(self
)
841 self
.zeroconf
= zeroconf
842 self
.readers
= {} # maps socket to reader
844 self
.condition
= threading
.Condition()
848 while not globals()['_GLOBAL_DONE']:
849 rs
= self
.getReaders()
851 # No sockets to manage, but we wait for the timeout
852 # or addition of a socket
854 self
.condition
.acquire()
855 self
.condition
.wait(self
.timeout
)
856 self
.condition
.release()
859 rr
, wr
, er
= select
.select(rs
, [], [], self
.timeout
)
862 self
.readers
[socket
].handle_read()
864 traceback
.print_exc()
868 def getReaders(self
):
870 self
.condition
.acquire()
871 result
= self
.readers
.keys()
872 self
.condition
.release()
875 def addReader(self
, reader
, socket
):
876 self
.condition
.acquire()
877 self
.readers
[socket
] = reader
878 self
.condition
.notify()
879 self
.condition
.release()
881 def delReader(self
, socket
):
882 self
.condition
.acquire()
883 del(self
.readers
[socket
])
884 self
.condition
.notify()
885 self
.condition
.release()
888 self
.condition
.acquire()
889 self
.condition
.notify()
890 self
.condition
.release()
892 class Listener(object):
893 """A Listener is used by this module to listen on the multicast
894 group to which DNS messages are sent, allowing the implementation
895 to cache information as it arrives.
897 It requires registration with an Engine object in order to have
898 the read() method called when a socket is availble for reading."""
900 def __init__(self
, zeroconf
):
901 self
.zeroconf
= zeroconf
902 self
.zeroconf
.engine
.addReader(self
, self
.zeroconf
.socket
)
904 def handle_read(self
):
905 data
, (addr
, port
) = self
.zeroconf
.socket
.recvfrom(_MAX_MSG_ABSOLUTE
)
907 msg
= DNSIncoming(data
)
909 # Always multicast responses
911 if port
== _MDNS_PORT
:
912 self
.zeroconf
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
913 # If it's not a multicast query, reply via unicast
916 elif port
== _DNS_PORT
:
917 self
.zeroconf
.handleQuery(msg
, addr
, port
)
918 self
.zeroconf
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
920 self
.zeroconf
.handleResponse(msg
)
923 class Reaper(threading
.Thread
):
924 """A Reaper is used by this module to remove cache entries that
927 def __init__(self
, zeroconf
):
928 threading
.Thread
.__init
__(self
)
929 self
.zeroconf
= zeroconf
934 self
.zeroconf
.wait(10 * 1000)
935 if globals()['_GLOBAL_DONE']:
937 now
= currentTimeMillis()
938 for record
in self
.zeroconf
.cache
.entries():
939 if record
.isExpired(now
):
940 self
.zeroconf
.updateRecord(now
, record
)
941 self
.zeroconf
.cache
.remove(record
)
944 class ServiceBrowser(threading
.Thread
):
945 """Used to browse for a service of a specific type.
947 The listener object will have its addService() and
948 removeService() methods called when this browser
949 discovers changes in the services availability."""
951 def __init__(self
, zeroconf
, type, listener
):
952 """Creates a browser for a specific type"""
953 threading
.Thread
.__init
__(self
)
954 self
.zeroconf
= zeroconf
956 self
.listener
= listener
958 self
.nextTime
= currentTimeMillis()
959 self
.delay
= _BROWSER_TIME
964 self
.zeroconf
.addListener(self
, DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
967 def updateRecord(self
, zeroconf
, now
, record
):
968 """Callback invoked by Zeroconf when new information arrives.
970 Updates information required by browser in the Zeroconf cache."""
971 if record
.type == _TYPE_PTR
and record
.name
== self
.type:
972 expired
= record
.isExpired(now
)
974 oldrecord
= self
.services
[record
.alias
.lower()]
976 oldrecord
.resetTTL(record
)
978 del(self
.services
[record
.alias
.lower()])
979 callback
= lambda x
: self
.listener
.removeService(x
, self
.type, record
.alias
)
980 self
.list.append(callback
)
984 self
.services
[record
.alias
.lower()] = record
985 callback
= lambda x
: self
.listener
.addService(x
, self
.type, record
.alias
)
986 self
.list.append(callback
)
988 expires
= record
.getExpirationTime(75)
989 if expires
< self
.nextTime
:
990 self
.nextTime
= expires
994 self
.zeroconf
.notifyAll()
999 now
= currentTimeMillis()
1000 if len(self
.list) == 0 and self
.nextTime
> now
:
1001 self
.zeroconf
.wait(self
.nextTime
- now
)
1002 if globals()['_GLOBAL_DONE'] or self
.done
:
1004 now
= currentTimeMillis()
1006 if self
.nextTime
<= now
:
1007 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
1008 out
.addQuestion(DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
1009 for record
in self
.services
.values():
1010 if not record
.isExpired(now
):
1011 out
.addAnswerAtTime(record
, now
)
1012 self
.zeroconf
.send(out
)
1013 self
.nextTime
= now
+ self
.delay
1014 self
.delay
= min(20 * 1000, self
.delay
* 2)
1016 if len(self
.list) > 0:
1017 event
= self
.list.pop(0)
1019 if event
is not None:
1020 event(self
.zeroconf
)
1023 class ServiceInfo(object):
1024 """Service information"""
1026 def __init__(self
, type, name
, address
=None, port
=None, weight
=0, priority
=0, properties
=None, server
=None):
1027 """Create a service description.
1029 type: fully qualified service type name
1030 name: fully qualified service name
1031 address: IP address as unsigned short, network byte order
1032 port: port that the service runs on
1033 weight: weight of the service
1034 priority: priority of the service
1035 properties: dictionary of properties (or a string holding the bytes for the text field)
1036 server: fully qualified name for service host (defaults to name)"""
1038 if not name
.endswith(type):
1039 raise BadTypeInNameException
1042 self
.address
= address
1044 self
.weight
= weight
1045 self
.priority
= priority
1047 self
.server
= server
1050 self
.setProperties(properties
)
1052 def setProperties(self
, properties
):
1053 """Sets properties and text of this info from a dictionary"""
1054 if isinstance(properties
, dict):
1055 self
.properties
= properties
1058 for key
in properties
:
1059 value
= properties
[key
]
1061 suffix
= ''.encode('utf-8')
1062 elif isinstance(value
, str):
1063 suffix
= value
.encode('utf-8')
1064 elif isinstance(value
, int):
1070 suffix
= ''.encode('utf-8')
1071 list.append('='.join((key
, suffix
)))
1073 result
= ''.join((result
, struct
.pack('!c', chr(len(item
))), item
))
1076 self
.text
= properties
1078 def setText(self
, text
):
1079 """Sets properties and text given a text field"""
1087 length
= ord(text
[index
])
1089 strs
.append(text
[index
:index
+length
])
1093 eindex
= s
.find('=')
1095 # No equals sign at all
1100 value
= s
[eindex
+1:]
1103 elif value
== 'false' or not value
:
1106 # Only update non-existent properties
1107 if key
and result
.get(key
) == None:
1110 self
.properties
= result
1112 traceback
.print_exc()
1113 self
.properties
= None
1121 if self
.type is not None and self
.name
.endswith("." + self
.type):
1122 return self
.name
[:len(self
.name
) - len(self
.type) - 1]
1125 def getAddress(self
):
1126 """Address accessor"""
1133 def getPriority(self
):
1134 """Pirority accessor"""
1135 return self
.priority
1137 def getWeight(self
):
1138 """Weight accessor"""
1141 def getProperties(self
):
1142 """Properties accessor"""
1143 return self
.properties
1149 def getServer(self
):
1150 """Server accessor"""
1153 def updateRecord(self
, zeroconf
, now
, record
):
1154 """Updates service information from a DNS record"""
1155 if record
is not None and not record
.isExpired(now
):
1156 if record
.type == _TYPE_A
:
1157 #if record.name == self.name:
1158 if record
.name
== self
.server
:
1159 self
.address
= record
.address
1160 elif record
.type == _TYPE_SRV
:
1161 if record
.name
== self
.name
:
1162 self
.server
= record
.server
1163 self
.port
= record
.port
1164 self
.weight
= record
.weight
1165 self
.priority
= record
.priority
1166 #self.address = None
1167 self
.updateRecord(zeroconf
, now
, zeroconf
.cache
.getByDetails(self
.server
, _TYPE_A
, _CLASS_IN
))
1168 elif record
.type == _TYPE_TXT
:
1169 if record
.name
== self
.name
:
1170 self
.setText(record
.text
)
1172 def request(self
, zeroconf
, timeout
):
1173 """Returns true if the service could be discovered on the
1174 network, and updates this object with details discovered.
1176 now
= currentTimeMillis()
1177 delay
= _LISTENER_TIME
1179 last
= now
+ timeout
1182 zeroconf
.addListener(self
, DNSQuestion(self
.name
, _TYPE_ANY
, _CLASS_IN
))
1183 while self
.server
is None or self
.address
is None or self
.text
is None:
1187 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
1188 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_SRV
, _CLASS_IN
))
1189 out
.addAnswerAtTime(zeroconf
.cache
.getByDetails(self
.name
, _TYPE_SRV
, _CLASS_IN
), now
)
1190 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_TXT
, _CLASS_IN
))
1191 out
.addAnswerAtTime(zeroconf
.cache
.getByDetails(self
.name
, _TYPE_TXT
, _CLASS_IN
), now
)
1192 if self
.server
is not None:
1193 out
.addQuestion(DNSQuestion(self
.server
, _TYPE_A
, _CLASS_IN
))
1194 out
.addAnswerAtTime(zeroconf
.cache
.getByDetails(self
.server
, _TYPE_A
, _CLASS_IN
), now
)
1199 zeroconf
.wait(min(next
, last
) - now
)
1200 now
= currentTimeMillis()
1203 zeroconf
.removeListener(self
)
1207 def __eq__(self
, other
):
1208 """Tests equality of service name"""
1209 if isinstance(other
, ServiceInfo
):
1210 return other
.name
== self
.name
1213 def __ne__(self
, other
):
1214 """Non-equality test"""
1215 return not self
.__eq
__(other
)
1218 """String representation"""
1219 result
= "service[%s,%s:%s," % (self
.name
, socket
.inet_ntoa(self
.getAddress()), self
.port
)
1220 if self
.text
is None:
1223 if len(self
.text
) < 20:
1226 result
+= self
.text
[:17] + "..."
1231 class Zeroconf(object):
1232 """Implementation of Zeroconf Multicast DNS Service Discovery
1234 Supports registration, unregistration, queries and browsing.
1236 def __init__(self
, bindaddress
=None):
1237 """Creates an instance of the Zeroconf class, establishing
1238 multicast communications, listening and reaping threads."""
1239 globals()['_GLOBAL_DONE'] = 0
1240 if bindaddress
is None:
1241 self
.intf
= socket
.gethostbyname(socket
.gethostname())
1243 self
.intf
= bindaddress
1244 self
.group
= ('', _MDNS_PORT
)
1245 self
.socket
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
1247 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
, 1)
1248 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEPORT
, 1)
1250 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1251 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1252 # Volume 2"), but some BSD-derived systems require
1253 # SO_REUSEPORT to be specified explicity. Also, not all
1254 # versions of Python have SO_REUSEPORT available. So
1255 # if you're on a BSD-based system, and haven't upgraded
1256 # to Python 2.3 yet, you may find this library doesn't
1260 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_TTL
, 255)
1261 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_LOOP
, 1)
1263 self
.socket
.bind(self
.group
)
1265 # Some versions of linux raise an exception even though
1266 # the SO_REUSE* options have been set, so ignore it
1269 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1270 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_ADD_MEMBERSHIP
, socket
.inet_aton(_MDNS_ADDR
) + socket
.inet_aton('0.0.0.0'))
1275 self
.servicetypes
= {}
1277 self
.cache
= DNSCache()
1279 self
.condition
= threading
.Condition()
1281 self
.engine
= Engine(self
)
1282 self
.listener
= Listener(self
)
1283 self
.reaper
= Reaper(self
)
1285 def isLoopback(self
):
1286 return self
.intf
.startswith("127.0.0.1")
1288 def isLinklocal(self
):
1289 return self
.intf
.startswith("169.254.")
1291 def wait(self
, timeout
):
1292 """Calling thread waits for a given number of milliseconds or
1294 self
.condition
.acquire()
1295 self
.condition
.wait(timeout
/1000)
1296 self
.condition
.release()
1298 def notifyAll(self
):
1299 """Notifies all waiting threads"""
1300 self
.condition
.acquire()
1301 self
.condition
.notifyAll()
1302 self
.condition
.release()
1304 def getServiceInfo(self
, type, name
, timeout
=3000):
1305 """Returns network's service information for a particular
1306 name and type, or None if no service matches by the timeout,
1307 which defaults to 3 seconds."""
1308 info
= ServiceInfo(type, name
)
1309 if info
.request(self
, timeout
):
1313 def addServiceListener(self
, type, listener
):
1314 """Adds a listener for a particular service type. This object
1315 will then have its updateRecord method called when information
1316 arrives for that type."""
1317 self
.removeServiceListener(listener
)
1318 self
.browsers
.append(ServiceBrowser(self
, type, listener
))
1320 def removeServiceListener(self
, listener
):
1321 """Removes a listener from the set that is currently listening."""
1322 for browser
in self
.browsers
:
1323 if browser
.listener
== listener
:
1327 def registerService(self
, info
, ttl
=_DNS_TTL
):
1328 """Registers service information to the network with a default TTL
1329 of 60 seconds. Zeroconf will then respond to requests for
1330 information for that service. The name of the service may be
1331 changed if needed to make it unique on the network."""
1332 self
.checkService(info
)
1333 self
.services
[info
.name
.lower()] = info
1334 if info
.type in self
.servicetypes
:
1335 self
.servicetypes
[info
.type]+=1
1337 self
.servicetypes
[info
.type]=1
1338 now
= currentTimeMillis()
1343 self
.wait(nextTime
- now
)
1344 now
= currentTimeMillis()
1346 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1347 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, ttl
, info
.name
), 0)
1348 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
, _CLASS_IN
, ttl
, info
.priority
, info
.weight
, info
.port
, info
.server
), 0)
1349 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
, ttl
, info
.text
), 0)
1351 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, ttl
, info
.address
), 0)
1354 nextTime
+= _REGISTER_TIME
1356 def unregisterService(self
, info
):
1357 """Unregister a service."""
1359 del(self
.services
[info
.name
.lower()])
1360 if self
.servicetypes
[info
.type]>1:
1361 self
.servicetypes
[info
.type]-=1
1363 del self
.servicetypes
[info
.type]
1366 now
= currentTimeMillis()
1371 self
.wait(nextTime
- now
)
1372 now
= currentTimeMillis()
1374 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1375 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, 0, info
.name
), 0)
1376 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
, _CLASS_IN
, 0, info
.priority
, info
.weight
, info
.port
, info
.name
), 0)
1377 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
, 0, info
.text
), 0)
1379 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, 0, info
.address
), 0)
1382 nextTime
+= _UNREGISTER_TIME
1384 def unregisterAllServices(self
):
1385 """Unregister all registered services."""
1386 if len(self
.services
) > 0:
1387 now
= currentTimeMillis()
1392 self
.wait(nextTime
- now
)
1393 now
= currentTimeMillis()
1395 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1396 for info
in self
.services
.values():
1397 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, 0, info
.name
), 0)
1398 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
, _CLASS_IN
, 0, info
.priority
, info
.weight
, info
.port
, info
.server
), 0)
1399 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
, 0, info
.text
), 0)
1401 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, 0, info
.address
), 0)
1404 nextTime
+= _UNREGISTER_TIME
1406 def checkService(self
, info
):
1407 """Checks the network for a unique service name, modifying the
1408 ServiceInfo passed in if it is not unique."""
1409 now
= currentTimeMillis()
1413 for record
in self
.cache
.entriesWithName(info
.type):
1414 if record
.type == _TYPE_PTR
and not record
.isExpired(now
) and record
.alias
== info
.name
:
1415 if (info
.name
.find('.') < 0):
1416 info
.name
= info
.name
+ ".[" + info
.address
+ ":" + info
.port
+ "]." + info
.type
1417 self
.checkService(info
)
1419 raise NonUniqueNameException
1421 self
.wait(nextTime
- now
)
1422 now
= currentTimeMillis()
1424 out
= DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA
)
1426 out
.addQuestion(DNSQuestion(info
.type, _TYPE_PTR
, _CLASS_IN
))
1427 out
.addAuthorativeAnswer(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, info
.name
))
1430 nextTime
+= _CHECK_TIME
1432 def addListener(self
, listener
, question
):
1433 """Adds a listener for a given question. The listener will have
1434 its updateRecord method called when information is available to
1435 answer the question."""
1436 now
= currentTimeMillis()
1437 self
.listeners
.append(listener
)
1438 if question
is not None:
1439 for record
in self
.cache
.entriesWithName(question
.name
):
1440 if question
.answeredBy(record
) and not record
.isExpired(now
):
1441 listener
.updateRecord(self
, now
, record
)
1444 def removeListener(self
, listener
):
1445 """Removes a listener."""
1447 self
.listeners
.remove(listener
)
1452 def updateRecord(self
, now
, rec
):
1453 """Used to notify listeners of new information that has updated
1455 for listener
in self
.listeners
:
1456 listener
.updateRecord(self
, now
, rec
)
1459 def handleResponse(self
, msg
):
1460 """Deal with incoming response packets. All answers
1461 are held in the cache, and listeners are notified."""
1462 now
= currentTimeMillis()
1463 for record
in msg
.answers
:
1464 expired
= record
.isExpired(now
)
1465 if record
in self
.cache
.entries():
1467 self
.cache
.remove(record
)
1469 entry
= self
.cache
.get(record
)
1470 if entry
is not None:
1471 entry
.resetTTL(record
)
1474 self
.cache
.add(record
)
1476 self
.updateRecord(now
, record
)
1478 def handleQuery(self
, msg
, addr
, port
):
1479 """Deal with incoming query packets. Provides a response if
1483 # Support unicast client responses
1485 if port
!= _MDNS_PORT
:
1486 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
, 0)
1487 for question
in msg
.questions
:
1488 out
.addQuestion(question
)
1490 for question
in msg
.questions
:
1491 if question
.type == _TYPE_PTR
:
1492 if question
.name
== "_services._dns-sd._udp.local.":
1493 for stype
in self
.servicetypes
.keys():
1495 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1496 out
.addAnswer(msg
, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, stype
))
1497 for service
in self
.services
.values():
1498 if question
.name
== service
.type:
1500 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1501 out
.addAnswer(msg
, DNSPointer(service
.type, _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, service
.name
))
1505 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1507 # Answer A record queries for any service addresses we know
1508 if question
.type == _TYPE_A
or question
.type == _TYPE_ANY
:
1509 for service
in self
.services
.values():
1510 if service
.server
== question
.name
.lower():
1511 out
.addAnswer(msg
, DNSAddress(question
.name
, _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.address
))
1513 service
= self
.services
.get(question
.name
.lower(), None)
1514 if not service
: continue
1516 if question
.type == _TYPE_SRV
or question
.type == _TYPE_ANY
:
1517 out
.addAnswer(msg
, DNSService(question
.name
, _TYPE_SRV
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.priority
, service
.weight
, service
.port
, service
.server
))
1518 if question
.type == _TYPE_TXT
or question
.type == _TYPE_ANY
:
1519 out
.addAnswer(msg
, DNSText(question
.name
, _TYPE_TXT
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.text
))
1520 if question
.type == _TYPE_SRV
:
1521 out
.addAdditionalAnswer(DNSAddress(service
.server
, _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.address
))
1523 traceback
.print_exc()
1525 if out
is not None and out
.answers
:
1527 self
.send(out
, addr
, port
)
1529 def send(self
, out
, addr
= _MDNS_ADDR
, port
= _MDNS_PORT
):
1530 """Sends an outgoing packet."""
1531 # This is a quick test to see if we can parse the packets we generate
1532 #temp = DNSIncoming(out.packet())
1534 bytes_sent
= self
.socket
.sendto(out
.packet(), 0, (addr
, port
))
1536 # Ignore this, it may be a temporary loss of network connection
1540 """Ends the background threads, and prevent this instance from
1541 servicing further queries."""
1542 if globals()['_GLOBAL_DONE'] == 0:
1543 globals()['_GLOBAL_DONE'] = 1
1545 self
.engine
.notify()
1546 self
.unregisterAllServices()
1547 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_DROP_MEMBERSHIP
, socket
.inet_aton(_MDNS_ADDR
) + socket
.inet_aton('0.0.0.0'))
1550 # Test a few module features, including service registration, service
1551 # query (for Zoe), and service unregistration.
1553 if __name__
== '__main__':
1554 print "Multicast DNS Service Discovery for Python, version", __version__
1556 print "1. Testing registration of a service..."
1557 desc
= {'version':'0.10','a':'test value', 'b':'another value'}
1558 info
= ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket
.inet_aton("127.0.0.1"), 1234, 0, 0, desc
)
1559 print " Registering service..."
1560 r
.registerService(info
)
1561 print " Registration done."
1562 print "2. Testing query of service information..."
1563 print " Getting ZOE service:", str(r
.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1564 print " Query done."
1565 print "3. Testing query of own service..."
1566 print " Getting self:", str(r
.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local."))
1567 print " Query done."
1568 print "4. Testing unregister of service information..."
1569 r
.unregisterService(info
)
1570 print " Unregister done."