1 """ Multicast DNS Service Discovery for Python, v0.13-wmcbrine
2 Copyright 2003 Paul Scott-Murphy, 2013 William McBrine
4 This module provides a framework for the use of DNS Service Discovery
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 __author__
= 'Paul Scott-Murphy'
24 __maintainer__
= 'William McBrine <wmcbrine@gmail.com>'
25 __version__
= '0.13-wmcbrine'
35 __all__
= ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
41 # Some timing constants
43 _UNREGISTER_TIME
= 125
51 _MDNS_ADDR
= '224.0.0.251'
54 _DNS_TTL
= 60 * 60; # one hour default TTL
56 _MAX_MSG_TYPICAL
= 1460 # unused
57 _MAX_MSG_ABSOLUTE
= 8972
59 _FLAGS_QR_MASK
= 0x8000 # query response mask
60 _FLAGS_QR_QUERY
= 0x0000 # query
61 _FLAGS_QR_RESPONSE
= 0x8000 # response
63 _FLAGS_AA
= 0x0400 # Authorative answer
64 _FLAGS_TC
= 0x0200 # Truncated
65 _FLAGS_RD
= 0x0100 # Recursion desired
66 _FLAGS_RA
= 0x8000 # Recursion available
68 _FLAGS_Z
= 0x0040 # Zero
69 _FLAGS_AD
= 0x0020 # Authentic data
70 _FLAGS_CD
= 0x0010 # Checking disabled
79 _CLASS_UNIQUE
= 0x8000
101 # Mapping constants to names
103 _CLASSES
= { _CLASS_IN
: "in",
107 _CLASS_NONE
: "none",
110 _TYPES
= { _TYPE_A
: "a",
114 _TYPE_CNAME
: "cname",
122 _TYPE_HINFO
: "hinfo",
123 _TYPE_MINFO
: "minfo",
126 _TYPE_AAAA
: "quada",
132 def currentTimeMillis():
133 """Current system time in milliseconds"""
134 return time
.time() * 1000
138 class NonLocalNameException(Exception):
141 class NonUniqueNameException(Exception):
144 class NamePartTooLongException(Exception):
147 class AbstractMethodException(Exception):
150 class BadTypeInNameException(Exception):
153 # implementation classes
155 class DNSEntry(object):
158 def __init__(self
, name
, type, clazz
):
159 self
.key
= name
.lower()
162 self
.clazz
= clazz
& _CLASS_MASK
163 self
.unique
= (clazz
& _CLASS_UNIQUE
) != 0
165 def __eq__(self
, other
):
166 """Equality test on name, type, and class"""
167 return (isinstance(other
, DNSEntry
) and
168 self
.name
== other
.name
and
169 self
.type == other
.type and
170 self
.clazz
== other
.clazz
)
172 def __ne__(self
, other
):
173 """Non-equality test"""
174 return not self
.__eq
__(other
)
176 def getClazz(self
, clazz
):
178 return _CLASSES
.get(clazz
, "?(%s)" % clazz
)
180 def getType(self
, t
):
182 return _TYPES
.get(t
, "?(%s)" % t
)
184 def toString(self
, hdr
, other
):
185 """String representation with additional information"""
186 result
= "%s[%s,%s" % (hdr
, self
.getType(self
.type),
187 self
.getClazz(self
.clazz
))
193 if other
is not None:
194 result
+= ",%s]" % (other
)
199 class DNSQuestion(DNSEntry
):
200 """A DNS question entry"""
202 def __init__(self
, name
, type, clazz
):
203 #if not name.endswith(".local."):
204 # raise NonLocalNameException
205 DNSEntry
.__init
__(self
, name
, type, clazz
)
207 def answeredBy(self
, rec
):
208 """Returns true if the question is answered by the record"""
209 return (self
.clazz
== rec
.clazz
and
210 (self
.type == rec
.type or self
.type == _TYPE_ANY
) and
211 self
.name
== rec
.name
)
214 """String representation"""
215 return DNSEntry
.toString(self
, "question", None)
218 class DNSRecord(DNSEntry
):
219 """A DNS record - like a DNS entry, but has a TTL"""
221 def __init__(self
, name
, type, clazz
, ttl
):
222 DNSEntry
.__init
__(self
, name
, type, clazz
)
224 self
.created
= currentTimeMillis()
226 def __eq__(self
, other
):
227 """Tests equality as per DNSRecord"""
228 return isinstance(other
, DNSRecord
) and DNSEntry
.__eq
__(self
, other
)
230 def suppressedBy(self
, msg
):
231 """Returns true if any answer in a message can suffice for the
232 information held in this record."""
233 for record
in msg
.answers
:
234 if self
.suppressedByAnswer(record
):
238 def suppressedByAnswer(self
, other
):
239 """Returns true if another record has same name, type and class,
240 and if its TTL is at least half of this record's."""
241 return self
== other
and other
.ttl
> (self
.ttl
/ 2)
243 def getExpirationTime(self
, percent
):
244 """Returns the time at which this record will have expired
245 by a certain percentage."""
246 return self
.created
+ (percent
* self
.ttl
* 10)
248 def getRemainingTTL(self
, now
):
249 """Returns the remaining TTL in seconds."""
250 return max(0, (self
.getExpirationTime(100) - now
) / 1000)
252 def isExpired(self
, now
):
253 """Returns true if this record has expired."""
254 return self
.getExpirationTime(100) <= now
256 def isStale(self
, now
):
257 """Returns true if this record is at least half way expired."""
258 return self
.getExpirationTime(50) <= now
260 def resetTTL(self
, other
):
261 """Sets this record's TTL and created time to that of
263 self
.created
= other
.created
266 def write(self
, out
):
267 """Abstract method"""
268 raise AbstractMethodException
270 def toString(self
, other
):
271 """String representation with addtional information"""
272 arg
= "%s/%s,%s" % (self
.ttl
,
273 self
.getRemainingTTL(currentTimeMillis()), other
)
274 return DNSEntry
.toString(self
, "record", arg
)
276 class DNSAddress(DNSRecord
):
277 """A DNS address record"""
279 def __init__(self
, name
, type, clazz
, ttl
, address
):
280 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
281 self
.address
= address
283 def write(self
, out
):
284 """Used in constructing an outgoing packet"""
285 out
.writeString(self
.address
)
287 def __eq__(self
, other
):
288 """Tests equality on address"""
289 return isinstance(other
, DNSAddress
) and self
.address
== other
.address
292 """String representation"""
294 return socket
.inet_ntoa(self
.address
)
298 class DNSHinfo(DNSRecord
):
299 """A DNS host information record"""
301 def __init__(self
, name
, type, clazz
, ttl
, cpu
, os
):
302 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
306 def write(self
, out
):
307 """Used in constructing an outgoing packet"""
308 out
.writeString(self
.cpu
)
309 out
.writeString(self
.oso
)
311 def __eq__(self
, other
):
312 """Tests equality on cpu and os"""
313 return (isinstance(other
, DNSHinfo
) and
314 self
.cpu
== other
.cpu
and self
.os
== other
.os
)
317 """String representation"""
318 return self
.cpu
+ " " + self
.os
320 class DNSPointer(DNSRecord
):
321 """A DNS pointer record"""
323 def __init__(self
, name
, type, clazz
, ttl
, alias
):
324 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
327 def write(self
, out
):
328 """Used in constructing an outgoing packet"""
329 out
.writeName(self
.alias
)
331 def __eq__(self
, other
):
332 """Tests equality on alias"""
333 return isinstance(other
, DNSPointer
) and self
.alias
== other
.alias
336 """String representation"""
337 return self
.toString(self
.alias
)
339 class DNSText(DNSRecord
):
340 """A DNS text record"""
342 def __init__(self
, name
, type, clazz
, ttl
, text
):
343 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
346 def write(self
, out
):
347 """Used in constructing an outgoing packet"""
348 out
.writeString(self
.text
)
350 def __eq__(self
, other
):
351 """Tests equality on text"""
352 return isinstance(other
, DNSText
) and self
.text
== other
.text
355 """String representation"""
356 if len(self
.text
) > 10:
357 return self
.toString(self
.text
[:7] + "...")
359 return self
.toString(self
.text
)
361 class DNSService(DNSRecord
):
362 """A DNS service record"""
364 def __init__(self
, name
, type, clazz
, ttl
, priority
, weight
, port
, server
):
365 DNSRecord
.__init
__(self
, name
, type, clazz
, ttl
)
366 self
.priority
= priority
371 def write(self
, out
):
372 """Used in constructing an outgoing packet"""
373 out
.writeShort(self
.priority
)
374 out
.writeShort(self
.weight
)
375 out
.writeShort(self
.port
)
376 out
.writeName(self
.server
)
378 def __eq__(self
, other
):
379 """Tests equality on priority, weight, port and server"""
380 return (isinstance(other
, DNSService
) and
381 self
.priority
== other
.priority
and
382 self
.weight
== other
.weight
and
383 self
.port
== other
.port
and
384 self
.server
== other
.server
)
387 """String representation"""
388 return self
.toString("%s:%s" % (self
.server
, self
.port
))
390 class DNSIncoming(object):
391 """Object representation of an incoming DNS packet"""
393 def __init__(self
, data
):
394 """Constructor from string holding bytes of packet"""
399 self
.numQuestions
= 0
401 self
.numAuthorities
= 0
402 self
.numAdditionals
= 0
408 def unpack(self
, format
):
409 length
= struct
.calcsize(format
)
410 info
= struct
.unpack(format
, self
.data
[self
.offset
:self
.offset
+length
])
411 self
.offset
+= length
414 def readHeader(self
):
415 """Reads header portion of packet"""
416 (self
.id, self
.flags
, self
.numQuestions
, self
.numAnswers
,
417 self
.numAuthorities
, self
.numAdditionals
) = self
.unpack('!6H')
419 def readQuestions(self
):
420 """Reads questions section of packet"""
421 for i
in xrange(self
.numQuestions
):
422 name
= self
.readName()
423 type, clazz
= self
.unpack('!HH')
425 question
= DNSQuestion(name
, type, clazz
)
426 self
.questions
.append(question
)
429 """Reads an integer from the packet"""
430 return self
.unpack('!I')[0]
432 def readCharacterString(self
):
433 """Reads a character string from the packet"""
434 length
= ord(self
.data
[self
.offset
])
436 return self
.readString(length
)
438 def readString(self
, length
):
439 """Reads a string of a given length from the packet"""
440 info
= self
.data
[self
.offset
:self
.offset
+length
]
441 self
.offset
+= length
444 def readUnsignedShort(self
):
445 """Reads an unsigned short from the packet"""
446 return self
.unpack('!H')[0]
448 def readOthers(self
):
449 """Reads the answers, authorities and additionals section of the
451 n
= self
.numAnswers
+ self
.numAuthorities
+ self
.numAdditionals
453 domain
= self
.readName()
454 type, clazz
, ttl
, length
= self
.unpack('!HHiH')
458 rec
= DNSAddress(domain
, type, clazz
, ttl
, self
.readString(4))
459 elif type == _TYPE_CNAME
or type == _TYPE_PTR
:
460 rec
= DNSPointer(domain
, type, clazz
, ttl
, self
.readName())
461 elif type == _TYPE_TXT
:
462 rec
= DNSText(domain
, type, clazz
, ttl
, self
.readString(length
))
463 elif type == _TYPE_SRV
:
464 rec
= DNSService(domain
, type, clazz
, ttl
,
465 self
.readUnsignedShort(), self
.readUnsignedShort(),
466 self
.readUnsignedShort(), self
.readName())
467 elif type == _TYPE_HINFO
:
468 rec
= DNSHinfo(domain
, type, clazz
, ttl
,
469 self
.readCharacterString(), self
.readCharacterString())
470 elif type == _TYPE_AAAA
:
471 rec
= DNSAddress(domain
, type, clazz
, ttl
, self
.readString(16))
473 # Try to ignore types we don't know about
474 # Skip the payload for the resource record so the next
475 # records can be parsed correctly
476 self
.offset
+= length
479 self
.answers
.append(rec
)
482 """Returns true if this is a query"""
483 return (self
.flags
& _FLAGS_QR_MASK
) == _FLAGS_QR_QUERY
485 def isResponse(self
):
486 """Returns true if this is a response"""
487 return (self
.flags
& _FLAGS_QR_MASK
) == _FLAGS_QR_RESPONSE
489 def readUTF(self
, offset
, length
):
490 """Reads a UTF-8 string of a given length from the packet"""
491 return unicode(self
.data
[offset
:offset
+length
], 'utf-8', 'replace')
494 """Reads a domain name from the packet"""
501 length
= ord(self
.data
[off
])
507 result
= ''.join((result
, self
.readUTF(off
, length
) + '.'))
512 off
= ((length
& 0x3F) << 8) |
ord(self
.data
[off
])
514 raise "Bad domain name (circular) at " + str(off
)
517 raise "Bad domain name at " + str(off
)
527 class DNSOutgoing(object):
528 """Object representation of an outgoing packet"""
530 def __init__(self
, flags
, multicast
=True):
531 self
.finished
= False
533 self
.multicast
= multicast
541 self
.authorities
= []
542 self
.additionals
= []
544 def addQuestion(self
, record
):
545 """Adds a question"""
546 self
.questions
.append(record
)
548 def addAnswer(self
, inp
, record
):
550 if not record
.suppressedBy(inp
):
551 self
.addAnswerAtTime(record
, 0)
553 def addAnswerAtTime(self
, record
, now
):
554 """Adds an answer if if does not expire by a certain time"""
555 if record
is not None:
556 if now
== 0 or not record
.isExpired(now
):
557 self
.answers
.append((record
, now
))
559 def addAuthorativeAnswer(self
, record
):
560 """Adds an authoritative answer"""
561 self
.authorities
.append(record
)
563 def addAdditionalAnswer(self
, record
):
564 """Adds an additional answer"""
565 self
.additionals
.append(record
)
567 def pack(self
, format
, value
):
568 self
.data
.append(struct
.pack(format
, value
))
569 self
.size
+= struct
.calcsize(format
)
571 def writeByte(self
, value
):
572 """Writes a single byte to the packet"""
573 self
.pack('!c', chr(value
))
575 def insertShort(self
, index
, value
):
576 """Inserts an unsigned short in a certain position in the packet"""
577 self
.data
.insert(index
, struct
.pack('!H', value
))
580 def writeShort(self
, value
):
581 """Writes an unsigned short to the packet"""
582 self
.pack('!H', value
)
584 def writeInt(self
, value
):
585 """Writes an unsigned integer to the packet"""
586 self
.pack('!I', int(value
))
588 def writeString(self
, value
):
589 """Writes a string to the packet"""
590 self
.data
.append(value
)
591 self
.size
+= len(value
)
593 def writeUTF(self
, s
):
594 """Writes a UTF-8 string of a given length to the packet"""
595 utfstr
= s
.encode('utf-8')
598 raise NamePartTooLongException
599 self
.writeByte(length
)
600 self
.writeString(utfstr
)
602 def writeName(self
, name
):
603 """Writes a domain name to the packet"""
605 if name
in self
.names
:
606 # Find existing instance of this name in packet
608 index
= self
.names
[name
]
610 # An index was found, so write a pointer to it
612 self
.writeByte((index
>> 8) |
0xC0)
613 self
.writeByte(index
& 0xFF)
615 # No record of this name already, so write it
616 # out as normal, recording the location of the name
617 # for future pointers to it.
619 self
.names
[name
] = self
.size
620 parts
= name
.split('.')
627 def writeQuestion(self
, question
):
628 """Writes a question to the packet"""
629 self
.writeName(question
.name
)
630 self
.writeShort(question
.type)
631 self
.writeShort(question
.clazz
)
633 def writeRecord(self
, record
, now
):
634 """Writes a record (answer, authoritative answer, additional) to
636 self
.writeName(record
.name
)
637 self
.writeShort(record
.type)
638 if record
.unique
and self
.multicast
:
639 self
.writeShort(record
.clazz | _CLASS_UNIQUE
)
641 self
.writeShort(record
.clazz
)
643 self
.writeInt(record
.ttl
)
645 self
.writeInt(record
.getRemainingTTL(now
))
646 index
= len(self
.data
)
647 # Adjust size for the short we will write before this record
653 length
= len(''.join(self
.data
[index
:]))
654 self
.insertShort(index
, length
) # Here is the short we adjusted for
657 """Returns a string containing the packet's bytes
659 No further parts should be added to the packet once this
661 if not self
.finished
:
663 for question
in self
.questions
:
664 self
.writeQuestion(question
)
665 for answer
, time
in self
.answers
:
666 self
.writeRecord(answer
, time
)
667 for authority
in self
.authorities
:
668 self
.writeRecord(authority
, 0)
669 for additional
in self
.additionals
:
670 self
.writeRecord(additional
, 0)
672 self
.insertShort(0, len(self
.additionals
))
673 self
.insertShort(0, len(self
.authorities
))
674 self
.insertShort(0, len(self
.answers
))
675 self
.insertShort(0, len(self
.questions
))
676 self
.insertShort(0, self
.flags
)
678 self
.insertShort(0, 0)
680 self
.insertShort(0, self
.id)
681 return ''.join(self
.data
)
684 class DNSCache(object):
685 """A cache of DNS entries"""
690 def add(self
, entry
):
693 list = self
.cache
[entry
.key
]
695 list = self
.cache
[entry
.key
] = []
698 def remove(self
, entry
):
699 """Removes an entry"""
701 list = self
.cache
[entry
.key
]
706 def get(self
, entry
):
707 """Gets an entry by key. Will return None if there is no
710 list = self
.cache
[entry
.key
]
711 return list[list.index(entry
)]
715 def getByDetails(self
, name
, type, clazz
):
716 """Gets an entry by details. Will return None if there is
717 no matching entry."""
718 entry
= DNSEntry(name
, type, clazz
)
719 return self
.get(entry
)
721 def entriesWithName(self
, name
):
722 """Returns a list of entries whose key matches the name."""
724 return self
.cache
[name
]
729 """Returns a list of all entries"""
730 def add(x
, y
): return x
+y
732 return reduce(add
, self
.cache
.values())
737 class Engine(threading
.Thread
):
738 """An engine wraps read access to sockets, allowing objects that
739 need to receive data from sockets to be called back when the
742 A reader needs a handle_read() method, which is called when the socket
743 it is interested in is ready for reading.
745 Writers are not implemented here, because we only send short
749 def __init__(self
, zc
):
750 threading
.Thread
.__init
__(self
)
752 self
.readers
= {} # maps socket to reader
754 self
.condition
= threading
.Condition()
758 while not _GLOBAL_DONE
:
759 rs
= self
.getReaders()
761 # No sockets to manage, but we wait for the timeout
762 # or addition of a socket
764 self
.condition
.acquire()
765 self
.condition
.wait(self
.timeout
)
766 self
.condition
.release()
769 rr
, wr
, er
= select
.select(rs
, [], [], self
.timeout
)
772 self
.readers
[socket
].handle_read()
774 traceback
.print_exc()
778 def getReaders(self
):
780 self
.condition
.acquire()
781 result
= self
.readers
.keys()
782 self
.condition
.release()
785 def addReader(self
, reader
, socket
):
786 self
.condition
.acquire()
787 self
.readers
[socket
] = reader
788 self
.condition
.notify()
789 self
.condition
.release()
791 def delReader(self
, socket
):
792 self
.condition
.acquire()
793 del(self
.readers
[socket
])
794 self
.condition
.notify()
795 self
.condition
.release()
798 self
.condition
.acquire()
799 self
.condition
.notify()
800 self
.condition
.release()
802 class Listener(object):
803 """A Listener is used by this module to listen on the multicast
804 group to which DNS messages are sent, allowing the implementation
805 to cache information as it arrives.
807 It requires registration with an Engine object in order to have
808 the read() method called when a socket is availble for reading."""
810 def __init__(self
, zc
):
812 self
.zc
.engine
.addReader(self
, self
.zc
.socket
)
814 def handle_read(self
):
816 data
, (addr
, port
) = self
.zc
.socket
.recvfrom(_MAX_MSG_ABSOLUTE
)
817 except socket
.error
, e
:
818 # If the socket was closed by another thread -- which happens
819 # regularly on shutdown -- an EBADF exception is thrown here.
821 if e
[0] == socket
.EBADF
:
826 msg
= DNSIncoming(data
)
828 # Always multicast responses
830 if port
== _MDNS_PORT
:
831 self
.zc
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
832 # If it's not a multicast query, reply via unicast
835 elif port
== _DNS_PORT
:
836 self
.zc
.handleQuery(msg
, addr
, port
)
837 self
.zc
.handleQuery(msg
, _MDNS_ADDR
, _MDNS_PORT
)
839 self
.zc
.handleResponse(msg
)
842 class Reaper(threading
.Thread
):
843 """A Reaper is used by this module to remove cache entries that
846 def __init__(self
, zc
):
847 threading
.Thread
.__init
__(self
)
853 self
.zc
.wait(10 * 1000)
856 now
= currentTimeMillis()
857 for record
in self
.zc
.cache
.entries():
858 if record
.isExpired(now
):
859 self
.zc
.updateRecord(now
, record
)
860 self
.zc
.cache
.remove(record
)
863 class ServiceBrowser(threading
.Thread
):
864 """Used to browse for a service of a specific type.
866 The listener object will have its addService() and
867 removeService() methods called when this browser
868 discovers changes in the services availability."""
870 def __init__(self
, zc
, type, listener
):
871 """Creates a browser for a specific type"""
872 threading
.Thread
.__init
__(self
)
875 self
.listener
= listener
877 self
.nextTime
= currentTimeMillis()
878 self
.delay
= _BROWSER_TIME
883 self
.zc
.addListener(self
, DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
886 def updateRecord(self
, zc
, now
, record
):
887 """Callback invoked by Zeroconf when new information arrives.
889 Updates information required by browser in the Zeroconf cache."""
890 if record
.type == _TYPE_PTR
and record
.name
== self
.type:
891 expired
= record
.isExpired(now
)
893 oldrecord
= self
.services
[record
.alias
.lower()]
895 oldrecord
.resetTTL(record
)
897 del(self
.services
[record
.alias
.lower()])
898 callback
= lambda x
: self
.listener
.removeService(x
,
899 self
.type, record
.alias
)
900 self
.list.append(callback
)
904 self
.services
[record
.alias
.lower()] = record
905 callback
= lambda x
: self
.listener
.addService(x
,
906 self
.type, record
.alias
)
907 self
.list.append(callback
)
909 expires
= record
.getExpirationTime(75)
910 if expires
< self
.nextTime
:
911 self
.nextTime
= expires
920 now
= currentTimeMillis()
921 if len(self
.list) == 0 and self
.nextTime
> now
:
922 self
.zc
.wait(self
.nextTime
- now
)
923 if _GLOBAL_DONE
or self
.done
:
925 now
= currentTimeMillis()
927 if self
.nextTime
<= now
:
928 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
929 out
.addQuestion(DNSQuestion(self
.type, _TYPE_PTR
, _CLASS_IN
))
930 for record
in self
.services
.values():
931 if not record
.isExpired(now
):
932 out
.addAnswerAtTime(record
, now
)
934 self
.nextTime
= now
+ self
.delay
935 self
.delay
= min(20 * 1000, self
.delay
* 2)
937 if len(self
.list) > 0:
938 event
= self
.list.pop(0)
940 if event
is not None:
944 class ServiceInfo(object):
945 """Service information"""
947 def __init__(self
, type, name
, address
=None, port
=None, weight
=0,
948 priority
=0, properties
=None, server
=None):
949 """Create a service description.
951 type: fully qualified service type name
952 name: fully qualified service name
953 address: IP address as unsigned short, network byte order
954 port: port that the service runs on
955 weight: weight of the service
956 priority: priority of the service
957 properties: dictionary of properties (or a string holding the
958 bytes for the text field)
959 server: fully qualified name for service host (defaults to name)"""
961 if not name
.endswith(type):
962 raise BadTypeInNameException
965 self
.address
= address
968 self
.priority
= priority
973 self
.setProperties(properties
)
975 def setProperties(self
, properties
):
976 """Sets properties and text of this info from a dictionary"""
977 if isinstance(properties
, dict):
978 self
.properties
= properties
981 for key
in properties
:
982 value
= properties
[key
]
984 suffix
= ''.encode('utf-8')
985 elif isinstance(value
, str):
986 suffix
= value
.encode('utf-8')
987 elif isinstance(value
, int):
993 suffix
= ''.encode('utf-8')
994 list.append('='.join((key
, suffix
)))
996 result
= ''.join((result
, chr(len(item
)), item
))
999 self
.text
= properties
1001 def setText(self
, text
):
1002 """Sets properties and text given a text field"""
1010 length
= ord(text
[index
])
1012 strs
.append(text
[index
:index
+length
])
1017 key
, value
= s
.split('=', 1)
1020 elif value
== 'false' or not value
:
1023 # No equals sign at all
1027 # Only update non-existent properties
1028 if key
and result
.get(key
) == None:
1031 self
.properties
= result
1033 traceback
.print_exc()
1034 self
.properties
= None
1042 if self
.type is not None and self
.name
.endswith("." + self
.type):
1043 return self
.name
[:len(self
.name
) - len(self
.type) - 1]
1046 def getAddress(self
):
1047 """Address accessor"""
1054 def getPriority(self
):
1055 """Pirority accessor"""
1056 return self
.priority
1058 def getWeight(self
):
1059 """Weight accessor"""
1062 def getProperties(self
):
1063 """Properties accessor"""
1064 return self
.properties
1070 def getServer(self
):
1071 """Server accessor"""
1074 def updateRecord(self
, zc
, now
, record
):
1075 """Updates service information from a DNS record"""
1076 if record
is not None and not record
.isExpired(now
):
1077 if record
.type == _TYPE_A
:
1078 #if record.name == self.name:
1079 if record
.name
== self
.server
:
1080 self
.address
= record
.address
1081 elif record
.type == _TYPE_SRV
:
1082 if record
.name
== self
.name
:
1083 self
.server
= record
.server
1084 self
.port
= record
.port
1085 self
.weight
= record
.weight
1086 self
.priority
= record
.priority
1087 #self.address = None
1088 self
.updateRecord(zc
, now
,
1089 zc
.cache
.getByDetails(self
.server
, _TYPE_A
, _CLASS_IN
))
1090 elif record
.type == _TYPE_TXT
:
1091 if record
.name
== self
.name
:
1092 self
.setText(record
.text
)
1094 def request(self
, zc
, timeout
):
1095 """Returns true if the service could be discovered on the
1096 network, and updates this object with details discovered.
1098 now
= currentTimeMillis()
1099 delay
= _LISTENER_TIME
1101 last
= now
+ timeout
1104 zc
.addListener(self
, DNSQuestion(self
.name
, _TYPE_ANY
, _CLASS_IN
))
1105 while (self
.server
is None or self
.address
is None or
1110 out
= DNSOutgoing(_FLAGS_QR_QUERY
)
1111 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_SRV
,
1113 out
.addAnswerAtTime(zc
.cache
.getByDetails(self
.name
,
1114 _TYPE_SRV
, _CLASS_IN
), now
)
1115 out
.addQuestion(DNSQuestion(self
.name
, _TYPE_TXT
,
1117 out
.addAnswerAtTime(zc
.cache
.getByDetails(self
.name
,
1118 _TYPE_TXT
, _CLASS_IN
), now
)
1119 if self
.server
is not None:
1120 out
.addQuestion(DNSQuestion(self
.server
,
1121 _TYPE_A
, _CLASS_IN
))
1122 out
.addAnswerAtTime(zc
.cache
.getByDetails(self
.server
,
1123 _TYPE_A
, _CLASS_IN
), now
)
1128 zc
.wait(min(next
, last
) - now
)
1129 now
= currentTimeMillis()
1132 zc
.removeListener(self
)
1136 def __eq__(self
, other
):
1137 """Tests equality of service name"""
1138 if isinstance(other
, ServiceInfo
):
1139 return other
.name
== self
.name
1142 def __ne__(self
, other
):
1143 """Non-equality test"""
1144 return not self
.__eq
__(other
)
1147 """String representation"""
1148 result
= "service[%s,%s:%s," % (self
.name
,
1149 socket
.inet_ntoa(self
.getAddress()), self
.port
)
1150 if self
.text
is None:
1153 if len(self
.text
) < 20:
1156 result
+= self
.text
[:17] + "..."
1161 class Zeroconf(object):
1162 """Implementation of Zeroconf Multicast DNS Service Discovery
1164 Supports registration, unregistration, queries and browsing.
1166 def __init__(self
, bindaddress
=None):
1167 """Creates an instance of the Zeroconf class, establishing
1168 multicast communications, listening and reaping threads."""
1170 _GLOBAL_DONE
= False
1171 if bindaddress
is None:
1173 s
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
1174 s
.connect(('4.2.2.1', 123))
1175 self
.intf
= s
.getsockname()[0]
1177 self
.intf
= socket
.gethostbyname(socket
.gethostname())
1179 self
.intf
= bindaddress
1180 self
.group
= ('', _MDNS_PORT
)
1181 self
.socket
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
1183 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
, 1)
1184 self
.socket
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEPORT
, 1)
1186 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1187 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1188 # Volume 2"), but some BSD-derived systems require
1189 # SO_REUSEPORT to be specified explicity. Also, not all
1190 # versions of Python have SO_REUSEPORT available. So
1191 # if you're on a BSD-based system, and haven't upgraded
1192 # to Python 2.3 yet, you may find this library doesn't
1196 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_TTL
, 255)
1197 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_MULTICAST_LOOP
, 1)
1199 self
.socket
.bind(self
.group
)
1201 # Some versions of linux raise an exception even though
1202 # the SO_REUSE* options have been set, so ignore it
1205 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF,
1206 # socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1207 self
.socket
.setsockopt(socket
.SOL_IP
, socket
.IP_ADD_MEMBERSHIP
,
1208 socket
.inet_aton(_MDNS_ADDR
) + socket
.inet_aton('0.0.0.0'))
1213 self
.servicetypes
= {}
1215 self
.cache
= DNSCache()
1217 self
.condition
= threading
.Condition()
1219 self
.engine
= Engine(self
)
1220 self
.listener
= Listener(self
)
1221 self
.reaper
= Reaper(self
)
1223 def isLoopback(self
):
1224 return self
.intf
.startswith("127.0.0.1")
1226 def isLinklocal(self
):
1227 return self
.intf
.startswith("169.254.")
1229 def wait(self
, timeout
):
1230 """Calling thread waits for a given number of milliseconds or
1232 self
.condition
.acquire()
1233 self
.condition
.wait(timeout
/1000)
1234 self
.condition
.release()
1236 def notifyAll(self
):
1237 """Notifies all waiting threads"""
1238 self
.condition
.acquire()
1239 self
.condition
.notifyAll()
1240 self
.condition
.release()
1242 def getServiceInfo(self
, type, name
, timeout
=3000):
1243 """Returns network's service information for a particular
1244 name and type, or None if no service matches by the timeout,
1245 which defaults to 3 seconds."""
1246 info
= ServiceInfo(type, name
)
1247 if info
.request(self
, timeout
):
1251 def addServiceListener(self
, type, listener
):
1252 """Adds a listener for a particular service type. This object
1253 will then have its updateRecord method called when information
1254 arrives for that type."""
1255 self
.removeServiceListener(listener
)
1256 self
.browsers
.append(ServiceBrowser(self
, type, listener
))
1258 def removeServiceListener(self
, listener
):
1259 """Removes a listener from the set that is currently listening."""
1260 for browser
in self
.browsers
:
1261 if browser
.listener
== listener
:
1265 def registerService(self
, info
, ttl
=_DNS_TTL
):
1266 """Registers service information to the network with a default TTL
1267 of 60 seconds. Zeroconf will then respond to requests for
1268 information for that service. The name of the service may be
1269 changed if needed to make it unique on the network."""
1270 self
.checkService(info
)
1271 self
.services
[info
.name
.lower()] = info
1272 if info
.type in self
.servicetypes
:
1273 self
.servicetypes
[info
.type]+=1
1275 self
.servicetypes
[info
.type]=1
1276 now
= currentTimeMillis()
1281 self
.wait(nextTime
- now
)
1282 now
= currentTimeMillis()
1284 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1285 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
,
1286 _CLASS_IN
, ttl
, info
.name
), 0)
1287 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
,
1288 _CLASS_IN
, ttl
, info
.priority
, info
.weight
, info
.port
,
1290 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
,
1293 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
,
1294 _CLASS_IN
, ttl
, info
.address
), 0)
1297 nextTime
+= _REGISTER_TIME
1299 def unregisterService(self
, info
):
1300 """Unregister a service."""
1302 del(self
.services
[info
.name
.lower()])
1303 if self
.servicetypes
[info
.type]>1:
1304 self
.servicetypes
[info
.type]-=1
1306 del self
.servicetypes
[info
.type]
1309 now
= currentTimeMillis()
1314 self
.wait(nextTime
- now
)
1315 now
= currentTimeMillis()
1317 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1318 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
,
1319 _CLASS_IN
, 0, info
.name
), 0)
1320 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
,
1321 _CLASS_IN
, 0, info
.priority
, info
.weight
, info
.port
,
1323 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
, _CLASS_IN
,
1326 out
.addAnswerAtTime(DNSAddress(info
.server
, _TYPE_A
,
1327 _CLASS_IN
, 0, info
.address
), 0)
1330 nextTime
+= _UNREGISTER_TIME
1332 def unregisterAllServices(self
):
1333 """Unregister all registered services."""
1334 if len(self
.services
) > 0:
1335 now
= currentTimeMillis()
1340 self
.wait(nextTime
- now
)
1341 now
= currentTimeMillis()
1343 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1344 for info
in self
.services
.values():
1345 out
.addAnswerAtTime(DNSPointer(info
.type, _TYPE_PTR
,
1346 _CLASS_IN
, 0, info
.name
), 0)
1347 out
.addAnswerAtTime(DNSService(info
.name
, _TYPE_SRV
,
1348 _CLASS_IN
, 0, info
.priority
, info
.weight
,
1349 info
.port
, info
.server
), 0)
1350 out
.addAnswerAtTime(DNSText(info
.name
, _TYPE_TXT
,
1351 _CLASS_IN
, 0, info
.text
), 0)
1353 out
.addAnswerAtTime(DNSAddress(info
.server
,
1354 _TYPE_A
, _CLASS_IN
, 0, info
.address
), 0)
1357 nextTime
+= _UNREGISTER_TIME
1359 def checkService(self
, info
):
1360 """Checks the network for a unique service name, modifying the
1361 ServiceInfo passed in if it is not unique."""
1362 now
= currentTimeMillis()
1366 for record
in self
.cache
.entriesWithName(info
.type):
1367 if (record
.type == _TYPE_PTR
and
1368 not record
.isExpired(now
) and
1369 record
.alias
== info
.name
):
1370 if info
.name
.find('.') < 0:
1371 info
.name
= '%s.[%s:%s].%s' % (info
.name
,
1372 info
.address
, info
.port
, info
.type)
1374 self
.checkService(info
)
1376 raise NonUniqueNameException
1378 self
.wait(nextTime
- now
)
1379 now
= currentTimeMillis()
1381 out
= DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA
)
1383 out
.addQuestion(DNSQuestion(info
.type, _TYPE_PTR
, _CLASS_IN
))
1384 out
.addAuthorativeAnswer(DNSPointer(info
.type, _TYPE_PTR
,
1385 _CLASS_IN
, _DNS_TTL
, info
.name
))
1388 nextTime
+= _CHECK_TIME
1390 def addListener(self
, listener
, question
):
1391 """Adds a listener for a given question. The listener will have
1392 its updateRecord method called when information is available to
1393 answer the question."""
1394 now
= currentTimeMillis()
1395 self
.listeners
.append(listener
)
1396 if question
is not None:
1397 for record
in self
.cache
.entriesWithName(question
.name
):
1398 if question
.answeredBy(record
) and not record
.isExpired(now
):
1399 listener
.updateRecord(self
, now
, record
)
1402 def removeListener(self
, listener
):
1403 """Removes a listener."""
1405 self
.listeners
.remove(listener
)
1410 def updateRecord(self
, now
, rec
):
1411 """Used to notify listeners of new information that has updated
1413 for listener
in self
.listeners
:
1414 listener
.updateRecord(self
, now
, rec
)
1417 def handleResponse(self
, msg
):
1418 """Deal with incoming response packets. All answers
1419 are held in the cache, and listeners are notified."""
1420 now
= currentTimeMillis()
1421 for record
in msg
.answers
:
1422 expired
= record
.isExpired(now
)
1423 if record
in self
.cache
.entries():
1425 self
.cache
.remove(record
)
1427 entry
= self
.cache
.get(record
)
1428 if entry
is not None:
1429 entry
.resetTTL(record
)
1432 self
.cache
.add(record
)
1434 self
.updateRecord(now
, record
)
1436 def handleQuery(self
, msg
, addr
, port
):
1437 """Deal with incoming query packets. Provides a response if
1441 # Support unicast client responses
1443 if port
!= _MDNS_PORT
:
1444 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
, False)
1445 for question
in msg
.questions
:
1446 out
.addQuestion(question
)
1448 for question
in msg
.questions
:
1449 if question
.type == _TYPE_PTR
:
1450 if question
.name
== "_services._dns-sd._udp.local.":
1451 for stype
in self
.servicetypes
.keys():
1453 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1455 DNSPointer("_services._dns-sd._udp.local.",
1456 _TYPE_PTR
, _CLASS_IN
, _DNS_TTL
, stype
))
1457 for service
in self
.services
.values():
1458 if question
.name
== service
.type:
1460 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1462 DNSPointer(service
.type, _TYPE_PTR
,
1463 _CLASS_IN
, _DNS_TTL
, service
.name
))
1467 out
= DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA
)
1469 # Answer A record queries for any service addresses we know
1470 if question
.type in (_TYPE_A
, _TYPE_ANY
):
1471 for service
in self
.services
.values():
1472 if service
.server
== question
.name
.lower():
1473 out
.addAnswer(msg
, DNSAddress(question
.name
,
1474 _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
,
1475 _DNS_TTL
, service
.address
))
1477 service
= self
.services
.get(question
.name
.lower(), None)
1478 if not service
: continue
1480 if question
.type in (_TYPE_SRV
, _TYPE_ANY
):
1481 out
.addAnswer(msg
, DNSService(question
.name
,
1482 _TYPE_SRV
, _CLASS_IN | _CLASS_UNIQUE
,
1483 _DNS_TTL
, service
.priority
, service
.weight
,
1484 service
.port
, service
.server
))
1485 if question
.type in (_TYPE_TXT
, _TYPE_ANY
):
1486 out
.addAnswer(msg
, DNSText(question
.name
,
1487 _TYPE_TXT
, _CLASS_IN | _CLASS_UNIQUE
,
1488 _DNS_TTL
, service
.text
))
1489 if question
.type == _TYPE_SRV
:
1490 out
.addAdditionalAnswer(DNSAddress(service
.server
,
1491 _TYPE_A
, _CLASS_IN | _CLASS_UNIQUE
,
1492 _DNS_TTL
, service
.address
))
1494 traceback
.print_exc()
1496 if out
is not None and out
.answers
:
1498 self
.send(out
, addr
, port
)
1500 def send(self
, out
, addr
= _MDNS_ADDR
, port
= _MDNS_PORT
):
1501 """Sends an outgoing packet."""
1502 packet
= out
.packet()
1505 bytes_sent
= self
.socket
.sendto(packet
, 0, (addr
, port
))
1508 packet
= packet
[bytes_sent
:]
1510 # Ignore this, it may be a temporary loss of network connection
1514 """Ends the background threads, and prevent this instance from
1515 servicing further queries."""
1517 if not _GLOBAL_DONE
:
1520 self
.engine
.notify()
1521 self
.unregisterAllServices()
1522 self
.socket
.setsockopt(socket
.SOL_IP
,
1523 socket
.IP_DROP_MEMBERSHIP
,
1524 socket
.inet_aton(_MDNS_ADDR
) +
1525 socket
.inet_aton('0.0.0.0'))
1528 # Test a few module features, including service registration, service
1529 # query (for Zoe), and service unregistration.
1531 if __name__
== '__main__':
1532 print "Multicast DNS Service Discovery for Python, version", __version__
1534 print "1. Testing registration of a service..."
1535 desc
= {'version':'0.10','a':'test value', 'b':'another value'}
1536 info
= ServiceInfo("_http._tcp.local.",
1537 "My Service Name._http._tcp.local.",
1538 socket
.inet_aton("127.0.0.1"), 1234, 0, 0, desc
)
1539 print " Registering service..."
1540 r
.registerService(info
)
1541 print " Registration done."
1542 print "2. Testing query of service information..."
1543 print " Getting ZOE service:",
1544 print str(r
.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1545 print " Query done."
1546 print "3. Testing query of own service..."
1547 print " Getting self:",
1548 print str(r
.getServiceInfo("_http._tcp.local.",
1549 "My Service Name._http._tcp.local."))
1550 print " Query done."
1551 print "4. Testing unregister of service information..."
1552 r
.unregisterService(info
)
1553 print " Unregister done."