1 """ Multicast DNS Service Discovery for Python, v0.12-wmcbrine
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-wmcbrine update - see git for changes"""
27 """0.12 update - allow selection of binding interface
28 typo fix - Thanks A. M. Kuchlingi
29 removed all use of word 'Rendezvous' - this is an API change"""
31 """0.11 update - correction to comments for addListener method
32 support for new record types seen from OS X
35 ignore unknown DNS record types
36 fixes to name decoding
37 works alongside other processes using port 5353
39 tested against Mac OS X 10.3.2's mDNSResponder
40 corrections to removal of list entries for service browser"""
42 """0.10 update - Jonathon Paisley contributed these corrections:
43 always multicast replies, even when query is unicast
44 correct a pointer encoding problem
45 can now write records in any order
46 traceback shown on failure
47 better TXT record parsing
48 server is now separate from name
49 can cancel a service browser
51 modified some unit tests to accommodate these changes"""
53 """0.09 update - remove all records on service unregistration
54 fix DOS security problem with readName"""
56 """0.08 update - changed licensing to LGPL"""
58 """0.07 update - faster shutdown on engine
59 pointer encoding of outgoing names
60 ServiceBrowser now works
63 """0.06 update - small improvements with unit tests
64 added defined exception types
66 fixed hostname/interface problem
67 fixed socket timeout problem
68 fixed addServiceListener() typo bug
69 using select() for socket reads
70 tested on Debian unstable with Python 2.2.2"""
72 """0.05 update - ensure case insensitivty on domain names
73 support for unicast DNS queries"""
75 """0.04 update - added some unit tests
76 added __ne__ adjuncts where required
77 ensure names end in '.local.'
78 timeout on receiving socket for clean shutdown"""
80 __author__
= "Paul Scott-Murphy"
81 __email__
= "paul at scott dash murphy dot com"
82 __version__
= "0.12-wmcbrine"
91 __all__
= ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
97 # Some timing constants
99 _UNREGISTER_TIME
= 125
107 _MDNS_ADDR
= '224.0.0.251'
110 _DNS_TTL
= 60 * 60; # one hour default TTL
112 _MAX_MSG_TYPICAL
= 1460 # unused
113 _MAX_MSG_ABSOLUTE
= 8972
115 _FLAGS_QR_MASK
= 0x8000 # query response mask
116 _FLAGS_QR_QUERY
= 0x0000 # query
117 _FLAGS_QR_RESPONSE
= 0x8000 # response
119 _FLAGS_AA
= 0x0400 # Authorative answer
120 _FLAGS_TC
= 0x0200 # Truncated
121 _FLAGS_RD
= 0x0100 # Recursion desired
122 _FLAGS_RA
= 0x8000 # Recursion available
124 _FLAGS_Z
= 0x0040 # Zero
125 _FLAGS_AD
= 0x0020 # Authentic data
126 _FLAGS_CD
= 0x0010 # Checking disabled
135 _CLASS_UNIQUE
= 0x8000
157 # Mapping constants to names
159 _CLASSES
= { _CLASS_IN
: "in",
163 _CLASS_NONE
: "none",
166 _TYPES
= { _TYPE_A
: "a",
170 _TYPE_CNAME
: "cname",
178 _TYPE_HINFO
: "hinfo",
179 _TYPE_MINFO
: "minfo",
182 _TYPE_AAAA
: "quada",
188 def currentTimeMillis():
189 """Current system time in milliseconds"""
190 return time
.time() * 1000
194 class NonLocalNameException(Exception):
197 class NonUniqueNameException(Exception):
200 class NamePartTooLongException(Exception):
203 class AbstractMethodException(Exception):
206 class BadTypeInNameException(Exception):
209 # implementation classes
211 class DNSEntry(object):
214 def __init__(self
, name
, type, clazz
):
215 self
.key
= name
.lower()
218 self
.clazz
= clazz
& _CLASS_MASK
219 self
.unique
= (clazz
& _CLASS_UNIQUE
) != 0
221 def __eq__(self
, other
):
222 """Equality test on name, type, and class"""
223 return (isinstance(other
, DNSEntry
) and
224 self
.name
== other
.name
and
225 self
.type == other
.type and
226 self
.clazz
== other
.clazz
)
228 def __ne__(self
, other
):
229 """Non-equality test"""
230 return not self
.__eq
__(other
)
232 def getClazz(self
, clazz
):
235 return _CLASSES
[clazz
]
237 return "?(%s)" % (clazz
)
239 def getType(self
, type):
244 return "?(%s)" % (type)
246 def toString(self
, hdr
, other
):
247 """String representation with additional information"""
248 result
= "%s[%s,%s" % (hdr
, self
.getType(self
.type),
249 self
.getClazz(self
.clazz
))
255 if other
is not None:
256 result
+= ",%s]" % (other
)
261 class DNSQuestion(DNSEntry
):
262 """A DNS question entry"""
264 def __init__(self
, name
, type, clazz
):
265 #if not name.endswith(".local."):
266 # raise NonLocalNameException
267 DNSEntry
.__init
__(self
, name
, type, clazz
)
269 def answeredBy(self
, rec
):
270 """Returns true if the question is answered by the record"""
271 return (self
.clazz
== rec
.clazz
and
272 (self
.type == rec
.type or self
.type == _TYPE_ANY
) and
273 self
.name
== rec
.name
)
276 """String representation"""
277 return DNSEntry
.toString(self
, "question", None)
280 class DNSRecord(DNSEntry
):
281 """A DNS record - like a DNS entry, but has a TTL"""
283 def __init__(self
, name
, type, clazz
, ttl
):
284 DNSEntry
.__init
__(self
, name
, type, clazz
)
286 self
.created
= currentTimeMillis()
288 def __eq__(self
, other
):
289 """Tests equality as per DNSRecord"""
290 return isinstance(other
, DNSRecord
) and DNSEntry
.__eq
__(self
, other
)
292 def suppressedBy(self
, msg
):
293 """Returns true if any answer in a message can suffice for the
294 information held in this record."""
295 for record
in msg
.answers
:
296 if self
.suppressedByAnswer(record
):
300 def suppressedByAnswer(self
, other
):
301 """Returns true if another record has same name, type and class,
302 and if its TTL is at least half of this record's."""
303 return self
== other
and other
.ttl
> (self
.ttl
/ 2)
305 def getExpirationTime(self
, percent
):
306 """Returns the time at which this record will have expired
307 by a certain percentage."""
308 return self
.created
+ (percent
* self
.ttl
* 10)
310 def getRemainingTTL(self
, now
):
311 """Returns the remaining TTL in seconds."""
312 return max(0, (self
.getExpirationTime(100) - now
) / 1000)
314 def isExpired(self
, now
):
315 """Returns true if this record has expired."""
316 return self
.getExpirationTime(100) <= now
318 def isStale(self
, now
):
319 """Returns true if this record is at least half way expired."""
320 return self
.getExpirationTime(50) <= now
322 def resetTTL(self
, other
):
323 """Sets this record's TTL and created time to that of
325 self
.created
= other
.created
328 def write(self
, out
):
329 """Abstract method"""
330 raise AbstractMethodException
332 def toString(self
, other
):
333 """String representation with addtional information"""
334 arg
= "%s/%s,%s" % (self
.ttl
,
335 self
.getRemainingTTL(currentTimeMillis()), other
)
336 return DNSEntry
.toString(self
, "record", arg
)
338 class DNSAddress(DNSRecord
):
339 """A DNS address record"""
341 def __init__(self
, name
, type, clazz
, ttl
, address
):
342 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
343 self
.address
= address
345 def write(self
, out
):
346 """Used in constructing an outgoing packet"""
347 out
.writeString(self
.address
)
349 def __eq__(self
, other
):
350 """Tests equality on address"""
351 return isinstance(other
, DNSAddress
) and self
.address
== other
.address
354 """String representation"""
356 return socket
.inet_ntoa(self
.address
)
360 class DNSHinfo(DNSRecord
):
361 """A DNS host information record"""
363 def __init__(self
, name
, type, clazz
, ttl
, cpu
, os
):
364 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
368 def write(self
, out
):
369 """Used in constructing an outgoing packet"""
370 out
.writeString(self
.cpu
)
371 out
.writeString(self
.oso
)
373 def __eq__(self
, other
):
374 """Tests equality on cpu and os"""
375 return (isinstance(other
, DNSHinfo
) and
376 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 return isinstance(other
, DNSPointer
) and self
.alias
== other
.alias
398 """String representation"""
399 return self
.toString(self
.alias
)
401 class DNSText(DNSRecord
):
402 """A DNS text record"""
404 def __init__(self
, name
, type, clazz
, ttl
, text
):
405 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
408 def write(self
, out
):
409 """Used in constructing an outgoing packet"""
410 out
.writeString(self
.text
)
412 def __eq__(self
, other
):
413 """Tests equality on text"""
414 return isinstance(other
, DNSText
) and self
.text
== other
.text
417 """String representation"""
418 if len(self
.text
) > 10:
419 return self
.toString(self
.text
[:7] + "...")
421 return self
.toString(self
.text
)
423 class DNSService(DNSRecord
):
424 """A DNS service record"""
426 def __init__(self
, name
, type, clazz
, ttl
, priority
, weight
, port
, server
):
427 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
428 self
.priority
= priority
433 def write(self
, out
):
434 """Used in constructing an outgoing packet"""
435 out
.writeShort(self
.priority
)
436 out
.writeShort(self
.weight
)
437 out
.writeShort(self
.port
)
438 out
.writeName(self
.server
)
440 def __eq__(self
, other
):
441 """Tests equality on priority, weight, port and server"""
442 return (isinstance(other
, DNSService
) and
443 self
.priority
== other
.priority
and
444 self
.weight
== other
.weight
and
445 self
.port
== other
.port
and
446 self
.server
== other
.server
)
449 """String representation"""
450 return self
.toString("%s:%s" % (self
.server
, self
.port
))
452 class DNSIncoming(object):
453 """Object representation of an incoming DNS packet"""
455 def __init__(self
, data
):
456 """Constructor from string holding bytes of packet"""
461 self
.numQuestions
= 0
463 self
.numAuthorities
= 0
464 self
.numAdditionals
= 0
470 def unpack(self
, format
):
471 length
= struct
.calcsize(format
)
472 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
473 self
.offset
+= length
476 def readHeader(self
):
477 """Reads header portion of packet"""
478 (self
.id, self
.flags
, self
.numQuestions
, self
.numAnswers
,
479 self
.numAuthorities
, self
.numAdditionals
) = self
.unpack('!6H')
481 def readQuestions(self
):
482 """Reads questions section of packet"""
483 for i
in xrange(self
.numQuestions
):
484 name
= self
.readName()
485 type, clazz
= self
.unpack('!HH')
487 question
= DNSQuestion(name
, type, clazz
)
488 self
.questions
.append(question
)
491 """Reads an integer from the packet"""
492 return self
.unpack('!I')[0]
494 def readCharacterString(self
):
495 """Reads a character string from the packet"""
496 length
= ord(self
.data
[self
.offset
])
498 return self
.readString(length
)
500 def readString(self
, length
):
501 """Reads a string of a given length from the packet"""
502 info
= self
.data
[self
.offset
:self
.offset
+length
]
503 self
.offset
+= length
506 def readUnsignedShort(self
):
507 """Reads an unsigned short from the packet"""
508 return self
.unpack('!H')[0]
510 def readOthers(self
):
511 """Reads the answers, authorities and additionals section of the
513 n
= self
.numAnswers
+ self
.numAuthorities
+ self
.numAdditionals
515 domain
= self
.readName()
516 type, clazz
, ttl
, length
= self
.unpack('!HHiH')
520 rec
= DNSAddress(domain
, type, clazz
, ttl
, self
.readString(4))
521 elif type == _TYPE_CNAME
or type == _TYPE_PTR
:
522 rec
= DNSPointer(domain
, type, clazz
, ttl
, self
.readName())
523 elif type == _TYPE_TXT
:
524 rec
= DNSText(domain
, type, clazz
, ttl
, self
.readString(length
))
525 elif type == _TYPE_SRV
:
526 rec
= DNSService(domain
, type, clazz
, ttl
,
527 self
.readUnsignedShort(), self
.readUnsignedShort(),
528 self
.readUnsignedShort(), self
.readName())
529 elif type == _TYPE_HINFO
:
530 rec
= DNSHinfo(domain
, type, clazz
, ttl
,
531 self
.readCharacterString(), self
.readCharacterString())
532 elif type == _TYPE_AAAA
:
533 rec
= DNSAddress(domain
, type, clazz
, ttl
, self
.readString(16))
535 # Try to ignore types we don't know about
536 # Skip the payload for the resource record so the next
537 # records can be parsed correctly
538 self
.offset
+= length
541 self
.answers
.append(rec
)
544 """Returns true if this is a query"""
545 return (self
.flags
& _FLAGS_QR_MASK
) == _FLAGS_QR_QUERY
547 def isResponse(self
):
548 """Returns true if this is a response"""
549 return (self
.flags
& _FLAGS_QR_MASK
) == _FLAGS_QR_RESPONSE
551 def readUTF(self
, offset
, len):
552 """Reads a UTF-8 string of a given length from the packet"""
553 return unicode(self
.data
[offset
:offset
+len], 'utf-8', 'replace')
556 """Reads a domain name from the packet"""
563 len = ord(self
.data
[off
])
569 result
= ''.join((result
, self
.readUTF(off
, len) + '.'))
574 off
= ((len & 0x3F) << 8) |
ord(self
.data
[off
])
576 raise "Bad domain name (circular) at " + str(off
)
579 raise "Bad domain name at " + str(off
)
589 class DNSOutgoing(object):
590 """Object representation of an outgoing packet"""
592 def __init__(self
, flags
, multicast
=True):
593 self
.finished
= False
595 self
.multicast
= multicast
603 self
.authorities
= []
604 self
.additionals
= []
606 def addQuestion(self
, record
):
607 """Adds a question"""
608 self
.questions
.append(record
)
610 def addAnswer(self
, inp
, record
):
612 if not record
.suppressedBy(inp
):
613 self
.addAnswerAtTime(record
, 0)
615 def addAnswerAtTime(self
, record
, now
):
616 """Adds an answer if if does not expire by a certain time"""
617 if record
is not None:
618 if now
== 0 or not record
.isExpired(now
):
619 self
.answers
.append((record
, now
))
621 def addAuthorativeAnswer(self
, record
):
622 """Adds an authoritative answer"""
623 self
.authorities
.append(record
)
625 def addAdditionalAnswer(self
, record
):
626 """Adds an additional answer"""
627 self
.additionals
.append(record
)
629 def pack(self
, format
, value
):
630 self
.data
.append(struct
.pack(format
, value
))
631 self
.size
+= struct
.calcsize(format
)
633 def writeByte(self
, value
):
634 """Writes a single byte to the packet"""
635 self
.pack('!c', chr(value
))
637 def insertShort(self
, index
, value
):
638 """Inserts an unsigned short in a certain position in the packet"""
639 self
.data
.insert(index
, struct
.pack('!H', value
))
642 def writeShort(self
, value
):
643 """Writes an unsigned short to the packet"""
644 self
.pack('!H', value
)
646 def writeInt(self
, value
):
647 """Writes an unsigned integer to the packet"""
648 self
.pack('!I', int(value
))
650 def writeString(self
, value
):
651 """Writes a string to the packet"""
652 self
.data
.append(value
)
653 self
.size
+= len(value
)
655 def writeUTF(self
, s
):
656 """Writes a UTF-8 string of a given length to the packet"""
657 utfstr
= s
.encode('utf-8')
660 raise NamePartTooLongException
661 self
.writeByte(length
)
662 self
.writeString(utfstr
)
664 def writeName(self
, name
):
665 """Writes a domain name to the packet"""
668 # Find existing instance of this name in packet
670 index
= self
.names
[name
]
672 # No record of this name already, so write it
673 # out as normal, recording the location of the name
674 # for future pointers to it.
676 self
.names
[name
] = self
.size
677 parts
= name
.split('.')
685 # An index was found, so write a pointer to it
687 self
.writeByte((index
>> 8) |
0xC0)
688 self
.writeByte(index
)
690 def writeQuestion(self
, question
):
691 """Writes a question to the packet"""
692 self
.writeName(question
.name
)
693 self
.writeShort(question
.type)
694 self
.writeShort(question
.clazz
)
696 def writeRecord(self
, record
, now
):
697 """Writes a record (answer, authoritative answer, additional) to
699 self
.writeName(record
.name
)
700 self
.writeShort(record
.type)
701 if record
.unique
and self
.multicast
:
702 self
.writeShort(record
.clazz | _CLASS_UNIQUE
)
704 self
.writeShort(record
.clazz
)
706 self
.writeInt(record
.ttl
)
708 self
.writeInt(record
.getRemainingTTL(now
))
709 index
= len(self
.data
)
710 # Adjust size for the short we will write before this record
716 length
= len(''.join(self
.data
[index
:]))
717 self
.insertShort(index
, length
) # Here is the short we adjusted for
720 """Returns a string containing the packet's bytes
722 No further parts should be added to the packet once this
724 if not self
.finished
:
726 for question
in self
.questions
:
727 self
.writeQuestion(question
)
728 for answer
, time
in self
.answers
:
729 self
.writeRecord(answer
, time
)
730 for authority
in self
.authorities
:
731 self
.writeRecord(authority
, 0)
732 for additional
in self
.additionals
:
733 self
.writeRecord(additional
, 0)
735 self
.insertShort(0, len(self
.additionals
))
736 self
.insertShort(0, len(self
.authorities
))
737 self
.insertShort(0, len(self
.answers
))
738 self
.insertShort(0, len(self
.questions
))
739 self
.insertShort(0, self
.flags
)
741 self
.insertShort(0, 0)
743 self
.insertShort(0, self
.id)
744 return ''.join(self
.data
)
747 class DNSCache(object):
748 """A cache of DNS entries"""
753 def add(self
, entry
):
756 list = self
.cache
[entry
.key
]
758 list = self
.cache
[entry
.key
] = []
761 def remove(self
, entry
):
762 """Removes an entry"""
764 list = self
.cache
[entry
.key
]
769 def get(self
, entry
):
770 """Gets an entry by key. Will return None if there is no
773 list = self
.cache
[entry
.key
]
774 return list[list.index(entry
)]
778 def getByDetails(self
, name
, type, clazz
):
779 """Gets an entry by details. Will return None if there is
780 no matching entry."""
781 entry
= DNSEntry(name
, type, clazz
)
782 return self
.get(entry
)
784 def entriesWithName(self
, name
):
785 """Returns a list of entries whose key matches the name."""
787 return self
.cache
[name
]
792 """Returns a list of all entries"""
793 def add(x
, y
): return x
+y
795 return reduce(add
, self
.cache
.values())
800 class Engine(threading
.Thread
):
801 """An engine wraps read access to sockets, allowing objects that
802 need to receive data from sockets to be called back when the
805 A reader needs a handle_read() method, which is called when the socket
806 it is interested in is ready for reading.
808 Writers are not implemented here, because we only send short
812 def __init__(self
, zc
):
813 threading
.Thread
.__init
__(self
)
815 self
.readers
= {} # maps socket to reader
817 self
.condition
= threading
.Condition()
821 while not _GLOBAL_DONE
:
822 rs
= self
.getReaders()
824 # No sockets to manage, but we wait for the timeout
825 # or addition of a socket
827 self
.condition
.acquire()
828 self
.condition
.wait(self
.timeout
)
829 self
.condition
.release()
832 rr
, wr
, er
= select
.select(rs
, [], [], self
.timeout
)
835 self
.readers
[socket
].handle_read()
837 traceback
.print_exc()
841 def getReaders(self
):
843 self
.condition
.acquire()
844 result
= self
.readers
.keys()
845 self
.condition
.release()
848 def addReader(self
, reader
, socket
):
849 self
.condition
.acquire()
850 self
.readers
[socket
] = reader
851 self
.condition
.notify()
852 self
.condition
.release()
854 def delReader(self
, socket
):
855 self
.condition
.acquire()
856 del(self
.readers
[socket
])
857 self
.condition
.notify()
858 self
.condition
.release()
861 self
.condition
.acquire()
862 self
.condition
.notify()
863 self
.condition
.release()
865 class Listener(object):
866 """A Listener is used by this module to listen on the multicast
867 group to which DNS messages are sent, allowing the implementation
868 to cache information as it arrives.
870 It requires registration with an Engine object in order to have
871 the read() method called when a socket is availble for reading."""
873 def __init__(self
, zc
):
875 self
.zc
.engine
.addReader(self
, self
.zc
.socket
)
877 def handle_read(self
):
879 data
, (addr
, port
) = self
.zc
.socket
.recvfrom(_MAX_MSG_ABSOLUTE
)
880 except socket
.error
, e
:
881 # If the socket was closed by another thread -- which happens
882 # regularly on shutdown -- an EBADF exception is thrown here.
884 if e
[0] == socket
.EBADF
:
889 msg
= DNSIncoming(data
)
891 # Always multicast responses
893 if port
== _MDNS_PORT
:
894 self
.zc
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
895 # If it's not a multicast query, reply via unicast
898 elif port
== _DNS_PORT
:
899 self
.zc
.handleQuery(msg
, addr
, port
)
900 self
.zc
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
902 self
.zc
.handleResponse(msg
)
905 class Reaper(threading
.Thread
):
906 """A Reaper is used by this module to remove cache entries that
909 def __init__(self
, zc
):
910 threading
.Thread
.__init
__(self
)
916 self
.zc
.wait(10 * 1000)
919 now
= currentTimeMillis()
920 for record
in self
.zc
.cache
.entries():
921 if record
.isExpired(now
):
922 self
.zc
.updateRecord(now
, record
)
923 self
.zc
.cache
.remove(record
)
926 class ServiceBrowser(threading
.Thread
):
927 """Used to browse for a service of a specific type.
929 The listener object will have its addService() and
930 removeService() methods called when this browser
931 discovers changes in the services availability."""
933 def __init__(self
, zc
, type, listener
):
934 """Creates a browser for a specific type"""
935 threading
.Thread
.__init
__(self
)
938 self
.listener
= listener
940 self
.nextTime
= currentTimeMillis()
941 self
.delay
= _BROWSER_TIME
946 self
.zc
.addListener(self
, DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
949 def updateRecord(self
, zc
, now
, record
):
950 """Callback invoked by Zeroconf when new information arrives.
952 Updates information required by browser in the Zeroconf cache."""
953 if record
.type == _TYPE_PTR
and record
.name
== self
.type:
954 expired
= record
.isExpired(now
)
956 oldrecord
= self
.services
[record
.alias
.lower()]
958 oldrecord
.resetTTL(record
)
960 del(self
.services
[record
.alias
.lower()])
961 callback
= lambda x
: self
.listener
.removeService(x
,
962 self
.type, record
.alias
)
963 self
.list.append(callback
)
967 self
.services
[record
.alias
.lower()] = record
968 callback
= lambda x
: self
.listener
.addService(x
,
969 self
.type, record
.alias
)
970 self
.list.append(callback
)
972 expires
= record
.getExpirationTime(75)
973 if expires
< self
.nextTime
:
974 self
.nextTime
= expires
983 now
= currentTimeMillis()
984 if len(self
.list) == 0 and self
.nextTime
> now
:
985 self
.zc
.wait(self
.nextTime
- now
)
986 if _GLOBAL_DONE
or self
.done
:
988 now
= currentTimeMillis()
990 if self
.nextTime
<= now
:
991 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
992 out
.addQuestion(DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
993 for record
in self
.services
.values():
994 if not record
.isExpired(now
):
995 out
.addAnswerAtTime(record
, now
)
997 self
.nextTime
= now
+ self
.delay
998 self
.delay
= min(20 * 1000, self
.delay
* 2)
1000 if len(self
.list) > 0:
1001 event
= self
.list.pop(0)
1003 if event
is not None:
1007 class ServiceInfo(object):
1008 """Service information"""
1010 def __init__(self
, type, name
, address
=None, port
=None, weight
=0,
1011 priority
=0, properties
=None, server
=None):
1012 """Create a service description.
1014 type: fully qualified service type name
1015 name: fully qualified service name
1016 address: IP address as unsigned short, network byte order
1017 port: port that the service runs on
1018 weight: weight of the service
1019 priority: priority of the service
1020 properties: dictionary of properties (or a string holding the
1021 bytes for the text field)
1022 server: fully qualified name for service host (defaults to name)"""
1024 if not name
.endswith(type):
1025 raise BadTypeInNameException
1028 self
.address
= address
1030 self
.weight
= weight
1031 self
.priority
= priority
1033 self
.server
= server
1036 self
.setProperties(properties
)
1038 def setProperties(self
, properties
):
1039 """Sets properties and text of this info from a dictionary"""
1040 if isinstance(properties
, dict):
1041 self
.properties
= properties
1044 for key
in properties
:
1045 value
= properties
[key
]
1047 suffix
= ''.encode('utf-8')
1048 elif isinstance(value
, str):
1049 suffix
= value
.encode('utf-8')
1050 elif isinstance(value
, int):
1056 suffix
= ''.encode('utf-8')
1057 list.append('='.join((key
, suffix
)))
1059 result
= ''.join((result
, chr(len(item
)), item
))
1062 self
.text
= properties
1064 def setText(self
, text
):
1065 """Sets properties and text given a text field"""
1073 length
= ord(text
[index
])
1075 strs
.append(text
[index
:index
+length
])
1080 key
, value
= s
.split('=', 1)
1083 elif value
== 'false' or not value
:
1086 # No equals sign at all
1090 # Only update non-existent properties
1091 if key
and result
.get(key
) == None:
1094 self
.properties
= result
1096 traceback
.print_exc()
1097 self
.properties
= None
1105 if self
.type is not None and self
.name
.endswith("." + self
.type):
1106 return self
.name
[:len(self
.name
) - len(self
.type) - 1]
1109 def getAddress(self
):
1110 """Address accessor"""
1117 def getPriority(self
):
1118 """Pirority accessor"""
1119 return self
.priority
1121 def getWeight(self
):
1122 """Weight accessor"""
1125 def getProperties(self
):
1126 """Properties accessor"""
1127 return self
.properties
1133 def getServer(self
):
1134 """Server accessor"""
1137 def updateRecord(self
, zc
, now
, record
):
1138 """Updates service information from a DNS record"""
1139 if record
is not None and not record
.isExpired(now
):
1140 if record
.type == _TYPE_A
:
1141 #if record.name == self.name:
1142 if record
.name
== self
.server
:
1143 self
.address
= record
.address
1144 elif record
.type == _TYPE_SRV
:
1145 if record
.name
== self
.name
:
1146 self
.server
= record
.server
1147 self
.port
= record
.port
1148 self
.weight
= record
.weight
1149 self
.priority
= record
.priority
1150 #self.address = None
1151 self
.updateRecord(zc
, now
,
1152 zc
.cache
.getByDetails(self
.server
, _TYPE_A
, _CLASS_IN
))
1153 elif record
.type == _TYPE_TXT
:
1154 if record
.name
== self
.name
:
1155 self
.setText(record
.text
)
1157 def request(self
, zc
, timeout
):
1158 """Returns true if the service could be discovered on the
1159 network, and updates this object with details discovered.
1161 now
= currentTimeMillis()
1162 delay
= _LISTENER_TIME
1164 last
= now
+ timeout
1167 zc
.addListener(self
, DNSQuestion(self
.name
, _TYPE_ANY
, _CLASS_IN
))
1168 while (self
.server
is None or self
.address
is None or
1173 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
1174 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_SRV
,
1176 out
.addAnswerAtTime(zc
.cache
.getByDetails(self
.name
,
1177 _TYPE_SRV
, _CLASS_IN
), now
)
1178 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_TXT
,
1180 out
.addAnswerAtTime(zc
.cache
.getByDetails(self
.name
,
1181 _TYPE_TXT
, _CLASS_IN
), now
)
1182 if self
.server
is not None:
1183 out
.addQuestion(DNSQuestion(self
.server
,
1184 _TYPE_A
, _CLASS_IN
))
1185 out
.addAnswerAtTime(zc
.cache
.getByDetails(self
.server
,
1186 _TYPE_A
, _CLASS_IN
), now
)
1191 zc
.wait(min(next
, last
) - now
)
1192 now
= currentTimeMillis()
1195 zc
.removeListener(self
)
1199 def __eq__(self
, other
):
1200 """Tests equality of service name"""
1201 if isinstance(other
, ServiceInfo
):
1202 return other
.name
== self
.name
1205 def __ne__(self
, other
):
1206 """Non-equality test"""
1207 return not self
.__eq
__(other
)
1210 """String representation"""
1211 result
= "service[%s,%s:%s," % (self
.name
,
1212 socket
.inet_ntoa(self
.getAddress()), self
.port
)
1213 if self
.text
is None:
1216 if len(self
.text
) < 20:
1219 result
+= self
.text
[:17] + "..."
1224 class Zeroconf(object):
1225 """Implementation of Zeroconf Multicast DNS Service Discovery
1227 Supports registration, unregistration, queries and browsing.
1229 def __init__(self
, bindaddress
=None):
1230 """Creates an instance of the Zeroconf class, establishing
1231 multicast communications, listening and reaping threads."""
1233 _GLOBAL_DONE
= False
1234 if bindaddress
is None:
1236 s
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
1237 s
.connect(('4.2.2.1', 123))
1238 self
.intf
= s
.getsockname()[0]
1240 self
.intf
= socket
.gethostbyname(socket
.gethostname())
1242 self
.intf
= bindaddress
1243 self
.group
= ('', _MDNS_PORT
)
1244 self
.socket
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
1246 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
, 1)
1247 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEPORT
, 1)
1249 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1250 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1251 # Volume 2"), but some BSD-derived systems require
1252 # SO_REUSEPORT to be specified explicity. Also, not all
1253 # versions of Python have SO_REUSEPORT available. So
1254 # if you're on a BSD-based system, and haven't upgraded
1255 # to Python 2.3 yet, you may find this library doesn't
1259 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_TTL
, 255)
1260 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_LOOP
, 1)
1262 self
.socket
.bind(self
.group
)
1264 # Some versions of linux raise an exception even though
1265 # the SO_REUSE* options have been set, so ignore it
1268 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF,
1269 # socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1270 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_ADD_MEMBERSHIP
,
1271 socket
.inet_aton(_MDNS_ADDR
) + socket
.inet_aton('0.0.0.0'))
1276 self
.servicetypes
= {}
1278 self
.cache
= DNSCache()
1280 self
.condition
= threading
.Condition()
1282 self
.engine
= Engine(self
)
1283 self
.listener
= Listener(self
)
1284 self
.reaper
= Reaper(self
)
1286 def isLoopback(self
):
1287 return self
.intf
.startswith("127.0.0.1")
1289 def isLinklocal(self
):
1290 return self
.intf
.startswith("169.254.")
1292 def wait(self
, timeout
):
1293 """Calling thread waits for a given number of milliseconds or
1295 self
.condition
.acquire()
1296 self
.condition
.wait(timeout
/1000)
1297 self
.condition
.release()
1299 def notifyAll(self
):
1300 """Notifies all waiting threads"""
1301 self
.condition
.acquire()
1302 self
.condition
.notifyAll()
1303 self
.condition
.release()
1305 def getServiceInfo(self
, type, name
, timeout
=3000):
1306 """Returns network's service information for a particular
1307 name and type, or None if no service matches by the timeout,
1308 which defaults to 3 seconds."""
1309 info
= ServiceInfo(type, name
)
1310 if info
.request(self
, timeout
):
1314 def addServiceListener(self
, type, listener
):
1315 """Adds a listener for a particular service type. This object
1316 will then have its updateRecord method called when information
1317 arrives for that type."""
1318 self
.removeServiceListener(listener
)
1319 self
.browsers
.append(ServiceBrowser(self
, type, listener
))
1321 def removeServiceListener(self
, listener
):
1322 """Removes a listener from the set that is currently listening."""
1323 for browser
in self
.browsers
:
1324 if browser
.listener
== listener
:
1328 def registerService(self
, info
, ttl
=_DNS_TTL
):
1329 """Registers service information to the network with a default TTL
1330 of 60 seconds. Zeroconf will then respond to requests for
1331 information for that service. The name of the service may be
1332 changed if needed to make it unique on the network."""
1333 self
.checkService(info
)
1334 self
.services
[info
.name
.lower()] = info
1335 if info
.type in self
.servicetypes
:
1336 self
.servicetypes
[info
.type]+=1
1338 self
.servicetypes
[info
.type]=1
1339 now
= currentTimeMillis()
1344 self
.wait(nextTime
- now
)
1345 now
= currentTimeMillis()
1347 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1348 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
,
1349 _CLASS_IN
, ttl
, info
.name
), 0)
1350 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
,
1351 _CLASS_IN
, ttl
, info
.priority
, info
.weight
, info
.port
,
1353 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
,
1356 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
,
1357 _CLASS_IN
, ttl
, info
.address
), 0)
1360 nextTime
+= _REGISTER_TIME
1362 def unregisterService(self
, info
):
1363 """Unregister a service."""
1365 del(self
.services
[info
.name
.lower()])
1366 if self
.servicetypes
[info
.type]>1:
1367 self
.servicetypes
[info
.type]-=1
1369 del self
.servicetypes
[info
.type]
1372 now
= currentTimeMillis()
1377 self
.wait(nextTime
- now
)
1378 now
= currentTimeMillis()
1380 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1381 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
,
1382 _CLASS_IN
, 0, info
.name
), 0)
1383 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
,
1384 _CLASS_IN
, 0, info
.priority
, info
.weight
, info
.port
,
1386 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
,
1389 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
,
1390 _CLASS_IN
, 0, info
.address
), 0)
1393 nextTime
+= _UNREGISTER_TIME
1395 def unregisterAllServices(self
):
1396 """Unregister all registered services."""
1397 if len(self
.services
) > 0:
1398 now
= currentTimeMillis()
1403 self
.wait(nextTime
- now
)
1404 now
= currentTimeMillis()
1406 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1407 for info
in self
.services
.values():
1408 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
,
1409 _CLASS_IN
, 0, info
.name
), 0)
1410 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
,
1411 _CLASS_IN
, 0, info
.priority
, info
.weight
,
1412 info
.port
, info
.server
), 0)
1413 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
,
1414 _CLASS_IN
, 0, info
.text
), 0)
1416 out
.addAnswerAtTime(DNSAddress(info
.server
,
1417 _TYPE_A
, _CLASS_IN
, 0, info
.address
), 0)
1420 nextTime
+= _UNREGISTER_TIME
1422 def checkService(self
, info
):
1423 """Checks the network for a unique service name, modifying the
1424 ServiceInfo passed in if it is not unique."""
1425 now
= currentTimeMillis()
1429 for record
in self
.cache
.entriesWithName(info
.type):
1430 if (record
.type == _TYPE_PTR
and
1431 not record
.isExpired(now
) and
1432 record
.alias
== info
.name
):
1433 if info
.name
.find('.') < 0:
1434 info
.name
= '%s.[%s:%s].%s' % (info
.name
,
1435 info
.address
, info
.port
, info
.type)
1437 self
.checkService(info
)
1439 raise NonUniqueNameException
1441 self
.wait(nextTime
- now
)
1442 now
= currentTimeMillis()
1444 out
= DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA
)
1446 out
.addQuestion(DNSQuestion(info
.type, _TYPE_PTR
, _CLASS_IN
))
1447 out
.addAuthorativeAnswer(DNSPointer(info
.type, _TYPE_PTR
,
1448 _CLASS_IN
, _DNS_TTL
, info
.name
))
1451 nextTime
+= _CHECK_TIME
1453 def addListener(self
, listener
, question
):
1454 """Adds a listener for a given question. The listener will have
1455 its updateRecord method called when information is available to
1456 answer the question."""
1457 now
= currentTimeMillis()
1458 self
.listeners
.append(listener
)
1459 if question
is not None:
1460 for record
in self
.cache
.entriesWithName(question
.name
):
1461 if question
.answeredBy(record
) and not record
.isExpired(now
):
1462 listener
.updateRecord(self
, now
, record
)
1465 def removeListener(self
, listener
):
1466 """Removes a listener."""
1468 self
.listeners
.remove(listener
)
1473 def updateRecord(self
, now
, rec
):
1474 """Used to notify listeners of new information that has updated
1476 for listener
in self
.listeners
:
1477 listener
.updateRecord(self
, now
, rec
)
1480 def handleResponse(self
, msg
):
1481 """Deal with incoming response packets. All answers
1482 are held in the cache, and listeners are notified."""
1483 now
= currentTimeMillis()
1484 for record
in msg
.answers
:
1485 expired
= record
.isExpired(now
)
1486 if record
in self
.cache
.entries():
1488 self
.cache
.remove(record
)
1490 entry
= self
.cache
.get(record
)
1491 if entry
is not None:
1492 entry
.resetTTL(record
)
1495 self
.cache
.add(record
)
1497 self
.updateRecord(now
, record
)
1499 def handleQuery(self
, msg
, addr
, port
):
1500 """Deal with incoming query packets. Provides a response if
1504 # Support unicast client responses
1506 if port
!= _MDNS_PORT
:
1507 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
, False)
1508 for question
in msg
.questions
:
1509 out
.addQuestion(question
)
1511 for question
in msg
.questions
:
1512 if question
.type == _TYPE_PTR
:
1513 if question
.name
== "_services._dns-sd._udp.local.":
1514 for stype
in self
.servicetypes
.keys():
1516 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1518 DNSPointer("_services._dns-sd._udp.local.",
1519 _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, stype
))
1520 for service
in self
.services
.values():
1521 if question
.name
== service
.type:
1523 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1525 DNSPointer(service
.type, _TYPE_PTR
,
1526 _CLASS_IN
, _DNS_TTL
, service
.name
))
1530 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1532 # Answer A record queries for any service addresses we know
1533 if question
.type in (_TYPE_A
, _TYPE_ANY
):
1534 for service
in self
.services
.values():
1535 if service
.server
== question
.name
.lower():
1536 out
.addAnswer(msg
, DNSAddress(question
.name
,
1537 _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
,
1538 _DNS_TTL
, service
.address
))
1540 service
= self
.services
.get(question
.name
.lower(), None)
1541 if not service
: continue
1543 if question
.type in (_TYPE_SRV
, _TYPE_ANY
):
1544 out
.addAnswer(msg
, DNSService(question
.name
,
1545 _TYPE_SRV
, _CLASS_IN | _CLASS_UNIQUE
,
1546 _DNS_TTL
, service
.priority
, service
.weight
,
1547 service
.port
, service
.server
))
1548 if question
.type in (_TYPE_TXT
, _TYPE_ANY
):
1549 out
.addAnswer(msg
, DNSText(question
.name
,
1550 _TYPE_TXT
, _CLASS_IN | _CLASS_UNIQUE
,
1551 _DNS_TTL
, service
.text
))
1552 if question
.type == _TYPE_SRV
:
1553 out
.addAdditionalAnswer(DNSAddress(service
.server
,
1554 _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
,
1555 _DNS_TTL
, service
.address
))
1557 traceback
.print_exc()
1559 if out
is not None and out
.answers
:
1561 self
.send(out
, addr
, port
)
1563 def send(self
, out
, addr
= _MDNS_ADDR
, port
= _MDNS_PORT
):
1564 """Sends an outgoing packet."""
1565 # This is a quick test to see if we can parse the packets we generate
1566 #temp = DNSIncoming(out.packet())
1568 bytes_sent
= self
.socket
.sendto(out
.packet(), 0, (addr
, port
))
1570 # Ignore this, it may be a temporary loss of network connection
1574 """Ends the background threads, and prevent this instance from
1575 servicing further queries."""
1577 if not _GLOBAL_DONE
:
1580 self
.engine
.notify()
1581 self
.unregisterAllServices()
1582 self
.socket
.setsockopt(socket
.SOL_IP
,
1583 socket
.IP_DROP_MEMBERSHIP
,
1584 socket
.inet_aton(_MDNS_ADDR
) +
1585 socket
.inet_aton('0.0.0.0'))
1588 # Test a few module features, including service registration, service
1589 # query (for Zoe), and service unregistration.
1591 if __name__
== '__main__':
1592 print "Multicast DNS Service Discovery for Python, version", __version__
1594 print "1. Testing registration of a service..."
1595 desc
= {'version':'0.10','a':'test value', 'b':'another value'}
1596 info
= ServiceInfo("_http._tcp.local.",
1597 "My Service Name._http._tcp.local.",
1598 socket
.inet_aton("127.0.0.1"), 1234, 0, 0, desc
)
1599 print " Registering service..."
1600 r
.registerService(info
)
1601 print " Registration done."
1602 print "2. Testing query of service information..."
1603 print " Getting ZOE service:",
1604 print str(r
.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1605 print " Query done."
1606 print "3. Testing query of own service..."
1607 print " Getting self:",
1608 print str(r
.getServiceInfo("_http._tcp.local.",
1609 "My Service Name._http._tcp.local."))
1610 print " Query done."
1611 print "4. Testing unregister of service information..."
1612 r
.unregisterService(info
)
1613 print " Unregister done."