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