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
):
906 data
, (addr
, port
) = self
.zeroconf
.socket
.recvfrom(_MAX_MSG_ABSOLUTE
)
907 except socket
.error
, e
:
908 # If the socket was closed by another thread -- which happens
909 # regularly on shutdown -- an EBADF exception is thrown here.
911 if e
[0] == socket
.EBADF
:
916 msg
= DNSIncoming(data
)
918 # Always multicast responses
920 if port
== _MDNS_PORT
:
921 self
.zeroconf
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
922 # If it's not a multicast query, reply via unicast
925 elif port
== _DNS_PORT
:
926 self
.zeroconf
.handleQuery(msg
, addr
, port
)
927 self
.zeroconf
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
929 self
.zeroconf
.handleResponse(msg
)
932 class Reaper(threading
.Thread
):
933 """A Reaper is used by this module to remove cache entries that
936 def __init__(self
, zeroconf
):
937 threading
.Thread
.__init
__(self
)
938 self
.zeroconf
= zeroconf
943 self
.zeroconf
.wait(10 * 1000)
944 if globals()['_GLOBAL_DONE']:
946 now
= currentTimeMillis()
947 for record
in self
.zeroconf
.cache
.entries():
948 if record
.isExpired(now
):
949 self
.zeroconf
.updateRecord(now
, record
)
950 self
.zeroconf
.cache
.remove(record
)
953 class ServiceBrowser(threading
.Thread
):
954 """Used to browse for a service of a specific type.
956 The listener object will have its addService() and
957 removeService() methods called when this browser
958 discovers changes in the services availability."""
960 def __init__(self
, zeroconf
, type, listener
):
961 """Creates a browser for a specific type"""
962 threading
.Thread
.__init
__(self
)
963 self
.zeroconf
= zeroconf
965 self
.listener
= listener
967 self
.nextTime
= currentTimeMillis()
968 self
.delay
= _BROWSER_TIME
973 self
.zeroconf
.addListener(self
, DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
976 def updateRecord(self
, zeroconf
, now
, record
):
977 """Callback invoked by Zeroconf when new information arrives.
979 Updates information required by browser in the Zeroconf cache."""
980 if record
.type == _TYPE_PTR
and record
.name
== self
.type:
981 expired
= record
.isExpired(now
)
983 oldrecord
= self
.services
[record
.alias
.lower()]
985 oldrecord
.resetTTL(record
)
987 del(self
.services
[record
.alias
.lower()])
988 callback
= lambda x
: self
.listener
.removeService(x
, self
.type, record
.alias
)
989 self
.list.append(callback
)
993 self
.services
[record
.alias
.lower()] = record
994 callback
= lambda x
: self
.listener
.addService(x
, self
.type, record
.alias
)
995 self
.list.append(callback
)
997 expires
= record
.getExpirationTime(75)
998 if expires
< self
.nextTime
:
999 self
.nextTime
= expires
1003 self
.zeroconf
.notifyAll()
1008 now
= currentTimeMillis()
1009 if len(self
.list) == 0 and self
.nextTime
> now
:
1010 self
.zeroconf
.wait(self
.nextTime
- now
)
1011 if globals()['_GLOBAL_DONE'] or self
.done
:
1013 now
= currentTimeMillis()
1015 if self
.nextTime
<= now
:
1016 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
1017 out
.addQuestion(DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
1018 for record
in self
.services
.values():
1019 if not record
.isExpired(now
):
1020 out
.addAnswerAtTime(record
, now
)
1021 self
.zeroconf
.send(out
)
1022 self
.nextTime
= now
+ self
.delay
1023 self
.delay
= min(20 * 1000, self
.delay
* 2)
1025 if len(self
.list) > 0:
1026 event
= self
.list.pop(0)
1028 if event
is not None:
1029 event(self
.zeroconf
)
1032 class ServiceInfo(object):
1033 """Service information"""
1035 def __init__(self
, type, name
, address
=None, port
=None, weight
=0, priority
=0, properties
=None, server
=None):
1036 """Create a service description.
1038 type: fully qualified service type name
1039 name: fully qualified service name
1040 address: IP address as unsigned short, network byte order
1041 port: port that the service runs on
1042 weight: weight of the service
1043 priority: priority of the service
1044 properties: dictionary of properties (or a string holding the bytes for the text field)
1045 server: fully qualified name for service host (defaults to name)"""
1047 if not name
.endswith(type):
1048 raise BadTypeInNameException
1051 self
.address
= address
1053 self
.weight
= weight
1054 self
.priority
= priority
1056 self
.server
= server
1059 self
.setProperties(properties
)
1061 def setProperties(self
, properties
):
1062 """Sets properties and text of this info from a dictionary"""
1063 if isinstance(properties
, dict):
1064 self
.properties
= properties
1067 for key
in properties
:
1068 value
= properties
[key
]
1070 suffix
= ''.encode('utf-8')
1071 elif isinstance(value
, str):
1072 suffix
= value
.encode('utf-8')
1073 elif isinstance(value
, int):
1079 suffix
= ''.encode('utf-8')
1080 list.append('='.join((key
, suffix
)))
1082 result
= ''.join((result
, struct
.pack('!c', chr(len(item
))), item
))
1085 self
.text
= properties
1087 def setText(self
, text
):
1088 """Sets properties and text given a text field"""
1096 length
= ord(text
[index
])
1098 strs
.append(text
[index
:index
+length
])
1102 eindex
= s
.find('=')
1104 # No equals sign at all
1109 value
= s
[eindex
+1:]
1112 elif value
== 'false' or not value
:
1115 # Only update non-existent properties
1116 if key
and result
.get(key
) == None:
1119 self
.properties
= result
1121 traceback
.print_exc()
1122 self
.properties
= None
1130 if self
.type is not None and self
.name
.endswith("." + self
.type):
1131 return self
.name
[:len(self
.name
) - len(self
.type) - 1]
1134 def getAddress(self
):
1135 """Address accessor"""
1142 def getPriority(self
):
1143 """Pirority accessor"""
1144 return self
.priority
1146 def getWeight(self
):
1147 """Weight accessor"""
1150 def getProperties(self
):
1151 """Properties accessor"""
1152 return self
.properties
1158 def getServer(self
):
1159 """Server accessor"""
1162 def updateRecord(self
, zeroconf
, now
, record
):
1163 """Updates service information from a DNS record"""
1164 if record
is not None and not record
.isExpired(now
):
1165 if record
.type == _TYPE_A
:
1166 #if record.name == self.name:
1167 if record
.name
== self
.server
:
1168 self
.address
= record
.address
1169 elif record
.type == _TYPE_SRV
:
1170 if record
.name
== self
.name
:
1171 self
.server
= record
.server
1172 self
.port
= record
.port
1173 self
.weight
= record
.weight
1174 self
.priority
= record
.priority
1175 #self.address = None
1176 self
.updateRecord(zeroconf
, now
, zeroconf
.cache
.getByDetails(self
.server
, _TYPE_A
, _CLASS_IN
))
1177 elif record
.type == _TYPE_TXT
:
1178 if record
.name
== self
.name
:
1179 self
.setText(record
.text
)
1181 def request(self
, zeroconf
, timeout
):
1182 """Returns true if the service could be discovered on the
1183 network, and updates this object with details discovered.
1185 now
= currentTimeMillis()
1186 delay
= _LISTENER_TIME
1188 last
= now
+ timeout
1191 zeroconf
.addListener(self
, DNSQuestion(self
.name
, _TYPE_ANY
, _CLASS_IN
))
1192 while self
.server
is None or self
.address
is None or self
.text
is None:
1196 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
1197 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_SRV
, _CLASS_IN
))
1198 out
.addAnswerAtTime(zeroconf
.cache
.getByDetails(self
.name
, _TYPE_SRV
, _CLASS_IN
), now
)
1199 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_TXT
, _CLASS_IN
))
1200 out
.addAnswerAtTime(zeroconf
.cache
.getByDetails(self
.name
, _TYPE_TXT
, _CLASS_IN
), now
)
1201 if self
.server
is not None:
1202 out
.addQuestion(DNSQuestion(self
.server
, _TYPE_A
, _CLASS_IN
))
1203 out
.addAnswerAtTime(zeroconf
.cache
.getByDetails(self
.server
, _TYPE_A
, _CLASS_IN
), now
)
1208 zeroconf
.wait(min(next
, last
) - now
)
1209 now
= currentTimeMillis()
1212 zeroconf
.removeListener(self
)
1216 def __eq__(self
, other
):
1217 """Tests equality of service name"""
1218 if isinstance(other
, ServiceInfo
):
1219 return other
.name
== self
.name
1222 def __ne__(self
, other
):
1223 """Non-equality test"""
1224 return not self
.__eq
__(other
)
1227 """String representation"""
1228 result
= "service[%s,%s:%s," % (self
.name
, socket
.inet_ntoa(self
.getAddress()), self
.port
)
1229 if self
.text
is None:
1232 if len(self
.text
) < 20:
1235 result
+= self
.text
[:17] + "..."
1240 class Zeroconf(object):
1241 """Implementation of Zeroconf Multicast DNS Service Discovery
1243 Supports registration, unregistration, queries and browsing.
1245 def __init__(self
, bindaddress
=None):
1246 """Creates an instance of the Zeroconf class, establishing
1247 multicast communications, listening and reaping threads."""
1248 globals()['_GLOBAL_DONE'] = 0
1249 if bindaddress
is None:
1250 self
.intf
= socket
.gethostbyname(socket
.gethostname())
1252 self
.intf
= bindaddress
1253 self
.group
= ('', _MDNS_PORT
)
1254 self
.socket
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
1256 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
, 1)
1257 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEPORT
, 1)
1259 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1260 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1261 # Volume 2"), but some BSD-derived systems require
1262 # SO_REUSEPORT to be specified explicity. Also, not all
1263 # versions of Python have SO_REUSEPORT available. So
1264 # if you're on a BSD-based system, and haven't upgraded
1265 # to Python 2.3 yet, you may find this library doesn't
1269 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_TTL
, 255)
1270 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_LOOP
, 1)
1272 self
.socket
.bind(self
.group
)
1274 # Some versions of linux raise an exception even though
1275 # the SO_REUSE* options have been set, so ignore it
1278 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1279 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_ADD_MEMBERSHIP
, socket
.inet_aton(_MDNS_ADDR
) + socket
.inet_aton('0.0.0.0'))
1284 self
.servicetypes
= {}
1286 self
.cache
= DNSCache()
1288 self
.condition
= threading
.Condition()
1290 self
.engine
= Engine(self
)
1291 self
.listener
= Listener(self
)
1292 self
.reaper
= Reaper(self
)
1294 def isLoopback(self
):
1295 return self
.intf
.startswith("127.0.0.1")
1297 def isLinklocal(self
):
1298 return self
.intf
.startswith("169.254.")
1300 def wait(self
, timeout
):
1301 """Calling thread waits for a given number of milliseconds or
1303 self
.condition
.acquire()
1304 self
.condition
.wait(timeout
/1000)
1305 self
.condition
.release()
1307 def notifyAll(self
):
1308 """Notifies all waiting threads"""
1309 self
.condition
.acquire()
1310 self
.condition
.notifyAll()
1311 self
.condition
.release()
1313 def getServiceInfo(self
, type, name
, timeout
=3000):
1314 """Returns network's service information for a particular
1315 name and type, or None if no service matches by the timeout,
1316 which defaults to 3 seconds."""
1317 info
= ServiceInfo(type, name
)
1318 if info
.request(self
, timeout
):
1322 def addServiceListener(self
, type, listener
):
1323 """Adds a listener for a particular service type. This object
1324 will then have its updateRecord method called when information
1325 arrives for that type."""
1326 self
.removeServiceListener(listener
)
1327 self
.browsers
.append(ServiceBrowser(self
, type, listener
))
1329 def removeServiceListener(self
, listener
):
1330 """Removes a listener from the set that is currently listening."""
1331 for browser
in self
.browsers
:
1332 if browser
.listener
== listener
:
1336 def registerService(self
, info
, ttl
=_DNS_TTL
):
1337 """Registers service information to the network with a default TTL
1338 of 60 seconds. Zeroconf will then respond to requests for
1339 information for that service. The name of the service may be
1340 changed if needed to make it unique on the network."""
1341 self
.checkService(info
)
1342 self
.services
[info
.name
.lower()] = info
1343 if info
.type in self
.servicetypes
:
1344 self
.servicetypes
[info
.type]+=1
1346 self
.servicetypes
[info
.type]=1
1347 now
= currentTimeMillis()
1352 self
.wait(nextTime
- now
)
1353 now
= currentTimeMillis()
1355 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1356 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, ttl
, info
.name
), 0)
1357 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
, _CLASS_IN
, ttl
, info
.priority
, info
.weight
, info
.port
, info
.server
), 0)
1358 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
, ttl
, info
.text
), 0)
1360 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, ttl
, info
.address
), 0)
1363 nextTime
+= _REGISTER_TIME
1365 def unregisterService(self
, info
):
1366 """Unregister a service."""
1368 del(self
.services
[info
.name
.lower()])
1369 if self
.servicetypes
[info
.type]>1:
1370 self
.servicetypes
[info
.type]-=1
1372 del self
.servicetypes
[info
.type]
1375 now
= currentTimeMillis()
1380 self
.wait(nextTime
- now
)
1381 now
= currentTimeMillis()
1383 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1384 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, 0, info
.name
), 0)
1385 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
, _CLASS_IN
, 0, info
.priority
, info
.weight
, info
.port
, info
.name
), 0)
1386 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
, 0, info
.text
), 0)
1388 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _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
, _CLASS_IN
, 0, info
.name
), 0)
1407 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
, _CLASS_IN
, 0, info
.priority
, info
.weight
, info
.port
, info
.server
), 0)
1408 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
, 0, info
.text
), 0)
1410 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, 0, info
.address
), 0)
1413 nextTime
+= _UNREGISTER_TIME
1415 def checkService(self
, info
):
1416 """Checks the network for a unique service name, modifying the
1417 ServiceInfo passed in if it is not unique."""
1418 now
= currentTimeMillis()
1422 for record
in self
.cache
.entriesWithName(info
.type):
1423 if record
.type == _TYPE_PTR
and not record
.isExpired(now
) and record
.alias
== info
.name
:
1424 if (info
.name
.find('.') < 0):
1425 info
.name
= info
.name
+ ".[" + info
.address
+ ":" + info
.port
+ "]." + info
.type
1426 self
.checkService(info
)
1428 raise NonUniqueNameException
1430 self
.wait(nextTime
- now
)
1431 now
= currentTimeMillis()
1433 out
= DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA
)
1435 out
.addQuestion(DNSQuestion(info
.type, _TYPE_PTR
, _CLASS_IN
))
1436 out
.addAuthorativeAnswer(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, info
.name
))
1439 nextTime
+= _CHECK_TIME
1441 def addListener(self
, listener
, question
):
1442 """Adds a listener for a given question. The listener will have
1443 its updateRecord method called when information is available to
1444 answer the question."""
1445 now
= currentTimeMillis()
1446 self
.listeners
.append(listener
)
1447 if question
is not None:
1448 for record
in self
.cache
.entriesWithName(question
.name
):
1449 if question
.answeredBy(record
) and not record
.isExpired(now
):
1450 listener
.updateRecord(self
, now
, record
)
1453 def removeListener(self
, listener
):
1454 """Removes a listener."""
1456 self
.listeners
.remove(listener
)
1461 def updateRecord(self
, now
, rec
):
1462 """Used to notify listeners of new information that has updated
1464 for listener
in self
.listeners
:
1465 listener
.updateRecord(self
, now
, rec
)
1468 def handleResponse(self
, msg
):
1469 """Deal with incoming response packets. All answers
1470 are held in the cache, and listeners are notified."""
1471 now
= currentTimeMillis()
1472 for record
in msg
.answers
:
1473 expired
= record
.isExpired(now
)
1474 if record
in self
.cache
.entries():
1476 self
.cache
.remove(record
)
1478 entry
= self
.cache
.get(record
)
1479 if entry
is not None:
1480 entry
.resetTTL(record
)
1483 self
.cache
.add(record
)
1485 self
.updateRecord(now
, record
)
1487 def handleQuery(self
, msg
, addr
, port
):
1488 """Deal with incoming query packets. Provides a response if
1492 # Support unicast client responses
1494 if port
!= _MDNS_PORT
:
1495 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
, 0)
1496 for question
in msg
.questions
:
1497 out
.addQuestion(question
)
1499 for question
in msg
.questions
:
1500 if question
.type == _TYPE_PTR
:
1501 if question
.name
== "_services._dns-sd._udp.local.":
1502 for stype
in self
.servicetypes
.keys():
1504 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1505 out
.addAnswer(msg
, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, stype
))
1506 for service
in self
.services
.values():
1507 if question
.name
== service
.type:
1509 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1510 out
.addAnswer(msg
, DNSPointer(service
.type, _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, service
.name
))
1514 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1516 # Answer A record queries for any service addresses we know
1517 if question
.type == _TYPE_A
or question
.type == _TYPE_ANY
:
1518 for service
in self
.services
.values():
1519 if service
.server
== question
.name
.lower():
1520 out
.addAnswer(msg
, DNSAddress(question
.name
, _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.address
))
1522 service
= self
.services
.get(question
.name
.lower(), None)
1523 if not service
: continue
1525 if question
.type == _TYPE_SRV
or question
.type == _TYPE_ANY
:
1526 out
.addAnswer(msg
, DNSService(question
.name
, _TYPE_SRV
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.priority
, service
.weight
, service
.port
, service
.server
))
1527 if question
.type == _TYPE_TXT
or question
.type == _TYPE_ANY
:
1528 out
.addAnswer(msg
, DNSText(question
.name
, _TYPE_TXT
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.text
))
1529 if question
.type == _TYPE_SRV
:
1530 out
.addAdditionalAnswer(DNSAddress(service
.server
, _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.address
))
1532 traceback
.print_exc()
1534 if out
is not None and out
.answers
:
1536 self
.send(out
, addr
, port
)
1538 def send(self
, out
, addr
= _MDNS_ADDR
, port
= _MDNS_PORT
):
1539 """Sends an outgoing packet."""
1540 # This is a quick test to see if we can parse the packets we generate
1541 #temp = DNSIncoming(out.packet())
1543 bytes_sent
= self
.socket
.sendto(out
.packet(), 0, (addr
, port
))
1545 # Ignore this, it may be a temporary loss of network connection
1549 """Ends the background threads, and prevent this instance from
1550 servicing further queries."""
1551 if globals()['_GLOBAL_DONE'] == 0:
1552 globals()['_GLOBAL_DONE'] = 1
1554 self
.engine
.notify()
1555 self
.unregisterAllServices()
1556 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_DROP_MEMBERSHIP
, socket
.inet_aton(_MDNS_ADDR
) + socket
.inet_aton('0.0.0.0'))
1559 # Test a few module features, including service registration, service
1560 # query (for Zoe), and service unregistration.
1562 if __name__
== '__main__':
1563 print "Multicast DNS Service Discovery for Python, version", __version__
1565 print "1. Testing registration of a service..."
1566 desc
= {'version':'0.10','a':'test value', 'b':'another value'}
1567 info
= ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket
.inet_aton("127.0.0.1"), 1234, 0, 0, desc
)
1568 print " Registering service..."
1569 r
.registerService(info
)
1570 print " Registration done."
1571 print "2. Testing query of service information..."
1572 print " Getting ZOE service:", str(r
.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1573 print " Query done."
1574 print "3. Testing query of own service..."
1575 print " Getting self:", str(r
.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local."))
1576 print " Query done."
1577 print "4. Testing unregister of service information..."
1578 r
.unregisterService(info
)
1579 print " Unregister done."