Zeroconf.py
1678 lines
| 57.0 KiB
| text/x-python
|
PythonLexer
|
r28296 | from __future__ import absolute_import, print_function | ||
|
r28295 | |||
|
r7071 | """ Multicast DNS Service Discovery for Python, v0.12 | ||
Copyright (C) 2003, Paul Scott-Murphy | ||||
This module provides a framework for the use of DNS Service Discovery | ||||
using IP multicast. It has been tested against the JRendezvous | ||||
implementation from <a href="http://strangeberry.com">StrangeBerry</a>, | ||||
and against the mDNSResponder from Mac OS X 10.3.8. | ||||
This library is free software; you can redistribute it and/or | ||||
modify it under the terms of the GNU Lesser General Public | ||||
License as published by the Free Software Foundation; either | ||||
version 2.1 of the License, or (at your option) any later version. | ||||
This library is distributed in the hope that it will be useful, | ||||
but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||||
Lesser General Public License for more details. | ||||
You should have received a copy of the GNU Lesser General Public | ||||
|
r15782 | License along with this library; if not, see | ||
<http://www.gnu.org/licenses/>. | ||||
|
r7877 | |||
|
r7071 | """ | ||
"""0.12 update - allow selection of binding interface | ||||
|
r28298 | typo fix - Thanks A. M. Kuchlingi | ||
removed all use of word 'Rendezvous' - this is an API change""" | ||||
|
r7071 | |||
"""0.11 update - correction to comments for addListener method | ||||
support for new record types seen from OS X | ||||
|
r28298 | - IPv6 address | ||
- hostinfo | ||||
ignore unknown DNS record types | ||||
fixes to name decoding | ||||
|
r28299 | works alongside other processes using port 5353 (e.g. Mac OS X) | ||
|
r28298 | tested against Mac OS X 10.3.2's mDNSResponder | ||
corrections to removal of list entries for service browser""" | ||||
|
r7071 | |||
"""0.10 update - Jonathon Paisley contributed these corrections: | ||||
always multicast replies, even when query is unicast | ||||
|
r28298 | correct a pointer encoding problem | ||
can now write records in any order | ||||
traceback shown on failure | ||||
better TXT record parsing | ||||
server is now separate from name | ||||
can cancel a service browser | ||||
|
r7071 | |||
|
r28298 | modified some unit tests to accommodate these changes""" | ||
|
r7071 | |||
"""0.09 update - remove all records on service unregistration | ||||
fix DOS security problem with readName""" | ||||
"""0.08 update - changed licensing to LGPL""" | ||||
"""0.07 update - faster shutdown on engine | ||||
pointer encoding of outgoing names | ||||
|
r28298 | ServiceBrowser now works | ||
new unit tests""" | ||||
|
r7071 | |||
"""0.06 update - small improvements with unit tests | ||||
added defined exception types | ||||
|
r28298 | new style objects | ||
fixed hostname/interface problem | ||||
fixed socket timeout problem | ||||
fixed addServiceListener() typo bug | ||||
using select() for socket reads | ||||
tested on Debian unstable with Python 2.2.2""" | ||||
|
r7071 | |||
|
r17424 | """0.05 update - ensure case insensitivity on domain names | ||
|
r7071 | support for unicast DNS queries""" | ||
"""0.04 update - added some unit tests | ||||
added __ne__ adjuncts where required | ||||
|
r28298 | ensure names end in '.local.' | ||
timeout on receiving socket for clean shutdown""" | ||||
|
r7071 | |||
__author__ = "Paul Scott-Murphy" | ||||
__email__ = "paul at scott dash murphy dot com" | ||||
__version__ = "0.12" | ||||
|
r28296 | import select | ||
import socket | ||||
|
r7071 | import string | ||
import struct | ||||
import threading | ||||
|
r28296 | import time | ||
|
r7071 | import traceback | ||
__all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"] | ||||
# hook for threads | ||||
globals()['_GLOBAL_DONE'] = 0 | ||||
# Some timing constants | ||||
_UNREGISTER_TIME = 125 | ||||
_CHECK_TIME = 175 | ||||
_REGISTER_TIME = 225 | ||||
_LISTENER_TIME = 200 | ||||
_BROWSER_TIME = 500 | ||||
# Some DNS constants | ||||
|
r7877 | |||
|
r7071 | _MDNS_ADDR = '224.0.0.251' | ||
|
r28297 | _MDNS_PORT = 5353 | ||
_DNS_PORT = 53 | ||||
_DNS_TTL = 60 * 60 # one hour default TTL | ||||
|
r7071 | |||
_MAX_MSG_TYPICAL = 1460 # unused | ||||
_MAX_MSG_ABSOLUTE = 8972 | ||||
_FLAGS_QR_MASK = 0x8000 # query response mask | ||||
_FLAGS_QR_QUERY = 0x0000 # query | ||||
_FLAGS_QR_RESPONSE = 0x8000 # response | ||||
|
r17476 | _FLAGS_AA = 0x0400 # Authoritative answer | ||
|
r7071 | _FLAGS_TC = 0x0200 # Truncated | ||
_FLAGS_RD = 0x0100 # Recursion desired | ||||
_FLAGS_RA = 0x8000 # Recursion available | ||||
_FLAGS_Z = 0x0040 # Zero | ||||
_FLAGS_AD = 0x0020 # Authentic data | ||||
_FLAGS_CD = 0x0010 # Checking disabled | ||||
_CLASS_IN = 1 | ||||
_CLASS_CS = 2 | ||||
_CLASS_CH = 3 | ||||
_CLASS_HS = 4 | ||||
_CLASS_NONE = 254 | ||||
_CLASS_ANY = 255 | ||||
_CLASS_MASK = 0x7FFF | ||||
_CLASS_UNIQUE = 0x8000 | ||||
_TYPE_A = 1 | ||||
_TYPE_NS = 2 | ||||
_TYPE_MD = 3 | ||||
_TYPE_MF = 4 | ||||
_TYPE_CNAME = 5 | ||||
_TYPE_SOA = 6 | ||||
_TYPE_MB = 7 | ||||
_TYPE_MG = 8 | ||||
_TYPE_MR = 9 | ||||
_TYPE_NULL = 10 | ||||
_TYPE_WKS = 11 | ||||
_TYPE_PTR = 12 | ||||
_TYPE_HINFO = 13 | ||||
_TYPE_MINFO = 14 | ||||
_TYPE_MX = 15 | ||||
_TYPE_TXT = 16 | ||||
_TYPE_AAAA = 28 | ||||
_TYPE_SRV = 33 | ||||
|
r27637 | _TYPE_ANY = 255 | ||
|
r7071 | |||
# Mapping constants to names | ||||
_CLASSES = { _CLASS_IN : "in", | ||||
|
r28298 | _CLASS_CS : "cs", | ||
_CLASS_CH : "ch", | ||||
_CLASS_HS : "hs", | ||||
_CLASS_NONE : "none", | ||||
_CLASS_ANY : "any" } | ||||
|
r7071 | |||
_TYPES = { _TYPE_A : "a", | ||||
|
r28298 | _TYPE_NS : "ns", | ||
_TYPE_MD : "md", | ||||
_TYPE_MF : "mf", | ||||
_TYPE_CNAME : "cname", | ||||
_TYPE_SOA : "soa", | ||||
_TYPE_MB : "mb", | ||||
_TYPE_MG : "mg", | ||||
_TYPE_MR : "mr", | ||||
_TYPE_NULL : "null", | ||||
_TYPE_WKS : "wks", | ||||
_TYPE_PTR : "ptr", | ||||
_TYPE_HINFO : "hinfo", | ||||
_TYPE_MINFO : "minfo", | ||||
_TYPE_MX : "mx", | ||||
_TYPE_TXT : "txt", | ||||
_TYPE_AAAA : "quada", | ||||
_TYPE_SRV : "srv", | ||||
_TYPE_ANY : "any" } | ||||
|
r7071 | |||
# utility functions | ||||
def currentTimeMillis(): | ||||
|
r28298 | """Current system time in milliseconds""" | ||
return time.time() * 1000 | ||||
|
r7071 | |||
# Exceptions | ||||
class NonLocalNameException(Exception): | ||||
|
r28298 | pass | ||
|
r7071 | |||
class NonUniqueNameException(Exception): | ||||
|
r28298 | pass | ||
|
r7071 | |||
class NamePartTooLongException(Exception): | ||||
|
r28298 | pass | ||
|
r7071 | |||
class AbstractMethodException(Exception): | ||||
|
r28298 | pass | ||
|
r7071 | |||
class BadTypeInNameException(Exception): | ||||
|
r28298 | pass | ||
|
r7071 | |||
|
r11435 | class BadDomainName(Exception): | ||
|
r28298 | def __init__(self, pos): | ||
Exception.__init__(self, "at position %s" % pos) | ||||
|
r11435 | |||
class BadDomainNameCircular(BadDomainName): | ||||
|
r28298 | pass | ||
|
r11435 | |||
|
r7071 | # implementation classes | ||
class DNSEntry(object): | ||||
|
r28298 | """A DNS entry""" | ||
|
r7877 | |||
|
r28298 | def __init__(self, name, type, clazz): | ||
self.key = string.lower(name) | ||||
self.name = name | ||||
self.type = type | ||||
self.clazz = clazz & _CLASS_MASK | ||||
self.unique = (clazz & _CLASS_UNIQUE) != 0 | ||||
|
r7071 | |||
|
r28298 | def __eq__(self, other): | ||
"""Equality test on name, type, and class""" | ||||
if isinstance(other, DNSEntry): | ||||
|
r28299 | return (self.name == other.name and self.type == other.type and | ||
self.clazz == other.clazz) | ||||
|
r28298 | return 0 | ||
|
r7071 | |||
|
r28298 | def __ne__(self, other): | ||
"""Non-equality test""" | ||||
return not self.__eq__(other) | ||||
|
r7071 | |||
|
r28298 | def getClazz(self, clazz): | ||
"""Class accessor""" | ||||
try: | ||||
return _CLASSES[clazz] | ||||
except KeyError: | ||||
return "?(%s)" % (clazz) | ||||
|
r7071 | |||
|
r28298 | def getType(self, type): | ||
"""Type accessor""" | ||||
try: | ||||
return _TYPES[type] | ||||
except KeyError: | ||||
return "?(%s)" % (type) | ||||
|
r7071 | |||
|
r28298 | def toString(self, hdr, other): | ||
"""String representation with additional information""" | ||||
|
r28299 | result = ("%s[%s,%s" % | ||
(hdr, self.getType(self.type), self.getClazz(self.clazz))) | ||||
|
r28298 | if self.unique: | ||
result += "-unique," | ||||
else: | ||||
result += "," | ||||
result += self.name | ||||
if other is not None: | ||||
result += ",%s]" % (other) | ||||
else: | ||||
result += "]" | ||||
return result | ||||
|
r7071 | |||
class DNSQuestion(DNSEntry): | ||||
|
r28298 | """A DNS question entry""" | ||
|
r7877 | |||
|
r28298 | def __init__(self, name, type, clazz): | ||
if not name.endswith(".local."): | ||||
raise NonLocalNameException(name) | ||||
DNSEntry.__init__(self, name, type, clazz) | ||||
|
r7071 | |||
|
r28298 | def answeredBy(self, rec): | ||
"""Returns true if the question is answered by the record""" | ||||
|
r28299 | return (self.clazz == rec.clazz and | ||
(self.type == rec.type or self.type == _TYPE_ANY) and | ||||
self.name == rec.name) | ||||
|
r7071 | |||
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
return DNSEntry.toString(self, "question", None) | ||||
|
r7071 | |||
class DNSRecord(DNSEntry): | ||||
|
r28298 | """A DNS record - like a DNS entry, but has a TTL""" | ||
|
r7877 | |||
|
r28298 | def __init__(self, name, type, clazz, ttl): | ||
DNSEntry.__init__(self, name, type, clazz) | ||||
self.ttl = ttl | ||||
self.created = currentTimeMillis() | ||||
|
r7071 | |||
|
r28298 | def __eq__(self, other): | ||
"""Tests equality as per DNSRecord""" | ||||
if isinstance(other, DNSRecord): | ||||
return DNSEntry.__eq__(self, other) | ||||
return 0 | ||||
|
r7071 | |||
|
r28298 | def suppressedBy(self, msg): | ||
"""Returns true if any answer in a message can suffice for the | ||||
information held in this record.""" | ||||
for record in msg.answers: | ||||
if self.suppressedByAnswer(record): | ||||
return 1 | ||||
return 0 | ||||
|
r7071 | |||
|
r28298 | def suppressedByAnswer(self, other): | ||
"""Returns true if another record has same name, type and class, | ||||
and if its TTL is at least half of this record's.""" | ||||
if self == other and other.ttl > (self.ttl / 2): | ||||
return 1 | ||||
return 0 | ||||
|
r7071 | |||
|
r28298 | def getExpirationTime(self, percent): | ||
"""Returns the time at which this record will have expired | ||||
by a certain percentage.""" | ||||
return self.created + (percent * self.ttl * 10) | ||||
|
r7071 | |||
|
r28298 | def getRemainingTTL(self, now): | ||
"""Returns the remaining TTL in seconds.""" | ||||
return max(0, (self.getExpirationTime(100) - now) / 1000) | ||||
|
r7071 | |||
|
r28298 | def isExpired(self, now): | ||
"""Returns true if this record has expired.""" | ||||
return self.getExpirationTime(100) <= now | ||||
|
r7071 | |||
|
r28298 | def isStale(self, now): | ||
"""Returns true if this record is at least half way expired.""" | ||||
return self.getExpirationTime(50) <= now | ||||
|
r7071 | |||
|
r28298 | def resetTTL(self, other): | ||
"""Sets this record's TTL and created time to that of | ||||
another record.""" | ||||
self.created = other.created | ||||
self.ttl = other.ttl | ||||
|
r7071 | |||
|
r28298 | def write(self, out): | ||
"""Abstract method""" | ||||
raise AbstractMethodException | ||||
|
r7071 | |||
|
r28298 | def toString(self, other): | ||
"""String representation with additional information""" | ||||
|
r28299 | arg = ("%s/%s,%s" % | ||
(self.ttl, self.getRemainingTTL(currentTimeMillis()), other)) | ||||
|
r28298 | return DNSEntry.toString(self, "record", arg) | ||
|
r7071 | |||
class DNSAddress(DNSRecord): | ||||
|
r28298 | """A DNS address record""" | ||
|
r7877 | |||
|
r28298 | def __init__(self, name, type, clazz, ttl, address): | ||
DNSRecord.__init__(self, name, type, clazz, ttl) | ||||
self.address = address | ||||
|
r7071 | |||
|
r28298 | def write(self, out): | ||
"""Used in constructing an outgoing packet""" | ||||
out.writeString(self.address, len(self.address)) | ||||
|
r7071 | |||
|
r28298 | def __eq__(self, other): | ||
"""Tests equality on address""" | ||||
if isinstance(other, DNSAddress): | ||||
return self.address == other.address | ||||
return 0 | ||||
|
r7071 | |||
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
try: | ||||
return socket.inet_ntoa(self.address) | ||||
except Exception: | ||||
return self.address | ||||
|
r7071 | |||
class DNSHinfo(DNSRecord): | ||||
|
r28298 | """A DNS host information record""" | ||
|
r7071 | |||
|
r28298 | def __init__(self, name, type, clazz, ttl, cpu, os): | ||
DNSRecord.__init__(self, name, type, clazz, ttl) | ||||
self.cpu = cpu | ||||
self.os = os | ||||
|
r7071 | |||
|
r28298 | def write(self, out): | ||
"""Used in constructing an outgoing packet""" | ||||
out.writeString(self.cpu, len(self.cpu)) | ||||
out.writeString(self.os, len(self.os)) | ||||
|
r7071 | |||
|
r28298 | def __eq__(self, other): | ||
"""Tests equality on cpu and os""" | ||||
if isinstance(other, DNSHinfo): | ||||
return self.cpu == other.cpu and self.os == other.os | ||||
return 0 | ||||
|
r7071 | |||
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
return self.cpu + " " + self.os | ||||
|
r7877 | |||
|
r7071 | class DNSPointer(DNSRecord): | ||
|
r28298 | """A DNS pointer record""" | ||
|
r7877 | |||
|
r28298 | def __init__(self, name, type, clazz, ttl, alias): | ||
DNSRecord.__init__(self, name, type, clazz, ttl) | ||||
self.alias = alias | ||||
|
r7071 | |||
|
r28298 | def write(self, out): | ||
"""Used in constructing an outgoing packet""" | ||||
out.writeName(self.alias) | ||||
|
r7071 | |||
|
r28298 | def __eq__(self, other): | ||
"""Tests equality on alias""" | ||||
if isinstance(other, DNSPointer): | ||||
return self.alias == other.alias | ||||
return 0 | ||||
|
r7071 | |||
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
return self.toString(self.alias) | ||||
|
r7071 | |||
class DNSText(DNSRecord): | ||||
|
r28298 | """A DNS text record""" | ||
|
r7877 | |||
|
r28298 | def __init__(self, name, type, clazz, ttl, text): | ||
DNSRecord.__init__(self, name, type, clazz, ttl) | ||||
self.text = text | ||||
|
r7071 | |||
|
r28298 | def write(self, out): | ||
"""Used in constructing an outgoing packet""" | ||||
out.writeString(self.text, len(self.text)) | ||||
|
r7071 | |||
|
r28298 | def __eq__(self, other): | ||
"""Tests equality on text""" | ||||
if isinstance(other, DNSText): | ||||
return self.text == other.text | ||||
return 0 | ||||
|
r7071 | |||
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
if len(self.text) > 10: | ||||
return self.toString(self.text[:7] + "...") | ||||
else: | ||||
return self.toString(self.text) | ||||
|
r7071 | |||
class DNSService(DNSRecord): | ||||
|
r28298 | """A DNS service record""" | ||
|
r7877 | |||
|
r28298 | def __init__(self, name, type, clazz, ttl, priority, weight, port, server): | ||
DNSRecord.__init__(self, name, type, clazz, ttl) | ||||
self.priority = priority | ||||
self.weight = weight | ||||
self.port = port | ||||
self.server = server | ||||
|
r7071 | |||
|
r28298 | def write(self, out): | ||
"""Used in constructing an outgoing packet""" | ||||
out.writeShort(self.priority) | ||||
out.writeShort(self.weight) | ||||
out.writeShort(self.port) | ||||
out.writeName(self.server) | ||||
|
r7071 | |||
|
r28298 | def __eq__(self, other): | ||
"""Tests equality on priority, weight, port and server""" | ||||
if isinstance(other, DNSService): | ||||
|
r28299 | return (self.priority == other.priority and | ||
self.weight == other.weight and | ||||
self.port == other.port and | ||||
self.server == other.server) | ||||
|
r28298 | return 0 | ||
|
r7071 | |||
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
return self.toString("%s:%s" % (self.server, self.port)) | ||||
|
r7071 | |||
class DNSIncoming(object): | ||||
|
r28298 | """Object representation of an incoming DNS packet""" | ||
|
r7877 | |||
|
r28298 | def __init__(self, data): | ||
"""Constructor from string holding bytes of packet""" | ||||
self.offset = 0 | ||||
self.data = data | ||||
self.questions = [] | ||||
self.answers = [] | ||||
self.numQuestions = 0 | ||||
self.numAnswers = 0 | ||||
self.numAuthorities = 0 | ||||
self.numAdditionals = 0 | ||||
|
r7877 | |||
|
r28298 | self.readHeader() | ||
self.readQuestions() | ||||
self.readOthers() | ||||
|
r7071 | |||
|
r28298 | def readHeader(self): | ||
"""Reads header portion of packet""" | ||||
format = '!HHHHHH' | ||||
length = struct.calcsize(format) | ||||
info = struct.unpack(format, self.data[self.offset:self.offset+length]) | ||||
self.offset += length | ||||
|
r7071 | |||
|
r28298 | self.id = info[0] | ||
self.flags = info[1] | ||||
self.numQuestions = info[2] | ||||
self.numAnswers = info[3] | ||||
self.numAuthorities = info[4] | ||||
self.numAdditionals = info[5] | ||||
|
r7071 | |||
|
r28298 | def readQuestions(self): | ||
"""Reads questions section of packet""" | ||||
format = '!HH' | ||||
length = struct.calcsize(format) | ||||
for i in range(0, self.numQuestions): | ||||
name = self.readName() | ||||
|
r28299 | info = struct.unpack(format, | ||
self.data[self.offset:self.offset+length]) | ||||
|
r28298 | self.offset += length | ||
|
r7877 | |||
|
r28298 | try: | ||
question = DNSQuestion(name, info[0], info[1]) | ||||
self.questions.append(question) | ||||
except NonLocalNameException: | ||||
pass | ||||
|
r7071 | |||
|
r28298 | def readInt(self): | ||
"""Reads an integer from the packet""" | ||||
format = '!I' | ||||
length = struct.calcsize(format) | ||||
info = struct.unpack(format, self.data[self.offset:self.offset+length]) | ||||
self.offset += length | ||||
return info[0] | ||||
|
r7071 | |||
|
r28298 | def readCharacterString(self): | ||
"""Reads a character string from the packet""" | ||||
length = ord(self.data[self.offset]) | ||||
self.offset += 1 | ||||
return self.readString(length) | ||||
|
r7071 | |||
|
r28298 | def readString(self, len): | ||
"""Reads a string of a given length from the packet""" | ||||
format = '!' + str(len) + 's' | ||||
length = struct.calcsize(format) | ||||
info = struct.unpack(format, self.data[self.offset:self.offset+length]) | ||||
self.offset += length | ||||
return info[0] | ||||
|
r7071 | |||
|
r28298 | def readUnsignedShort(self): | ||
"""Reads an unsigned short from the packet""" | ||||
format = '!H' | ||||
length = struct.calcsize(format) | ||||
info = struct.unpack(format, self.data[self.offset:self.offset+length]) | ||||
self.offset += length | ||||
return info[0] | ||||
|
r7071 | |||
|
r28298 | def readOthers(self): | ||
|
r28299 | """Reads answers, authorities and additionals section of the packet""" | ||
|
r28298 | format = '!HHiH' | ||
length = struct.calcsize(format) | ||||
n = self.numAnswers + self.numAuthorities + self.numAdditionals | ||||
for i in range(0, n): | ||||
domain = self.readName() | ||||
|
r28299 | info = struct.unpack(format, | ||
self.data[self.offset:self.offset+length]) | ||||
|
r28298 | self.offset += length | ||
|
r7071 | |||
|
r28298 | rec = None | ||
if info[0] == _TYPE_A: | ||||
|
r28299 | rec = DNSAddress(domain, info[0], info[1], info[2], | ||
self.readString(4)) | ||||
|
r28298 | elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR: | ||
|
r28299 | rec = DNSPointer(domain, info[0], info[1], info[2], | ||
self.readName()) | ||||
|
r28298 | elif info[0] == _TYPE_TXT: | ||
|
r28299 | rec = DNSText(domain, info[0], info[1], info[2], | ||
self.readString(info[3])) | ||||
|
r28298 | elif info[0] == _TYPE_SRV: | ||
|
r28299 | rec = DNSService(domain, info[0], info[1], info[2], | ||
self.readUnsignedShort(), | ||||
self.readUnsignedShort(), | ||||
self.readUnsignedShort(), | ||||
self.readName()) | ||||
|
r28298 | elif info[0] == _TYPE_HINFO: | ||
|
r28299 | rec = DNSHinfo(domain, info[0], info[1], info[2], | ||
self.readCharacterString(), | ||||
self.readCharacterString()) | ||||
|
r28298 | elif info[0] == _TYPE_AAAA: | ||
|
r28299 | rec = DNSAddress(domain, info[0], info[1], info[2], | ||
self.readString(16)) | ||||
|
r28298 | else: | ||
# Try to ignore types we don't know about | ||||
# this may mean the rest of the name is | ||||
# unable to be parsed, and may show errors | ||||
# so this is left for debugging. New types | ||||
# encountered need to be parsed properly. | ||||
# | ||||
#print "UNKNOWN TYPE = " + str(info[0]) | ||||
#raise BadTypeInNameException | ||||
self.offset += info[3] | ||||
|
r7071 | |||
|
r28298 | if rec is not None: | ||
self.answers.append(rec) | ||||
|
r7877 | |||
|
r28298 | def isQuery(self): | ||
"""Returns true if this is a query""" | ||||
return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY | ||||
|
r7071 | |||
|
r28298 | def isResponse(self): | ||
"""Returns true if this is a response""" | ||||
return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE | ||||
|
r7071 | |||
|
r28298 | def readUTF(self, offset, len): | ||
"""Reads a UTF-8 string of a given length from the packet""" | ||||
return self.data[offset:offset+len].decode('utf-8') | ||||
|
r7877 | |||
|
r28298 | def readName(self): | ||
"""Reads a domain name from the packet""" | ||||
result = '' | ||||
off = self.offset | ||||
next = -1 | ||||
first = off | ||||
|
r7071 | |||
|
r28298 | while True: | ||
len = ord(self.data[off]) | ||||
off += 1 | ||||
if len == 0: | ||||
break | ||||
t = len & 0xC0 | ||||
if t == 0x00: | ||||
result = ''.join((result, self.readUTF(off, len) + '.')) | ||||
off += len | ||||
elif t == 0xC0: | ||||
if next < 0: | ||||
next = off + 1 | ||||
off = ((len & 0x3F) << 8) | ord(self.data[off]) | ||||
if off >= first: | ||||
raise BadDomainNameCircular(off) | ||||
first = off | ||||
else: | ||||
raise BadDomainName(off) | ||||
|
r7071 | |||
|
r28298 | if next >= 0: | ||
self.offset = next | ||||
else: | ||||
self.offset = off | ||||
|
r7071 | |||
|
r28298 | return result | ||
|
r7877 | |||
|
r7071 | class DNSOutgoing(object): | ||
|
r28298 | """Object representation of an outgoing packet""" | ||
|
r7877 | |||
|
r28298 | def __init__(self, flags, multicast = 1): | ||
self.finished = 0 | ||||
self.id = 0 | ||||
self.multicast = multicast | ||||
self.flags = flags | ||||
self.names = {} | ||||
self.data = [] | ||||
self.size = 12 | ||||
|
r7877 | |||
|
r28298 | self.questions = [] | ||
self.answers = [] | ||||
self.authorities = [] | ||||
self.additionals = [] | ||||
|
r7071 | |||
|
r28298 | def addQuestion(self, record): | ||
"""Adds a question""" | ||||
self.questions.append(record) | ||||
|
r7071 | |||
|
r28298 | def addAnswer(self, inp, record): | ||
"""Adds an answer""" | ||||
if not record.suppressedBy(inp): | ||||
self.addAnswerAtTime(record, 0) | ||||
|
r7071 | |||
|
r28298 | def addAnswerAtTime(self, record, now): | ||
"""Adds an answer if if does not expire by a certain time""" | ||||
if record is not None: | ||||
if now == 0 or not record.isExpired(now): | ||||
self.answers.append((record, now)) | ||||
|
r7071 | |||
|
r28298 | def addAuthoritativeAnswer(self, record): | ||
"""Adds an authoritative answer""" | ||||
self.authorities.append(record) | ||||
|
r7071 | |||
|
r28298 | def addAdditionalAnswer(self, record): | ||
"""Adds an additional answer""" | ||||
self.additionals.append(record) | ||||
|
r7071 | |||
|
r28298 | def writeByte(self, value): | ||
"""Writes a single byte to the packet""" | ||||
format = '!c' | ||||
self.data.append(struct.pack(format, chr(value))) | ||||
self.size += 1 | ||||
|
r7071 | |||
|
r28298 | def insertShort(self, index, value): | ||
"""Inserts an unsigned short in a certain position in the packet""" | ||||
format = '!H' | ||||
self.data.insert(index, struct.pack(format, value)) | ||||
self.size += 2 | ||||
|
r7877 | |||
|
r28298 | def writeShort(self, value): | ||
"""Writes an unsigned short to the packet""" | ||||
format = '!H' | ||||
self.data.append(struct.pack(format, value)) | ||||
self.size += 2 | ||||
|
r7071 | |||
|
r28298 | def writeInt(self, value): | ||
"""Writes an unsigned integer to the packet""" | ||||
format = '!I' | ||||
self.data.append(struct.pack(format, int(value))) | ||||
self.size += 4 | ||||
|
r7071 | |||
|
r28298 | def writeString(self, value, length): | ||
"""Writes a string to the packet""" | ||||
format = '!' + str(length) + 's' | ||||
self.data.append(struct.pack(format, value)) | ||||
self.size += length | ||||
|
r7071 | |||
|
r28298 | def writeUTF(self, s): | ||
"""Writes a UTF-8 string of a given length to the packet""" | ||||
utfstr = s.encode('utf-8') | ||||
length = len(utfstr) | ||||
if length > 64: | ||||
raise NamePartTooLongException | ||||
self.writeByte(length) | ||||
self.writeString(utfstr, length) | ||||
|
r7071 | |||
|
r28298 | def writeName(self, name): | ||
"""Writes a domain name to the packet""" | ||||
|
r7071 | |||
|
r28298 | try: | ||
# Find existing instance of this name in packet | ||||
# | ||||
index = self.names[name] | ||||
except KeyError: | ||||
# No record of this name already, so write it | ||||
# out as normal, recording the location of the name | ||||
# for future pointers to it. | ||||
# | ||||
self.names[name] = self.size | ||||
parts = name.split('.') | ||||
if parts[-1] == '': | ||||
parts = parts[:-1] | ||||
for part in parts: | ||||
self.writeUTF(part) | ||||
self.writeByte(0) | ||||
return | ||||
|
r7071 | |||
|
r28298 | # An index was found, so write a pointer to it | ||
# | ||||
self.writeByte((index >> 8) | 0xC0) | ||||
self.writeByte(index) | ||||
|
r7071 | |||
|
r28298 | def writeQuestion(self, question): | ||
"""Writes a question to the packet""" | ||||
self.writeName(question.name) | ||||
self.writeShort(question.type) | ||||
self.writeShort(question.clazz) | ||||
|
r7071 | |||
|
r28298 | def writeRecord(self, record, now): | ||
"""Writes a record (answer, authoritative answer, additional) to | ||||
the packet""" | ||||
self.writeName(record.name) | ||||
self.writeShort(record.type) | ||||
if record.unique and self.multicast: | ||||
self.writeShort(record.clazz | _CLASS_UNIQUE) | ||||
else: | ||||
self.writeShort(record.clazz) | ||||
if now == 0: | ||||
self.writeInt(record.ttl) | ||||
else: | ||||
self.writeInt(record.getRemainingTTL(now)) | ||||
index = len(self.data) | ||||
# Adjust size for the short we will write before this record | ||||
# | ||||
self.size += 2 | ||||
record.write(self) | ||||
self.size -= 2 | ||||
|
r7877 | |||
|
r28298 | length = len(''.join(self.data[index:])) | ||
self.insertShort(index, length) # Here is the short we adjusted for | ||||
|
r7071 | |||
|
r28298 | def packet(self): | ||
"""Returns a string containing the packet's bytes | ||||
|
r7071 | |||
|
r28298 | No further parts should be added to the packet once this | ||
is done.""" | ||||
if not self.finished: | ||||
self.finished = 1 | ||||
for question in self.questions: | ||||
self.writeQuestion(question) | ||||
for answer, time_ in self.answers: | ||||
self.writeRecord(answer, time_) | ||||
for authority in self.authorities: | ||||
self.writeRecord(authority, 0) | ||||
for additional in self.additionals: | ||||
self.writeRecord(additional, 0) | ||||
|
r7877 | |||
|
r28298 | self.insertShort(0, len(self.additionals)) | ||
self.insertShort(0, len(self.authorities)) | ||||
self.insertShort(0, len(self.answers)) | ||||
self.insertShort(0, len(self.questions)) | ||||
self.insertShort(0, self.flags) | ||||
if self.multicast: | ||||
self.insertShort(0, 0) | ||||
else: | ||||
self.insertShort(0, self.id) | ||||
return ''.join(self.data) | ||||
|
r7071 | |||
class DNSCache(object): | ||||
|
r28298 | """A cache of DNS entries""" | ||
|
r7877 | |||
|
r28298 | def __init__(self): | ||
self.cache = {} | ||||
|
r7071 | |||
|
r28298 | def add(self, entry): | ||
"""Adds an entry""" | ||||
try: | ||||
list = self.cache[entry.key] | ||||
except KeyError: | ||||
list = self.cache[entry.key] = [] | ||||
list.append(entry) | ||||
|
r7071 | |||
|
r28298 | def remove(self, entry): | ||
"""Removes an entry""" | ||||
try: | ||||
list = self.cache[entry.key] | ||||
list.remove(entry) | ||||
except KeyError: | ||||
pass | ||||
|
r7071 | |||
|
r28298 | def get(self, entry): | ||
"""Gets an entry by key. Will return None if there is no | ||||
matching entry.""" | ||||
try: | ||||
list = self.cache[entry.key] | ||||
return list[list.index(entry)] | ||||
except (KeyError, ValueError): | ||||
return None | ||||
|
r7071 | |||
|
r28298 | def getByDetails(self, name, type, clazz): | ||
"""Gets an entry by details. Will return None if there is | ||||
no matching entry.""" | ||||
entry = DNSEntry(name, type, clazz) | ||||
return self.get(entry) | ||||
|
r7071 | |||
|
r28298 | def entriesWithName(self, name): | ||
"""Returns a list of entries whose key matches the name.""" | ||||
try: | ||||
return self.cache[name] | ||||
except KeyError: | ||||
return [] | ||||
|
r7071 | |||
|
r28298 | def entries(self): | ||
"""Returns a list of all entries""" | ||||
def add(x, y): return x+y | ||||
try: | ||||
return reduce(add, self.cache.values()) | ||||
except Exception: | ||||
return [] | ||||
|
r7071 | |||
class Engine(threading.Thread): | ||||
|
r28298 | """An engine wraps read access to sockets, allowing objects that | ||
need to receive data from sockets to be called back when the | ||||
sockets are ready. | ||||
|
r7071 | |||
|
r28298 | A reader needs a handle_read() method, which is called when the socket | ||
it is interested in is ready for reading. | ||||
|
r7071 | |||
|
r28298 | Writers are not implemented here, because we only send short | ||
packets. | ||||
""" | ||||
|
r7071 | |||
|
r28298 | def __init__(self, zeroconf): | ||
threading.Thread.__init__(self) | ||||
self.zeroconf = zeroconf | ||||
self.readers = {} # maps socket to reader | ||||
self.timeout = 5 | ||||
self.condition = threading.Condition() | ||||
self.start() | ||||
|
r7071 | |||
|
r28298 | def run(self): | ||
while not globals()['_GLOBAL_DONE']: | ||||
rs = self.getReaders() | ||||
if len(rs) == 0: | ||||
# No sockets to manage, but we wait for the timeout | ||||
# or addition of a socket | ||||
# | ||||
self.condition.acquire() | ||||
self.condition.wait(self.timeout) | ||||
self.condition.release() | ||||
else: | ||||
try: | ||||
rr, wr, er = select.select(rs, [], [], self.timeout) | ||||
for sock in rr: | ||||
try: | ||||
self.readers[sock].handle_read() | ||||
except Exception: | ||||
if not globals()['_GLOBAL_DONE']: | ||||
traceback.print_exc() | ||||
except Exception: | ||||
pass | ||||
|
r7071 | |||
|
r28298 | def getReaders(self): | ||
self.condition.acquire() | ||||
result = self.readers.keys() | ||||
self.condition.release() | ||||
return result | ||||
|
r7877 | |||
|
r28298 | def addReader(self, reader, socket): | ||
self.condition.acquire() | ||||
self.readers[socket] = reader | ||||
self.condition.notify() | ||||
self.condition.release() | ||||
|
r7071 | |||
|
r28298 | def delReader(self, socket): | ||
self.condition.acquire() | ||||
del(self.readers[socket]) | ||||
self.condition.notify() | ||||
self.condition.release() | ||||
|
r7071 | |||
|
r28298 | def notify(self): | ||
self.condition.acquire() | ||||
self.condition.notify() | ||||
self.condition.release() | ||||
|
r7071 | |||
class Listener(object): | ||||
|
r28298 | """A Listener is used by this module to listen on the multicast | ||
group to which DNS messages are sent, allowing the implementation | ||||
to cache information as it arrives. | ||||
|
r7071 | |||
|
r28298 | It requires registration with an Engine object in order to have | ||
the read() method called when a socket is available for reading.""" | ||||
|
r7877 | |||
|
r28298 | def __init__(self, zeroconf): | ||
self.zeroconf = zeroconf | ||||
self.zeroconf.engine.addReader(self, self.zeroconf.socket) | ||||
|
r7071 | |||
|
r28298 | def handle_read(self): | ||
data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE) | ||||
self.data = data | ||||
msg = DNSIncoming(data) | ||||
if msg.isQuery(): | ||||
# Always multicast responses | ||||
# | ||||
if port == _MDNS_PORT: | ||||
self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT) | ||||
# If it's not a multicast query, reply via unicast | ||||
# and multicast | ||||
# | ||||
elif port == _DNS_PORT: | ||||
self.zeroconf.handleQuery(msg, addr, port) | ||||
self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT) | ||||
else: | ||||
self.zeroconf.handleResponse(msg) | ||||
|
r7071 | |||
class Reaper(threading.Thread): | ||||
|
r28298 | """A Reaper is used by this module to remove cache entries that | ||
have expired.""" | ||||
|
r7877 | |||
|
r28298 | def __init__(self, zeroconf): | ||
threading.Thread.__init__(self) | ||||
self.zeroconf = zeroconf | ||||
self.start() | ||||
|
r7071 | |||
|
r28298 | def run(self): | ||
while True: | ||||
self.zeroconf.wait(10 * 1000) | ||||
if globals()['_GLOBAL_DONE']: | ||||
return | ||||
now = currentTimeMillis() | ||||
for record in self.zeroconf.cache.entries(): | ||||
if record.isExpired(now): | ||||
self.zeroconf.updateRecord(now, record) | ||||
self.zeroconf.cache.remove(record) | ||||
|
r7071 | |||
class ServiceBrowser(threading.Thread): | ||||
|
r28298 | """Used to browse for a service of a specific type. | ||
|
r7071 | |||
|
r28298 | The listener object will have its addService() and | ||
removeService() methods called when this browser | ||||
discovers changes in the services availability.""" | ||||
|
r7877 | |||
|
r28298 | def __init__(self, zeroconf, type, listener): | ||
"""Creates a browser for a specific type""" | ||||
threading.Thread.__init__(self) | ||||
self.zeroconf = zeroconf | ||||
self.type = type | ||||
self.listener = listener | ||||
self.services = {} | ||||
self.nextTime = currentTimeMillis() | ||||
self.delay = _BROWSER_TIME | ||||
self.list = [] | ||||
|
r7877 | |||
|
r28298 | self.done = 0 | ||
|
r7071 | |||
|
r28299 | self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, | ||
_CLASS_IN)) | ||||
|
r28298 | self.start() | ||
|
r7071 | |||
|
r28298 | def updateRecord(self, zeroconf, now, record): | ||
"""Callback invoked by Zeroconf when new information arrives. | ||||
|
r7071 | |||
|
r28298 | Updates information required by browser in the Zeroconf cache.""" | ||
if record.type == _TYPE_PTR and record.name == self.type: | ||||
expired = record.isExpired(now) | ||||
try: | ||||
oldrecord = self.services[record.alias.lower()] | ||||
if not expired: | ||||
oldrecord.resetTTL(record) | ||||
else: | ||||
del(self.services[record.alias.lower()]) | ||||
|
r28299 | callback = (lambda x: | ||
self.listener.removeService(x, self.type, record.alias)) | ||||
|
r28298 | self.list.append(callback) | ||
return | ||||
except Exception: | ||||
if not expired: | ||||
self.services[record.alias.lower()] = record | ||||
|
r28299 | callback = (lambda x: | ||
self.listener.addService(x, self.type, record.alias)) | ||||
|
r28298 | self.list.append(callback) | ||
|
r7071 | |||
|
r28298 | expires = record.getExpirationTime(75) | ||
if expires < self.nextTime: | ||||
self.nextTime = expires | ||||
|
r7071 | |||
|
r28298 | def cancel(self): | ||
self.done = 1 | ||||
self.zeroconf.notifyAll() | ||||
|
r7071 | |||
|
r28298 | def run(self): | ||
while True: | ||||
event = None | ||||
now = currentTimeMillis() | ||||
if len(self.list) == 0 and self.nextTime > now: | ||||
self.zeroconf.wait(self.nextTime - now) | ||||
if globals()['_GLOBAL_DONE'] or self.done: | ||||
return | ||||
now = currentTimeMillis() | ||||
|
r7071 | |||
|
r28298 | if self.nextTime <= now: | ||
out = DNSOutgoing(_FLAGS_QR_QUERY) | ||||
out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN)) | ||||
for record in self.services.values(): | ||||
if not record.isExpired(now): | ||||
out.addAnswerAtTime(record, now) | ||||
self.zeroconf.send(out) | ||||
self.nextTime = now + self.delay | ||||
self.delay = min(20 * 1000, self.delay * 2) | ||||
|
r7071 | |||
|
r28298 | if len(self.list) > 0: | ||
event = self.list.pop(0) | ||||
|
r7071 | |||
|
r28298 | if event is not None: | ||
event(self.zeroconf) | ||||
|
r7877 | |||
|
r7071 | |||
class ServiceInfo(object): | ||||
|
r28298 | """Service information""" | ||
|
r7877 | |||
|
r28299 | def __init__(self, type, name, address=None, port=None, weight=0, | ||
priority=0, properties=None, server=None): | ||||
|
r28298 | """Create a service description. | ||
|
r7071 | |||
|
r28298 | type: fully qualified service type name | ||
name: fully qualified service name | ||||
address: IP address as unsigned short, network byte order | ||||
port: port that the service runs on | ||||
weight: weight of the service | ||||
priority: priority of the service | ||||
|
r28299 | properties: dictionary of properties (or a string holding the bytes for | ||
the text field) | ||||
|
r28298 | server: fully qualified name for service host (defaults to name)""" | ||
|
r7071 | |||
|
r28298 | if not name.endswith(type): | ||
raise BadTypeInNameException | ||||
self.type = type | ||||
self.name = name | ||||
self.address = address | ||||
self.port = port | ||||
self.weight = weight | ||||
self.priority = priority | ||||
if server: | ||||
self.server = server | ||||
else: | ||||
self.server = name | ||||
self.setProperties(properties) | ||||
|
r7071 | |||
|
r28298 | def setProperties(self, properties): | ||
"""Sets properties and text of this info from a dictionary""" | ||||
if isinstance(properties, dict): | ||||
self.properties = properties | ||||
list = [] | ||||
result = '' | ||||
for key in properties: | ||||
value = properties[key] | ||||
if value is None: | ||||
suffix = '' | ||||
elif isinstance(value, str): | ||||
suffix = value | ||||
elif isinstance(value, int): | ||||
if value: | ||||
suffix = 'true' | ||||
else: | ||||
suffix = 'false' | ||||
else: | ||||
suffix = '' | ||||
list.append('='.join((key, suffix))) | ||||
for item in list: | ||||
|
r28299 | result = ''.join((result, struct.pack('!c', chr(len(item))), | ||
item)) | ||||
|
r28298 | self.text = result | ||
else: | ||||
self.text = properties | ||||
|
r7071 | |||
|
r28298 | def setText(self, text): | ||
"""Sets properties and text given a text field""" | ||||
self.text = text | ||||
try: | ||||
result = {} | ||||
end = len(text) | ||||
index = 0 | ||||
strs = [] | ||||
while index < end: | ||||
length = ord(text[index]) | ||||
index += 1 | ||||
strs.append(text[index:index+length]) | ||||
index += length | ||||
|
r7877 | |||
|
r28298 | for s in strs: | ||
eindex = s.find('=') | ||||
if eindex == -1: | ||||
# No equals sign at all | ||||
key = s | ||||
value = 0 | ||||
else: | ||||
key = s[:eindex] | ||||
value = s[eindex+1:] | ||||
if value == 'true': | ||||
value = 1 | ||||
elif value == 'false' or not value: | ||||
value = 0 | ||||
|
r7071 | |||
|
r28298 | # Only update non-existent properties | ||
if key and result.get(key) == None: | ||||
result[key] = value | ||||
|
r7071 | |||
|
r28298 | self.properties = result | ||
except Exception: | ||||
traceback.print_exc() | ||||
self.properties = None | ||||
|
r7877 | |||
|
r28298 | def getType(self): | ||
"""Type accessor""" | ||||
return self.type | ||||
|
r7071 | |||
|
r28298 | def getName(self): | ||
"""Name accessor""" | ||||
if self.type is not None and self.name.endswith("." + self.type): | ||||
return self.name[:len(self.name) - len(self.type) - 1] | ||||
return self.name | ||||
|
r7071 | |||
|
r28298 | def getAddress(self): | ||
"""Address accessor""" | ||||
return self.address | ||||
|
r7071 | |||
|
r28298 | def getPort(self): | ||
"""Port accessor""" | ||||
return self.port | ||||
|
r7071 | |||
|
r28298 | def getPriority(self): | ||
"""Priority accessor""" | ||||
return self.priority | ||||
|
r7071 | |||
|
r28298 | def getWeight(self): | ||
"""Weight accessor""" | ||||
return self.weight | ||||
|
r7071 | |||
|
r28298 | def getProperties(self): | ||
"""Properties accessor""" | ||||
return self.properties | ||||
|
r7071 | |||
|
r28298 | def getText(self): | ||
"""Text accessor""" | ||||
return self.text | ||||
|
r7071 | |||
|
r28298 | def getServer(self): | ||
"""Server accessor""" | ||||
return self.server | ||||
|
r7071 | |||
|
r28298 | def updateRecord(self, zeroconf, now, record): | ||
"""Updates service information from a DNS record""" | ||||
if record is not None and not record.isExpired(now): | ||||
if record.type == _TYPE_A: | ||||
#if record.name == self.name: | ||||
if record.name == self.server: | ||||
self.address = record.address | ||||
elif record.type == _TYPE_SRV: | ||||
if record.name == self.name: | ||||
self.server = record.server | ||||
self.port = record.port | ||||
self.weight = record.weight | ||||
self.priority = record.priority | ||||
#self.address = None | ||||
|
r28299 | self.updateRecord(zeroconf, now, | ||
zeroconf.cache.getByDetails(self.server, | ||||
_TYPE_A, _CLASS_IN)) | ||||
|
r28298 | elif record.type == _TYPE_TXT: | ||
if record.name == self.name: | ||||
self.setText(record.text) | ||||
|
r7071 | |||
|
r28298 | def request(self, zeroconf, timeout): | ||
"""Returns true if the service could be discovered on the | ||||
network, and updates this object with details discovered. | ||||
""" | ||||
now = currentTimeMillis() | ||||
delay = _LISTENER_TIME | ||||
next = now + delay | ||||
last = now + timeout | ||||
result = 0 | ||||
try: | ||||
|
r28299 | zeroconf.addListener(self, DNSQuestion(self.name, _TYPE_ANY, | ||
_CLASS_IN)) | ||||
while (self.server is None or self.address is None or | ||||
self.text is None): | ||||
|
r28298 | if last <= now: | ||
return 0 | ||||
if next <= now: | ||||
out = DNSOutgoing(_FLAGS_QR_QUERY) | ||||
|
r28299 | out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, | ||
_CLASS_IN)) | ||||
out.addAnswerAtTime( | ||||
zeroconf.cache.getByDetails(self.name, | ||||
_TYPE_SRV, | ||||
_CLASS_IN), | ||||
now) | ||||
out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, | ||||
_CLASS_IN)) | ||||
out.addAnswerAtTime( | ||||
zeroconf.cache.getByDetails(self.name, _TYPE_TXT, | ||||
_CLASS_IN), | ||||
now) | ||||
|
r28298 | if self.server is not None: | ||
|
r28299 | out.addQuestion( | ||
DNSQuestion(self.server, _TYPE_A, _CLASS_IN)) | ||||
out.addAnswerAtTime( | ||||
zeroconf.cache.getByDetails(self.server, _TYPE_A, | ||||
_CLASS_IN), | ||||
now) | ||||
|
r28298 | zeroconf.send(out) | ||
next = now + delay | ||||
delay = delay * 2 | ||||
|
r7071 | |||
|
r28298 | zeroconf.wait(min(next, last) - now) | ||
now = currentTimeMillis() | ||||
result = 1 | ||||
finally: | ||||
zeroconf.removeListener(self) | ||||
|
r7877 | |||
|
r28298 | return result | ||
|
r7071 | |||
|
r28298 | def __eq__(self, other): | ||
"""Tests equality of service name""" | ||||
if isinstance(other, ServiceInfo): | ||||
return other.name == self.name | ||||
return 0 | ||||
|
r7071 | |||
|
r28298 | def __ne__(self, other): | ||
"""Non-equality test""" | ||||
return not self.__eq__(other) | ||||
|
r7071 | |||
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
|
r28299 | result = ("service[%s,%s:%s," % | ||
(self.name, socket.inet_ntoa(self.getAddress()), self.port)) | ||||
|
r28298 | if self.text is None: | ||
result += "None" | ||||
else: | ||||
if len(self.text) < 20: | ||||
result += self.text | ||||
else: | ||||
result += self.text[:17] + "..." | ||||
result += "]" | ||||
return result | ||||
|
r7877 | |||
|
r7071 | |||
class Zeroconf(object): | ||||
|
r28298 | """Implementation of Zeroconf Multicast DNS Service Discovery | ||
|
r7071 | |||
|
r28298 | Supports registration, unregistration, queries and browsing. | ||
""" | ||||
def __init__(self, bindaddress=None): | ||||
"""Creates an instance of the Zeroconf class, establishing | ||||
multicast communications, listening and reaping threads.""" | ||||
globals()['_GLOBAL_DONE'] = 0 | ||||
if bindaddress is None: | ||||
self.intf = socket.gethostbyname(socket.gethostname()) | ||||
else: | ||||
self.intf = bindaddress | ||||
self.group = ('', _MDNS_PORT) | ||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | ||||
try: | ||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) | ||||
except Exception: | ||||
# SO_REUSEADDR should be equivalent to SO_REUSEPORT for | ||||
# multicast UDP sockets (p 731, "TCP/IP Illustrated, | ||||
# Volume 2"), but some BSD-derived systems require | ||||
# SO_REUSEPORT to be specified explicitly. Also, not all | ||||
# versions of Python have SO_REUSEPORT available. So | ||||
# if you're on a BSD-based system, and haven't upgraded | ||||
# to Python 2.3 yet, you may find this library doesn't | ||||
# work as expected. | ||||
# | ||||
pass | ||||
self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, "\xff") | ||||
self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, "\x01") | ||||
try: | ||||
self.socket.bind(self.group) | ||||
except Exception: | ||||
# Some versions of linux raise an exception even though | ||||
# SO_REUSEADDR and SO_REUSEPORT have been set, so ignore it | ||||
pass | ||||
|
r28299 | self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, | ||
socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0')) | ||||
|
r7071 | |||
|
r28298 | self.listeners = [] | ||
self.browsers = [] | ||||
self.services = {} | ||||
self.servicetypes = {} | ||||
|
r7071 | |||
|
r28298 | self.cache = DNSCache() | ||
|
r7071 | |||
|
r28298 | self.condition = threading.Condition() | ||
|
r7877 | |||
|
r28298 | self.engine = Engine(self) | ||
self.listener = Listener(self) | ||||
self.reaper = Reaper(self) | ||||
|
r7071 | |||
|
r28298 | def isLoopback(self): | ||
return self.intf.startswith("127.0.0.1") | ||||
|
r7071 | |||
|
r28298 | def isLinklocal(self): | ||
return self.intf.startswith("169.254.") | ||||
|
r7071 | |||
|
r28298 | def wait(self, timeout): | ||
"""Calling thread waits for a given number of milliseconds or | ||||
until notified.""" | ||||
self.condition.acquire() | ||||
self.condition.wait(timeout/1000) | ||||
self.condition.release() | ||||
|
r7071 | |||
|
r28298 | def notifyAll(self): | ||
"""Notifies all waiting threads""" | ||||
self.condition.acquire() | ||||
self.condition.notifyAll() | ||||
self.condition.release() | ||||
|
r7071 | |||
|
r28298 | def getServiceInfo(self, type, name, timeout=3000): | ||
"""Returns network's service information for a particular | ||||
name and type, or None if no service matches by the timeout, | ||||
which defaults to 3 seconds.""" | ||||
info = ServiceInfo(type, name) | ||||
if info.request(self, timeout): | ||||
return info | ||||
return None | ||||
|
r7071 | |||
|
r28298 | def addServiceListener(self, type, listener): | ||
"""Adds a listener for a particular service type. This object | ||||
will then have its updateRecord method called when information | ||||
arrives for that type.""" | ||||
self.removeServiceListener(listener) | ||||
self.browsers.append(ServiceBrowser(self, type, listener)) | ||||
|
r7071 | |||
|
r28298 | def removeServiceListener(self, listener): | ||
"""Removes a listener from the set that is currently listening.""" | ||||
for browser in self.browsers: | ||||
if browser.listener == listener: | ||||
browser.cancel() | ||||
del(browser) | ||||
|
r7071 | |||
|
r28298 | def registerService(self, info, ttl=_DNS_TTL): | ||
"""Registers service information to the network with a default TTL | ||||
of 60 seconds. Zeroconf will then respond to requests for | ||||
information for that service. The name of the service may be | ||||
changed if needed to make it unique on the network.""" | ||||
self.checkService(info) | ||||
self.services[info.name.lower()] = info | ||||
if self.servicetypes.has_key(info.type): | ||||
self.servicetypes[info.type]+=1 | ||||
else: | ||||
self.servicetypes[info.type]=1 | ||||
now = currentTimeMillis() | ||||
nextTime = now | ||||
i = 0 | ||||
while i < 3: | ||||
if now < nextTime: | ||||
self.wait(nextTime - now) | ||||
now = currentTimeMillis() | ||||
continue | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||||
|
r28299 | out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, | ||
_CLASS_IN, ttl, info.name), 0) | ||||
out.addAnswerAtTime( | ||||
DNSService( | ||||
info.name, _TYPE_SRV, | ||||
_CLASS_IN, ttl, info.priority, info.weight, info.port, | ||||
info.server), | ||||
0) | ||||
out.addAnswerAtTime( | ||||
DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), | ||||
0) | ||||
|
r28298 | if info.address: | ||
|
r28299 | out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, | ||
_CLASS_IN, ttl, info.address), 0) | ||||
|
r28298 | self.send(out) | ||
i += 1 | ||||
nextTime += _REGISTER_TIME | ||||
|
r7071 | |||
|
r28298 | def unregisterService(self, info): | ||
"""Unregister a service.""" | ||||
try: | ||||
del(self.services[info.name.lower()]) | ||||
if self.servicetypes[info.type]>1: | ||||
self.servicetypes[info.type]-=1 | ||||
else: | ||||
del self.servicetypes[info.type] | ||||
except KeyError: | ||||
pass | ||||
now = currentTimeMillis() | ||||
nextTime = now | ||||
i = 0 | ||||
while i < 3: | ||||
if now < nextTime: | ||||
self.wait(nextTime - now) | ||||
now = currentTimeMillis() | ||||
continue | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||||
|
r28299 | out.addAnswerAtTime( | ||
DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0) | ||||
out.addAnswerAtTime( | ||||
DNSService(info.name, _TYPE_SRV, | ||||
_CLASS_IN, 0, info.priority, info.weight, info.port, | ||||
info.name), | ||||
0) | ||||
out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, | ||||
_CLASS_IN, 0, info.text), 0) | ||||
|
r28298 | if info.address: | ||
|
r28299 | out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, | ||
_CLASS_IN, 0, info.address), 0) | ||||
|
r28298 | self.send(out) | ||
i += 1 | ||||
nextTime += _UNREGISTER_TIME | ||||
|
r7071 | |||
|
r28298 | def unregisterAllServices(self): | ||
"""Unregister all registered services.""" | ||||
if len(self.services) > 0: | ||||
now = currentTimeMillis() | ||||
nextTime = now | ||||
i = 0 | ||||
while i < 3: | ||||
if now < nextTime: | ||||
self.wait(nextTime - now) | ||||
now = currentTimeMillis() | ||||
continue | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||||
for info in self.services.values(): | ||||
|
r28299 | out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, | ||
_CLASS_IN, 0, info.name), 0) | ||||
out.addAnswerAtTime( | ||||
DNSService(info.name, _TYPE_SRV, | ||||
_CLASS_IN, 0, info.priority, info.weight, | ||||
info.port, info.server), | ||||
0) | ||||
out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, | ||||
_CLASS_IN, 0, info.text), 0) | ||||
|
r28298 | if info.address: | ||
|
r28299 | out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, | ||
_CLASS_IN, 0, info.address), 0) | ||||
|
r28298 | self.send(out) | ||
i += 1 | ||||
nextTime += _UNREGISTER_TIME | ||||
|
r7071 | |||
|
r28298 | def checkService(self, info): | ||
"""Checks the network for a unique service name, modifying the | ||||
ServiceInfo passed in if it is not unique.""" | ||||
now = currentTimeMillis() | ||||
nextTime = now | ||||
i = 0 | ||||
while i < 3: | ||||
for record in self.cache.entriesWithName(info.type): | ||||
|
r28299 | if (record.type == _TYPE_PTR and not record.isExpired(now) and | ||
record.alias == info.name): | ||||
|
r28298 | if (info.name.find('.') < 0): | ||
|
r28299 | info.name = ("%w.[%s:%d].%s" % | ||
(info.name, info.address, info.port, info.type)) | ||||
|
r28298 | self.checkService(info) | ||
return | ||||
raise NonUniqueNameException | ||||
if now < nextTime: | ||||
self.wait(nextTime - now) | ||||
now = currentTimeMillis() | ||||
continue | ||||
out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA) | ||||
self.debug = out | ||||
out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN)) | ||||
|
r28299 | out.addAuthoritativeAnswer(DNSPointer(info.type, _TYPE_PTR, | ||
_CLASS_IN, _DNS_TTL, info.name)) | ||||
|
r28298 | self.send(out) | ||
i += 1 | ||||
nextTime += _CHECK_TIME | ||||
|
r7071 | |||
|
r28298 | def addListener(self, listener, question): | ||
"""Adds a listener for a given question. The listener will have | ||||
its updateRecord method called when information is available to | ||||
answer the question.""" | ||||
now = currentTimeMillis() | ||||
self.listeners.append(listener) | ||||
if question is not None: | ||||
for record in self.cache.entriesWithName(question.name): | ||||
if question.answeredBy(record) and not record.isExpired(now): | ||||
listener.updateRecord(self, now, record) | ||||
self.notifyAll() | ||||
|
r7071 | |||
|
r28298 | def removeListener(self, listener): | ||
"""Removes a listener.""" | ||||
try: | ||||
self.listeners.remove(listener) | ||||
self.notifyAll() | ||||
except Exception: | ||||
pass | ||||
|
r7071 | |||
|
r28298 | def updateRecord(self, now, rec): | ||
"""Used to notify listeners of new information that has updated | ||||
a record.""" | ||||
for listener in self.listeners: | ||||
listener.updateRecord(self, now, rec) | ||||
self.notifyAll() | ||||
|
r7071 | |||
|
r28298 | def handleResponse(self, msg): | ||
"""Deal with incoming response packets. All answers | ||||
are held in the cache, and listeners are notified.""" | ||||
now = currentTimeMillis() | ||||
for record in msg.answers: | ||||
expired = record.isExpired(now) | ||||
if record in self.cache.entries(): | ||||
if expired: | ||||
self.cache.remove(record) | ||||
else: | ||||
entry = self.cache.get(record) | ||||
if entry is not None: | ||||
entry.resetTTL(record) | ||||
record = entry | ||||
else: | ||||
self.cache.add(record) | ||||
|
r7877 | |||
|
r28298 | self.updateRecord(now, record) | ||
|
r7071 | |||
|
r28298 | def handleQuery(self, msg, addr, port): | ||
"""Deal with incoming query packets. Provides a response if | ||||
possible.""" | ||||
out = None | ||||
|
r7071 | |||
|
r28298 | # Support unicast client responses | ||
# | ||||
if port != _MDNS_PORT: | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0) | ||||
for question in msg.questions: | ||||
out.addQuestion(question) | ||||
|
r7874 | |||
|
r28298 | for question in msg.questions: | ||
if question.type == _TYPE_PTR: | ||||
if question.name == "_services._dns-sd._udp.local.": | ||||
for stype in self.servicetypes.keys(): | ||||
if out is None: | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||||
|
r28299 | out.addAnswer(msg, | ||
DNSPointer( | ||||
"_services._dns-sd._udp.local.", | ||||
_TYPE_PTR, _CLASS_IN, | ||||
_DNS_TTL, stype)) | ||||
|
r28298 | for service in self.services.values(): | ||
if question.name == service.type: | ||||
if out is None: | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||||
|
r28299 | out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, | ||
_CLASS_IN, _DNS_TTL, service.name)) | ||||
|
r28298 | else: | ||
try: | ||||
if out is None: | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||||
|
r7874 | |||
|
r28298 | # Answer A record queries for any service addresses we know | ||
if question.type == _TYPE_A or question.type == _TYPE_ANY: | ||||
for service in self.services.values(): | ||||
if service.server == question.name.lower(): | ||||
|
r28299 | out.addAnswer(msg, | ||
DNSAddress(question.name, _TYPE_A, | ||||
_CLASS_IN | _CLASS_UNIQUE, | ||||
_DNS_TTL, service.address)) | ||||
|
r7874 | |||
|
r28298 | service = self.services.get(question.name.lower(), None) | ||
if not service: continue | ||||
|
r7874 | |||
|
r28299 | if (question.type == _TYPE_SRV or | ||
question.type == _TYPE_ANY): | ||||
out.addAnswer(msg, | ||||
DNSService(question.name, _TYPE_SRV, | ||||
_CLASS_IN | _CLASS_UNIQUE, | ||||
_DNS_TTL, service.priority, | ||||
service.weight, service.port, | ||||
service.server)) | ||||
if (question.type == _TYPE_TXT or | ||||
question.type == _TYPE_ANY): | ||||
out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, | ||||
_CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text)) | ||||
|
r28298 | if question.type == _TYPE_SRV: | ||
|
r28299 | out.addAdditionalAnswer( | ||
DNSAddress(service.server, _TYPE_A, | ||||
_CLASS_IN | _CLASS_UNIQUE, | ||||
_DNS_TTL, service.address)) | ||||
|
r28298 | except Exception: | ||
traceback.print_exc() | ||||
|
r7874 | |||
|
r28298 | if out is not None and out.answers: | ||
out.id = msg.id | ||||
self.send(out, addr, port) | ||||
|
r7071 | |||
|
r28298 | def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT): | ||
"""Sends an outgoing packet.""" | ||||
# This is a quick test to see if we can parse the packets we generate | ||||
#temp = DNSIncoming(out.packet()) | ||||
try: | ||||
self.socket.sendto(out.packet(), 0, (addr, port)) | ||||
except Exception: | ||||
# Ignore this, it may be a temporary loss of network connection | ||||
pass | ||||
|
r7071 | |||
|
r28298 | def close(self): | ||
"""Ends the background threads, and prevent this instance from | ||||
servicing further queries.""" | ||||
if globals()['_GLOBAL_DONE'] == 0: | ||||
globals()['_GLOBAL_DONE'] = 1 | ||||
self.notifyAll() | ||||
self.engine.notify() | ||||
self.unregisterAllServices() | ||||
|
r28299 | self.socket.setsockopt(socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, | ||
socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0')) | ||||
|
r28298 | self.socket.close() | ||
|
r7877 | |||
|
r7071 | # Test a few module features, including service registration, service | ||
# query (for Zoe), and service unregistration. | ||||
|
r7877 | if __name__ == '__main__': | ||
|
r28298 | print("Multicast DNS Service Discovery for Python, version", __version__) | ||
r = Zeroconf() | ||||
print("1. Testing registration of a service...") | ||||
desc = {'version':'0.10','a':'test value', 'b':'another value'} | ||||
|
r28299 | info = ServiceInfo("_http._tcp.local.", | ||
"My Service Name._http._tcp.local.", | ||||
socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc) | ||||
|
r28298 | print(" Registering service...") | ||
r.registerService(info) | ||||
print(" Registration done.") | ||||
print("2. Testing query of service information...") | ||||
|
r28299 | print(" Getting ZOE service:", | ||
str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))) | ||||
|
r28298 | print(" Query done.") | ||
print("3. Testing query of own service...") | ||||
|
r28299 | print(" Getting self:", | ||
str(r.getServiceInfo("_http._tcp.local.", | ||||
"My Service Name._http._tcp.local."))) | ||||
|
r28298 | print(" Query done.") | ||
print("4. Testing unregister of service information...") | ||||
r.unregisterService(info) | ||||
print(" Unregister done.") | ||||
r.close() | ||||
|
r10288 | |||
# no-check-code | ||||