1 """ Multicast DNS Service Discovery for Python, v0.12
2 Copyright (C) 2003, Paul Scott-Murphy
4 This module provides a framework for the use of DNS Service Discovery
5 using IP multicast. It has been tested against the JRendezvous
6 implementation from <a href="http://strangeberry.com">StrangeBerry</a>,
7 and against the mDNSResponder from Mac OS X 10.3.8.
9 This library is free software; you can redistribute it and/or
10 modify it under the terms of the GNU Lesser General Public
11 License as published by the Free Software Foundation; either
12 version 2.1 of the License, or (at your option) any later version.
14 This library is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 Lesser General Public License for more details.
19 You should have received a copy of the GNU Lesser General Public
20 License along with this library; if not, write to the Free Software
21 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 """0.12 update - allow selection of binding interface
26 typo fix - Thanks A. M. Kuchlingi
27 removed all use of word 'Rendezvous' - this is an API change"""
29 """0.11 update - correction to comments for addListener method
30 support for new record types seen from OS X
33 ignore unknown DNS record types
34 fixes to name decoding
35 works alongside other processes using port 5353 (e.g. on Mac OS X)
36 tested against Mac OS X 10.3.2's mDNSResponder
37 corrections to removal of list entries for service browser"""
39 """0.10 update - Jonathon Paisley contributed these corrections:
40 always multicast replies, even when query is unicast
41 correct a pointer encoding problem
42 can now write records in any order
43 traceback shown on failure
44 better TXT record parsing
45 server is now separate from name
46 can cancel a service browser
48 modified some unit tests to accommodate these changes"""
50 """0.09 update - remove all records on service unregistration
51 fix DOS security problem with readName"""
53 """0.08 update - changed licensing to LGPL"""
55 """0.07 update - faster shutdown on engine
56 pointer encoding of outgoing names
57 ServiceBrowser now works
60 """0.06 update - small improvements with unit tests
61 added defined exception types
63 fixed hostname/interface problem
64 fixed socket timeout problem
65 fixed addServiceListener() typo bug
66 using select() for socket reads
67 tested on Debian unstable with Python 2.2.2"""
69 """0.05 update - ensure case insensitivty on domain names
70 support for unicast DNS queries"""
72 """0.04 update - added some unit tests
73 added __ne__ adjuncts where required
74 ensure names end in '.local.'
75 timeout on receiving socket for clean shutdown"""
77 __author__
= "Paul Scott-Murphy"
78 __email__
= "paul at scott dash murphy dot com"
88 __all__
= ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
92 globals()['_GLOBAL_DONE'] = 0
94 # Some timing constants
96 _UNREGISTER_TIME
= 125
104 _MDNS_ADDR
= '224.0.0.251'
107 _DNS_TTL
= 60 * 60; # one hour default TTL
109 _MAX_MSG_TYPICAL
= 1460 # unused
110 _MAX_MSG_ABSOLUTE
= 8972
112 _FLAGS_QR_MASK
= 0x8000 # query response mask
113 _FLAGS_QR_QUERY
= 0x0000 # query
114 _FLAGS_QR_RESPONSE
= 0x8000 # response
116 _FLAGS_AA
= 0x0400 # Authorative answer
117 _FLAGS_TC
= 0x0200 # Truncated
118 _FLAGS_RD
= 0x0100 # Recursion desired
119 _FLAGS_RA
= 0x8000 # Recursion available
121 _FLAGS_Z
= 0x0040 # Zero
122 _FLAGS_AD
= 0x0020 # Authentic data
123 _FLAGS_CD
= 0x0010 # Checking disabled
132 _CLASS_UNIQUE
= 0x8000
154 # Mapping constants to names
156 _CLASSES
= { _CLASS_IN
: "in",
160 _CLASS_NONE
: "none",
163 _TYPES
= { _TYPE_A
: "a",
167 _TYPE_CNAME
: "cname",
175 _TYPE_HINFO
: "hinfo",
176 _TYPE_MINFO
: "minfo",
179 _TYPE_AAAA
: "quada",
185 def currentTimeMillis():
186 """Current system time in milliseconds"""
187 return time
.time() * 1000
191 class NonLocalNameException(Exception):
194 class NonUniqueNameException(Exception):
197 class NamePartTooLongException(Exception):
200 class AbstractMethodException(Exception):
203 class BadTypeInNameException(Exception):
206 # implementation classes
208 class DNSEntry(object):
211 def __init__(self
, name
, type, clazz
):
212 self
.key
= name
.lower()
215 self
.clazz
= clazz
& _CLASS_MASK
216 self
.unique
= (clazz
& _CLASS_UNIQUE
) != 0
218 def __eq__(self
, other
):
219 """Equality test on name, type, and class"""
220 if isinstance(other
, DNSEntry
):
221 return self
.name
== other
.name
and self
.type == other
.type and self
.clazz
== other
.clazz
224 def __ne__(self
, other
):
225 """Non-equality test"""
226 return not self
.__eq
__(other
)
228 def getClazz(self
, clazz
):
231 return _CLASSES
[clazz
]
233 return "?(%s)" % (clazz
)
235 def getType(self
, type):
240 return "?(%s)" % (type)
242 def toString(self
, hdr
, other
):
243 """String representation with additional information"""
244 result
= "%s[%s,%s" % (hdr
, self
.getType(self
.type), self
.getClazz(self
.clazz
))
250 if other
is not None:
251 result
+= ",%s]" % (other
)
256 class DNSQuestion(DNSEntry
):
257 """A DNS question entry"""
259 def __init__(self
, name
, type, clazz
):
260 #if not name.endswith(".local."):
261 # raise NonLocalNameException
262 DNSEntry
.__init
__(self
, name
, type, clazz
)
264 def answeredBy(self
, rec
):
265 """Returns true if the question is answered by the record"""
266 return self
.clazz
== rec
.clazz
and (self
.type == rec
.type or self
.type == _TYPE_ANY
) and self
.name
== rec
.name
269 """String representation"""
270 return DNSEntry
.toString(self
, "question", None)
273 class DNSRecord(DNSEntry
):
274 """A DNS record - like a DNS entry, but has a TTL"""
276 def __init__(self
, name
, type, clazz
, ttl
):
277 DNSEntry
.__init
__(self
, name
, type, clazz
)
279 self
.created
= currentTimeMillis()
281 def __eq__(self
, other
):
282 """Tests equality as per DNSRecord"""
283 if isinstance(other
, DNSRecord
):
284 return DNSEntry
.__eq
__(self
, other
)
287 def suppressedBy(self
, msg
):
288 """Returns true if any answer in a message can suffice for the
289 information held in this record."""
290 for record
in msg
.answers
:
291 if self
.suppressedByAnswer(record
):
295 def suppressedByAnswer(self
, other
):
296 """Returns true if another record has same name, type and class,
297 and if its TTL is at least half of this record's."""
298 if self
== other
and other
.ttl
> (self
.ttl
/ 2):
302 def getExpirationTime(self
, percent
):
303 """Returns the time at which this record will have expired
304 by a certain percentage."""
305 return self
.created
+ (percent
* self
.ttl
* 10)
307 def getRemainingTTL(self
, now
):
308 """Returns the remaining TTL in seconds."""
309 return max(0, (self
.getExpirationTime(100) - now
) / 1000)
311 def isExpired(self
, now
):
312 """Returns true if this record has expired."""
313 return self
.getExpirationTime(100) <= now
315 def isStale(self
, now
):
316 """Returns true if this record is at least half way expired."""
317 return self
.getExpirationTime(50) <= now
319 def resetTTL(self
, other
):
320 """Sets this record's TTL and created time to that of
322 self
.created
= other
.created
325 def write(self
, out
):
326 """Abstract method"""
327 raise AbstractMethodException
329 def toString(self
, other
):
330 """String representation with addtional information"""
331 arg
= "%s/%s,%s" % (self
.ttl
, self
.getRemainingTTL(currentTimeMillis()), other
)
332 return DNSEntry
.toString(self
, "record", arg
)
334 class DNSAddress(DNSRecord
):
335 """A DNS address record"""
337 def __init__(self
, name
, type, clazz
, ttl
, address
):
338 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
339 self
.address
= address
341 def write(self
, out
):
342 """Used in constructing an outgoing packet"""
343 out
.writeString(self
.address
, len(self
.address
))
345 def __eq__(self
, other
):
346 """Tests equality on address"""
347 if isinstance(other
, DNSAddress
):
348 return self
.address
== other
.address
352 """String representation"""
354 return socket
.inet_ntoa(self
.address
)
358 class DNSHinfo(DNSRecord
):
359 """A DNS host information record"""
361 def __init__(self
, name
, type, clazz
, ttl
, cpu
, os
):
362 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
366 def write(self
, out
):
367 """Used in constructing an outgoing packet"""
368 out
.writeString(self
.cpu
, len(self
.cpu
))
369 out
.writeString(self
.os
, len(self
.os
))
371 def __eq__(self
, other
):
372 """Tests equality on cpu and os"""
373 if isinstance(other
, DNSHinfo
):
374 return self
.cpu
== other
.cpu
and self
.os
== other
.os
378 """String representation"""
379 return self
.cpu
+ " " + self
.os
381 class DNSPointer(DNSRecord
):
382 """A DNS pointer record"""
384 def __init__(self
, name
, type, clazz
, ttl
, alias
):
385 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
388 def write(self
, out
):
389 """Used in constructing an outgoing packet"""
390 out
.writeName(self
.alias
)
392 def __eq__(self
, other
):
393 """Tests equality on alias"""
394 if isinstance(other
, DNSPointer
):
395 return self
.alias
== other
.alias
399 """String representation"""
400 return self
.toString(self
.alias
)
402 class DNSText(DNSRecord
):
403 """A DNS text record"""
405 def __init__(self
, name
, type, clazz
, ttl
, text
):
406 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
409 def write(self
, out
):
410 """Used in constructing an outgoing packet"""
411 out
.writeString(self
.text
, len(self
.text
))
413 def __eq__(self
, other
):
414 """Tests equality on text"""
415 if isinstance(other
, DNSText
):
416 return self
.text
== other
.text
420 """String representation"""
421 if len(self
.text
) > 10:
422 return self
.toString(self
.text
[:7] + "...")
424 return self
.toString(self
.text
)
426 class DNSService(DNSRecord
):
427 """A DNS service record"""
429 def __init__(self
, name
, type, clazz
, ttl
, priority
, weight
, port
, server
):
430 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
431 self
.priority
= priority
436 def write(self
, out
):
437 """Used in constructing an outgoing packet"""
438 out
.writeShort(self
.priority
)
439 out
.writeShort(self
.weight
)
440 out
.writeShort(self
.port
)
441 out
.writeName(self
.server
)
443 def __eq__(self
, other
):
444 """Tests equality on priority, weight, port and server"""
445 if isinstance(other
, DNSService
):
446 return self
.priority
== other
.priority
and self
.weight
== other
.weight
and self
.port
== other
.port
and self
.server
== other
.server
450 """String representation"""
451 return self
.toString("%s:%s" % (self
.server
, self
.port
))
453 class DNSIncoming(object):
454 """Object representation of an incoming DNS packet"""
456 def __init__(self
, data
):
457 """Constructor from string holding bytes of packet"""
462 self
.numQuestions
= 0
464 self
.numAuthorities
= 0
465 self
.numAdditionals
= 0
471 def readHeader(self
):
472 """Reads header portion of packet"""
474 length
= struct
.calcsize(format
)
475 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
476 self
.offset
+= length
480 self
.numQuestions
= info
[2]
481 self
.numAnswers
= info
[3]
482 self
.numAuthorities
= info
[4]
483 self
.numAdditionals
= info
[5]
485 def readQuestions(self
):
486 """Reads questions section of packet"""
488 length
= struct
.calcsize(format
)
489 for i
in range(0, self
.numQuestions
):
490 name
= self
.readName()
491 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
492 self
.offset
+= length
494 question
= DNSQuestion(name
, info
[0], info
[1])
495 self
.questions
.append(question
)
498 """Reads an integer from the packet"""
500 length
= struct
.calcsize(format
)
501 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
502 self
.offset
+= length
505 def readCharacterString(self
):
506 """Reads a character string from the packet"""
507 length
= ord(self
.data
[self
.offset
])
509 return self
.readString(length
)
511 def readString(self
, len):
512 """Reads a string of a given length from the packet"""
513 format
= '!' + str(len) + 's'
514 length
= struct
.calcsize(format
)
515 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
516 self
.offset
+= length
519 def readUnsignedShort(self
):
520 """Reads an unsigned short from the packet"""
522 length
= struct
.calcsize(format
)
523 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
524 self
.offset
+= length
527 def readOthers(self
):
528 """Reads the answers, authorities and additionals section of the packet"""
530 length
= struct
.calcsize(format
)
531 n
= self
.numAnswers
+ self
.numAuthorities
+ self
.numAdditionals
532 for i
in range(0, n
):
533 domain
= self
.readName()
534 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
535 self
.offset
+= length
538 if info
[0] == _TYPE_A
:
539 rec
= DNSAddress(domain
, info
[0], info
[1], info
[2], self
.readString(4))
540 elif info
[0] == _TYPE_CNAME
or info
[0] == _TYPE_PTR
:
541 rec
= DNSPointer(domain
, info
[0], info
[1], info
[2], self
.readName())
542 elif info
[0] == _TYPE_TXT
:
543 rec
= DNSText(domain
, info
[0], info
[1], info
[2], self
.readString(info
[3]))
544 elif info
[0] == _TYPE_SRV
:
545 rec
= DNSService(domain
, info
[0], info
[1], info
[2], self
.readUnsignedShort(), self
.readUnsignedShort(), self
.readUnsignedShort(), self
.readName())
546 elif info
[0] == _TYPE_HINFO
:
547 rec
= DNSHinfo(domain
, info
[0], info
[1], info
[2], self
.readCharacterString(), self
.readCharacterString())
548 elif info
[0] == _TYPE_AAAA
:
549 rec
= DNSAddress(domain
, info
[0], info
[1], info
[2], self
.readString(16))
551 # Try to ignore types we don't know about
552 # Skip the payload for the resource record so the next
553 # records can be parsed correctly
554 self
.offset
+= info
[3]
557 self
.answers
.append(rec
)
560 """Returns true if this is a query"""
561 return (self
.flags
& _FLAGS_QR_MASK
) == _FLAGS_QR_QUERY
563 def isResponse(self
):
564 """Returns true if this is a response"""
565 return (self
.flags
& _FLAGS_QR_MASK
) == _FLAGS_QR_RESPONSE
567 def readUTF(self
, offset
, len):
568 """Reads a UTF-8 string of a given length from the packet"""
569 return unicode(self
.data
[offset
:offset
+len], 'utf-8', 'replace')
572 """Reads a domain name from the packet"""
579 len = ord(self
.data
[off
])
585 result
= ''.join((result
, self
.readUTF(off
, len) + '.'))
590 off
= ((len & 0x3F) << 8) |
ord(self
.data
[off
])
592 raise "Bad domain name (circular) at " + str(off
)
595 raise "Bad domain name at " + str(off
)
605 class DNSOutgoing(object):
606 """Object representation of an outgoing packet"""
608 def __init__(self
, flags
, multicast
= 1):
611 self
.multicast
= multicast
619 self
.authorities
= []
620 self
.additionals
= []
622 def addQuestion(self
, record
):
623 """Adds a question"""
624 self
.questions
.append(record
)
626 def addAnswer(self
, inp
, record
):
628 if not record
.suppressedBy(inp
):
629 self
.addAnswerAtTime(record
, 0)
631 def addAnswerAtTime(self
, record
, now
):
632 """Adds an answer if if does not expire by a certain time"""
633 if record
is not None:
634 if now
== 0 or not record
.isExpired(now
):
635 self
.answers
.append((record
, now
))
637 def addAuthorativeAnswer(self
, record
):
638 """Adds an authoritative answer"""
639 self
.authorities
.append(record
)
641 def addAdditionalAnswer(self
, record
):
642 """Adds an additional answer"""
643 self
.additionals
.append(record
)
645 def writeByte(self
, value
):
646 """Writes a single byte to the packet"""
648 self
.data
.append(struct
.pack(format
, chr(value
)))
651 def insertShort(self
, index
, value
):
652 """Inserts an unsigned short in a certain position in the packet"""
654 self
.data
.insert(index
, struct
.pack(format
, value
))
657 def writeShort(self
, value
):
658 """Writes an unsigned short to the packet"""
660 self
.data
.append(struct
.pack(format
, value
))
663 def writeInt(self
, value
):
664 """Writes an unsigned integer to the packet"""
666 self
.data
.append(struct
.pack(format
, int(value
)))
669 def writeString(self
, value
, length
):
670 """Writes a string to the packet"""
671 format
= '!' + str(length
) + 's'
672 self
.data
.append(struct
.pack(format
, value
))
675 def writeUTF(self
, s
):
676 """Writes a UTF-8 string of a given length to the packet"""
677 utfstr
= s
.encode('utf-8')
680 raise NamePartTooLongException
681 self
.writeByte(length
)
682 self
.writeString(utfstr
, length
)
684 def writeName(self
, name
):
685 """Writes a domain name to the packet"""
688 # Find existing instance of this name in packet
690 index
= self
.names
[name
]
692 # No record of this name already, so write it
693 # out as normal, recording the location of the name
694 # for future pointers to it.
696 self
.names
[name
] = self
.size
697 parts
= name
.split('.')
705 # An index was found, so write a pointer to it
707 self
.writeByte((index
>> 8) |
0xC0)
708 self
.writeByte(index
)
710 def writeQuestion(self
, question
):
711 """Writes a question to the packet"""
712 self
.writeName(question
.name
)
713 self
.writeShort(question
.type)
714 self
.writeShort(question
.clazz
)
716 def writeRecord(self
, record
, now
):
717 """Writes a record (answer, authoritative answer, additional) to
719 self
.writeName(record
.name
)
720 self
.writeShort(record
.type)
721 if record
.unique
and self
.multicast
:
722 self
.writeShort(record
.clazz | _CLASS_UNIQUE
)
724 self
.writeShort(record
.clazz
)
726 self
.writeInt(record
.ttl
)
728 self
.writeInt(record
.getRemainingTTL(now
))
729 index
= len(self
.data
)
730 # Adjust size for the short we will write before this record
736 length
= len(''.join(self
.data
[index
:]))
737 self
.insertShort(index
, length
) # Here is the short we adjusted for
740 """Returns a string containing the packet's bytes
742 No further parts should be added to the packet once this
744 if not self
.finished
:
746 for question
in self
.questions
:
747 self
.writeQuestion(question
)
748 for answer
, time
in self
.answers
:
749 self
.writeRecord(answer
, time
)
750 for authority
in self
.authorities
:
751 self
.writeRecord(authority
, 0)
752 for additional
in self
.additionals
:
753 self
.writeRecord(additional
, 0)
755 self
.insertShort(0, len(self
.additionals
))
756 self
.insertShort(0, len(self
.authorities
))
757 self
.insertShort(0, len(self
.answers
))
758 self
.insertShort(0, len(self
.questions
))
759 self
.insertShort(0, self
.flags
)
761 self
.insertShort(0, 0)
763 self
.insertShort(0, self
.id)
764 return ''.join(self
.data
)
767 class DNSCache(object):
768 """A cache of DNS entries"""
773 def add(self
, entry
):
776 list = self
.cache
[entry
.key
]
778 list = self
.cache
[entry
.key
] = []
781 def remove(self
, entry
):
782 """Removes an entry"""
784 list = self
.cache
[entry
.key
]
789 def get(self
, entry
):
790 """Gets an entry by key. Will return None if there is no
793 list = self
.cache
[entry
.key
]
794 return list[list.index(entry
)]
798 def getByDetails(self
, name
, type, clazz
):
799 """Gets an entry by details. Will return None if there is
800 no matching entry."""
801 entry
= DNSEntry(name
, type, clazz
)
802 return self
.get(entry
)
804 def entriesWithName(self
, name
):
805 """Returns a list of entries whose key matches the name."""
807 return self
.cache
[name
]
812 """Returns a list of all entries"""
813 def add(x
, y
): return x
+y
815 return reduce(add
, self
.cache
.values())
820 class Engine(threading
.Thread
):
821 """An engine wraps read access to sockets, allowing objects that
822 need to receive data from sockets to be called back when the
825 A reader needs a handle_read() method, which is called when the socket
826 it is interested in is ready for reading.
828 Writers are not implemented here, because we only send short
832 def __init__(self
, zeroconf
):
833 threading
.Thread
.__init
__(self
)
834 self
.zeroconf
= zeroconf
835 self
.readers
= {} # maps socket to reader
837 self
.condition
= threading
.Condition()
841 while not globals()['_GLOBAL_DONE']:
842 rs
= self
.getReaders()
844 # No sockets to manage, but we wait for the timeout
845 # or addition of a socket
847 self
.condition
.acquire()
848 self
.condition
.wait(self
.timeout
)
849 self
.condition
.release()
852 rr
, wr
, er
= select
.select(rs
, [], [], self
.timeout
)
855 self
.readers
[socket
].handle_read()
857 traceback
.print_exc()
861 def getReaders(self
):
863 self
.condition
.acquire()
864 result
= self
.readers
.keys()
865 self
.condition
.release()
868 def addReader(self
, reader
, socket
):
869 self
.condition
.acquire()
870 self
.readers
[socket
] = reader
871 self
.condition
.notify()
872 self
.condition
.release()
874 def delReader(self
, socket
):
875 self
.condition
.acquire()
876 del(self
.readers
[socket
])
877 self
.condition
.notify()
878 self
.condition
.release()
881 self
.condition
.acquire()
882 self
.condition
.notify()
883 self
.condition
.release()
885 class Listener(object):
886 """A Listener is used by this module to listen on the multicast
887 group to which DNS messages are sent, allowing the implementation
888 to cache information as it arrives.
890 It requires registration with an Engine object in order to have
891 the read() method called when a socket is availble for reading."""
893 def __init__(self
, zeroconf
):
894 self
.zeroconf
= zeroconf
895 self
.zeroconf
.engine
.addReader(self
, self
.zeroconf
.socket
)
897 def handle_read(self
):
899 data
, (addr
, port
) = self
.zeroconf
.socket
.recvfrom(_MAX_MSG_ABSOLUTE
)
900 except socket
.error
, e
:
901 # If the socket was closed by another thread -- which happens
902 # regularly on shutdown -- an EBADF exception is thrown here.
904 if e
[0] == socket
.EBADF
:
909 msg
= DNSIncoming(data
)
911 # Always multicast responses
913 if port
== _MDNS_PORT
:
914 self
.zeroconf
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
915 # If it's not a multicast query, reply via unicast
918 elif port
== _DNS_PORT
:
919 self
.zeroconf
.handleQuery(msg
, addr
, port
)
920 self
.zeroconf
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
922 self
.zeroconf
.handleResponse(msg
)
925 class Reaper(threading
.Thread
):
926 """A Reaper is used by this module to remove cache entries that
929 def __init__(self
, zeroconf
):
930 threading
.Thread
.__init
__(self
)
931 self
.zeroconf
= zeroconf
936 self
.zeroconf
.wait(10 * 1000)
937 if globals()['_GLOBAL_DONE']:
939 now
= currentTimeMillis()
940 for record
in self
.zeroconf
.cache
.entries():
941 if record
.isExpired(now
):
942 self
.zeroconf
.updateRecord(now
, record
)
943 self
.zeroconf
.cache
.remove(record
)
946 class ServiceBrowser(threading
.Thread
):
947 """Used to browse for a service of a specific type.
949 The listener object will have its addService() and
950 removeService() methods called when this browser
951 discovers changes in the services availability."""
953 def __init__(self
, zeroconf
, type, listener
):
954 """Creates a browser for a specific type"""
955 threading
.Thread
.__init
__(self
)
956 self
.zeroconf
= zeroconf
958 self
.listener
= listener
960 self
.nextTime
= currentTimeMillis()
961 self
.delay
= _BROWSER_TIME
966 self
.zeroconf
.addListener(self
, DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
969 def updateRecord(self
, zeroconf
, now
, record
):
970 """Callback invoked by Zeroconf when new information arrives.
972 Updates information required by browser in the Zeroconf cache."""
973 if record
.type == _TYPE_PTR
and record
.name
== self
.type:
974 expired
= record
.isExpired(now
)
976 oldrecord
= self
.services
[record
.alias
.lower()]
978 oldrecord
.resetTTL(record
)
980 del(self
.services
[record
.alias
.lower()])
981 callback
= lambda x
: self
.listener
.removeService(x
, self
.type, record
.alias
)
982 self
.list.append(callback
)
986 self
.services
[record
.alias
.lower()] = record
987 callback
= lambda x
: self
.listener
.addService(x
, self
.type, record
.alias
)
988 self
.list.append(callback
)
990 expires
= record
.getExpirationTime(75)
991 if expires
< self
.nextTime
:
992 self
.nextTime
= expires
996 self
.zeroconf
.notifyAll()
1001 now
= currentTimeMillis()
1002 if len(self
.list) == 0 and self
.nextTime
> now
:
1003 self
.zeroconf
.wait(self
.nextTime
- now
)
1004 if globals()['_GLOBAL_DONE'] or self
.done
:
1006 now
= currentTimeMillis()
1008 if self
.nextTime
<= now
:
1009 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
1010 out
.addQuestion(DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
1011 for record
in self
.services
.values():
1012 if not record
.isExpired(now
):
1013 out
.addAnswerAtTime(record
, now
)
1014 self
.zeroconf
.send(out
)
1015 self
.nextTime
= now
+ self
.delay
1016 self
.delay
= min(20 * 1000, self
.delay
* 2)
1018 if len(self
.list) > 0:
1019 event
= self
.list.pop(0)
1021 if event
is not None:
1022 event(self
.zeroconf
)
1025 class ServiceInfo(object):
1026 """Service information"""
1028 def __init__(self
, type, name
, address
=None, port
=None, weight
=0, priority
=0, properties
=None, server
=None):
1029 """Create a service description.
1031 type: fully qualified service type name
1032 name: fully qualified service name
1033 address: IP address as unsigned short, network byte order
1034 port: port that the service runs on
1035 weight: weight of the service
1036 priority: priority of the service
1037 properties: dictionary of properties (or a string holding the bytes for the text field)
1038 server: fully qualified name for service host (defaults to name)"""
1040 if not name
.endswith(type):
1041 raise BadTypeInNameException
1044 self
.address
= address
1046 self
.weight
= weight
1047 self
.priority
= priority
1049 self
.server
= server
1052 self
.setProperties(properties
)
1054 def setProperties(self
, properties
):
1055 """Sets properties and text of this info from a dictionary"""
1056 if isinstance(properties
, dict):
1057 self
.properties
= properties
1060 for key
in properties
:
1061 value
= properties
[key
]
1063 suffix
= ''.encode('utf-8')
1064 elif isinstance(value
, str):
1065 suffix
= value
.encode('utf-8')
1066 elif isinstance(value
, int):
1072 suffix
= ''.encode('utf-8')
1073 list.append('='.join((key
, suffix
)))
1075 result
= ''.join((result
, struct
.pack('!c', chr(len(item
))), item
))
1078 self
.text
= properties
1080 def setText(self
, text
):
1081 """Sets properties and text given a text field"""
1089 length
= ord(text
[index
])
1091 strs
.append(text
[index
:index
+length
])
1095 eindex
= s
.find('=')
1097 # No equals sign at all
1102 value
= s
[eindex
+1:]
1105 elif value
== 'false' or not value
:
1108 # Only update non-existent properties
1109 if key
and result
.get(key
) == None:
1112 self
.properties
= result
1114 traceback
.print_exc()
1115 self
.properties
= None
1123 if self
.type is not None and self
.name
.endswith("." + self
.type):
1124 return self
.name
[:len(self
.name
) - len(self
.type) - 1]
1127 def getAddress(self
):
1128 """Address accessor"""
1135 def getPriority(self
):
1136 """Pirority accessor"""
1137 return self
.priority
1139 def getWeight(self
):
1140 """Weight accessor"""
1143 def getProperties(self
):
1144 """Properties accessor"""
1145 return self
.properties
1151 def getServer(self
):
1152 """Server accessor"""
1155 def updateRecord(self
, zeroconf
, now
, record
):
1156 """Updates service information from a DNS record"""
1157 if record
is not None and not record
.isExpired(now
):
1158 if record
.type == _TYPE_A
:
1159 #if record.name == self.name:
1160 if record
.name
== self
.server
:
1161 self
.address
= record
.address
1162 elif record
.type == _TYPE_SRV
:
1163 if record
.name
== self
.name
:
1164 self
.server
= record
.server
1165 self
.port
= record
.port
1166 self
.weight
= record
.weight
1167 self
.priority
= record
.priority
1168 #self.address = None
1169 self
.updateRecord(zeroconf
, now
, zeroconf
.cache
.getByDetails(self
.server
, _TYPE_A
, _CLASS_IN
))
1170 elif record
.type == _TYPE_TXT
:
1171 if record
.name
== self
.name
:
1172 self
.setText(record
.text
)
1174 def request(self
, zeroconf
, timeout
):
1175 """Returns true if the service could be discovered on the
1176 network, and updates this object with details discovered.
1178 now
= currentTimeMillis()
1179 delay
= _LISTENER_TIME
1181 last
= now
+ timeout
1184 zeroconf
.addListener(self
, DNSQuestion(self
.name
, _TYPE_ANY
, _CLASS_IN
))
1185 while self
.server
is None or self
.address
is None or self
.text
is None:
1189 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
1190 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_SRV
, _CLASS_IN
))
1191 out
.addAnswerAtTime(zeroconf
.cache
.getByDetails(self
.name
, _TYPE_SRV
, _CLASS_IN
), now
)
1192 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_TXT
, _CLASS_IN
))
1193 out
.addAnswerAtTime(zeroconf
.cache
.getByDetails(self
.name
, _TYPE_TXT
, _CLASS_IN
), now
)
1194 if self
.server
is not None:
1195 out
.addQuestion(DNSQuestion(self
.server
, _TYPE_A
, _CLASS_IN
))
1196 out
.addAnswerAtTime(zeroconf
.cache
.getByDetails(self
.server
, _TYPE_A
, _CLASS_IN
), now
)
1201 zeroconf
.wait(min(next
, last
) - now
)
1202 now
= currentTimeMillis()
1205 zeroconf
.removeListener(self
)
1209 def __eq__(self
, other
):
1210 """Tests equality of service name"""
1211 if isinstance(other
, ServiceInfo
):
1212 return other
.name
== self
.name
1215 def __ne__(self
, other
):
1216 """Non-equality test"""
1217 return not self
.__eq
__(other
)
1220 """String representation"""
1221 result
= "service[%s,%s:%s," % (self
.name
, socket
.inet_ntoa(self
.getAddress()), self
.port
)
1222 if self
.text
is None:
1225 if len(self
.text
) < 20:
1228 result
+= self
.text
[:17] + "..."
1233 class Zeroconf(object):
1234 """Implementation of Zeroconf Multicast DNS Service Discovery
1236 Supports registration, unregistration, queries and browsing.
1238 def __init__(self
, bindaddress
=None):
1239 """Creates an instance of the Zeroconf class, establishing
1240 multicast communications, listening and reaping threads."""
1241 globals()['_GLOBAL_DONE'] = 0
1242 if bindaddress
is None:
1243 self
.intf
= socket
.gethostbyname(socket
.gethostname())
1245 self
.intf
= bindaddress
1246 self
.group
= ('', _MDNS_PORT
)
1247 self
.socket
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
1249 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
, 1)
1250 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEPORT
, 1)
1252 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1253 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1254 # Volume 2"), but some BSD-derived systems require
1255 # SO_REUSEPORT to be specified explicity. Also, not all
1256 # versions of Python have SO_REUSEPORT available. So
1257 # if you're on a BSD-based system, and haven't upgraded
1258 # to Python 2.3 yet, you may find this library doesn't
1262 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_TTL
, 255)
1263 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_LOOP
, 1)
1265 self
.socket
.bind(self
.group
)
1267 # Some versions of linux raise an exception even though
1268 # the SO_REUSE* options have been set, so ignore it
1271 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1272 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_ADD_MEMBERSHIP
, socket
.inet_aton(_MDNS_ADDR
) + socket
.inet_aton('0.0.0.0'))
1277 self
.servicetypes
= {}
1279 self
.cache
= DNSCache()
1281 self
.condition
= threading
.Condition()
1283 self
.engine
= Engine(self
)
1284 self
.listener
= Listener(self
)
1285 self
.reaper
= Reaper(self
)
1287 def isLoopback(self
):
1288 return self
.intf
.startswith("127.0.0.1")
1290 def isLinklocal(self
):
1291 return self
.intf
.startswith("169.254.")
1293 def wait(self
, timeout
):
1294 """Calling thread waits for a given number of milliseconds or
1296 self
.condition
.acquire()
1297 self
.condition
.wait(timeout
/1000)
1298 self
.condition
.release()
1300 def notifyAll(self
):
1301 """Notifies all waiting threads"""
1302 self
.condition
.acquire()
1303 self
.condition
.notifyAll()
1304 self
.condition
.release()
1306 def getServiceInfo(self
, type, name
, timeout
=3000):
1307 """Returns network's service information for a particular
1308 name and type, or None if no service matches by the timeout,
1309 which defaults to 3 seconds."""
1310 info
= ServiceInfo(type, name
)
1311 if info
.request(self
, timeout
):
1315 def addServiceListener(self
, type, listener
):
1316 """Adds a listener for a particular service type. This object
1317 will then have its updateRecord method called when information
1318 arrives for that type."""
1319 self
.removeServiceListener(listener
)
1320 self
.browsers
.append(ServiceBrowser(self
, type, listener
))
1322 def removeServiceListener(self
, listener
):
1323 """Removes a listener from the set that is currently listening."""
1324 for browser
in self
.browsers
:
1325 if browser
.listener
== listener
:
1329 def registerService(self
, info
, ttl
=_DNS_TTL
):
1330 """Registers service information to the network with a default TTL
1331 of 60 seconds. Zeroconf will then respond to requests for
1332 information for that service. The name of the service may be
1333 changed if needed to make it unique on the network."""
1334 self
.checkService(info
)
1335 self
.services
[info
.name
.lower()] = info
1336 if info
.type in self
.servicetypes
:
1337 self
.servicetypes
[info
.type]+=1
1339 self
.servicetypes
[info
.type]=1
1340 now
= currentTimeMillis()
1345 self
.wait(nextTime
- now
)
1346 now
= currentTimeMillis()
1348 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1349 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, ttl
, info
.name
), 0)
1350 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
, _CLASS_IN
, ttl
, info
.priority
, info
.weight
, info
.port
, info
.server
), 0)
1351 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
, ttl
, info
.text
), 0)
1353 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, ttl
, info
.address
), 0)
1356 nextTime
+= _REGISTER_TIME
1358 def unregisterService(self
, info
):
1359 """Unregister a service."""
1361 del(self
.services
[info
.name
.lower()])
1362 if self
.servicetypes
[info
.type]>1:
1363 self
.servicetypes
[info
.type]-=1
1365 del self
.servicetypes
[info
.type]
1368 now
= currentTimeMillis()
1373 self
.wait(nextTime
- now
)
1374 now
= currentTimeMillis()
1376 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1377 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, 0, info
.name
), 0)
1378 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
, _CLASS_IN
, 0, info
.priority
, info
.weight
, info
.port
, info
.name
), 0)
1379 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
, 0, info
.text
), 0)
1381 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, 0, info
.address
), 0)
1384 nextTime
+= _UNREGISTER_TIME
1386 def unregisterAllServices(self
):
1387 """Unregister all registered services."""
1388 if len(self
.services
) > 0:
1389 now
= currentTimeMillis()
1394 self
.wait(nextTime
- now
)
1395 now
= currentTimeMillis()
1397 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1398 for info
in self
.services
.values():
1399 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, 0, info
.name
), 0)
1400 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
, _CLASS_IN
, 0, info
.priority
, info
.weight
, info
.port
, info
.server
), 0)
1401 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
, 0, info
.text
), 0)
1403 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
, _CLASS_IN
, 0, info
.address
), 0)
1406 nextTime
+= _UNREGISTER_TIME
1408 def checkService(self
, info
):
1409 """Checks the network for a unique service name, modifying the
1410 ServiceInfo passed in if it is not unique."""
1411 now
= currentTimeMillis()
1415 for record
in self
.cache
.entriesWithName(info
.type):
1416 if record
.type == _TYPE_PTR
and not record
.isExpired(now
) and record
.alias
== info
.name
:
1417 if (info
.name
.find('.') < 0):
1418 info
.name
= info
.name
+ ".[" + info
.address
+ ":" + info
.port
+ "]." + info
.type
1419 self
.checkService(info
)
1421 raise NonUniqueNameException
1423 self
.wait(nextTime
- now
)
1424 now
= currentTimeMillis()
1426 out
= DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA
)
1428 out
.addQuestion(DNSQuestion(info
.type, _TYPE_PTR
, _CLASS_IN
))
1429 out
.addAuthorativeAnswer(DNSPointer(info
.type, _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, info
.name
))
1432 nextTime
+= _CHECK_TIME
1434 def addListener(self
, listener
, question
):
1435 """Adds a listener for a given question. The listener will have
1436 its updateRecord method called when information is available to
1437 answer the question."""
1438 now
= currentTimeMillis()
1439 self
.listeners
.append(listener
)
1440 if question
is not None:
1441 for record
in self
.cache
.entriesWithName(question
.name
):
1442 if question
.answeredBy(record
) and not record
.isExpired(now
):
1443 listener
.updateRecord(self
, now
, record
)
1446 def removeListener(self
, listener
):
1447 """Removes a listener."""
1449 self
.listeners
.remove(listener
)
1454 def updateRecord(self
, now
, rec
):
1455 """Used to notify listeners of new information that has updated
1457 for listener
in self
.listeners
:
1458 listener
.updateRecord(self
, now
, rec
)
1461 def handleResponse(self
, msg
):
1462 """Deal with incoming response packets. All answers
1463 are held in the cache, and listeners are notified."""
1464 now
= currentTimeMillis()
1465 for record
in msg
.answers
:
1466 expired
= record
.isExpired(now
)
1467 if record
in self
.cache
.entries():
1469 self
.cache
.remove(record
)
1471 entry
= self
.cache
.get(record
)
1472 if entry
is not None:
1473 entry
.resetTTL(record
)
1476 self
.cache
.add(record
)
1478 self
.updateRecord(now
, record
)
1480 def handleQuery(self
, msg
, addr
, port
):
1481 """Deal with incoming query packets. Provides a response if
1485 # Support unicast client responses
1487 if port
!= _MDNS_PORT
:
1488 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
, 0)
1489 for question
in msg
.questions
:
1490 out
.addQuestion(question
)
1492 for question
in msg
.questions
:
1493 if question
.type == _TYPE_PTR
:
1494 if question
.name
== "_services._dns-sd._udp.local.":
1495 for stype
in self
.servicetypes
.keys():
1497 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1498 out
.addAnswer(msg
, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, stype
))
1499 for service
in self
.services
.values():
1500 if question
.name
== service
.type:
1502 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1503 out
.addAnswer(msg
, DNSPointer(service
.type, _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, service
.name
))
1507 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1509 # Answer A record queries for any service addresses we know
1510 if question
.type == _TYPE_A
or question
.type == _TYPE_ANY
:
1511 for service
in self
.services
.values():
1512 if service
.server
== question
.name
.lower():
1513 out
.addAnswer(msg
, DNSAddress(question
.name
, _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.address
))
1515 service
= self
.services
.get(question
.name
.lower(), None)
1516 if not service
: continue
1518 if question
.type == _TYPE_SRV
or question
.type == _TYPE_ANY
:
1519 out
.addAnswer(msg
, DNSService(question
.name
, _TYPE_SRV
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.priority
, service
.weight
, service
.port
, service
.server
))
1520 if question
.type == _TYPE_TXT
or question
.type == _TYPE_ANY
:
1521 out
.addAnswer(msg
, DNSText(question
.name
, _TYPE_TXT
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.text
))
1522 if question
.type == _TYPE_SRV
:
1523 out
.addAdditionalAnswer(DNSAddress(service
.server
, _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
, _DNS_TTL
, service
.address
))
1525 traceback
.print_exc()
1527 if out
is not None and out
.answers
:
1529 self
.send(out
, addr
, port
)
1531 def send(self
, out
, addr
= _MDNS_ADDR
, port
= _MDNS_PORT
):
1532 """Sends an outgoing packet."""
1533 # This is a quick test to see if we can parse the packets we generate
1534 #temp = DNSIncoming(out.packet())
1536 bytes_sent
= self
.socket
.sendto(out
.packet(), 0, (addr
, port
))
1538 # Ignore this, it may be a temporary loss of network connection
1542 """Ends the background threads, and prevent this instance from
1543 servicing further queries."""
1544 if globals()['_GLOBAL_DONE'] == 0:
1545 globals()['_GLOBAL_DONE'] = 1
1547 self
.engine
.notify()
1548 self
.unregisterAllServices()
1549 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_DROP_MEMBERSHIP
, socket
.inet_aton(_MDNS_ADDR
) + socket
.inet_aton('0.0.0.0'))
1552 # Test a few module features, including service registration, service
1553 # query (for Zoe), and service unregistration.
1555 if __name__
== '__main__':
1556 print "Multicast DNS Service Discovery for Python, version", __version__
1558 print "1. Testing registration of a service..."
1559 desc
= {'version':'0.10','a':'test value', 'b':'another value'}
1560 info
= ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket
.inet_aton("127.0.0.1"), 1234, 0, 0, desc
)
1561 print " Registering service..."
1562 r
.registerService(info
)
1563 print " Registration done."
1564 print "2. Testing query of service information..."
1565 print " Getting ZOE service:", str(r
.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1566 print " Query done."
1567 print "3. Testing query of own service..."
1568 print " Getting self:", str(r
.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local."))
1569 print " Query done."
1570 print "4. Testing unregister of service information..."
1571 r
.unregisterService(info
)
1572 print " Unregister done."