1573 lines · 44.9 KB · Python
RawDownload
""" 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    License along with this library; if not, write to the Free Software    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ """0.12 update - allow selection of binding interface		 typo fix - Thanks A. M. Kuchlingi		 removed all use of word 'Rendezvous' - this is an API change""" """0.11 update - correction to comments for addListener method                 support for new record types seen from OS X				  - IPv6 address				  - hostinfo				 ignore unknown DNS record types				 fixes to name decoding				 works alongside other processes using port 5353 (e.g. on Mac OS X)				 tested against Mac OS X 10.3.2's mDNSResponder				 corrections to removal of list entries for service browser""" """0.10 update - Jonathon Paisley contributed these corrections:                 always multicast replies, even when query is unicast				 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 				 modified some unit tests to accommodate these changes""" """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				 ServiceBrowser now works				 new unit tests""" """0.06 update - small improvements with unit tests                 added defined exception types				 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""" """0.05 update - ensure case insensitivty on domain names                 support for unicast DNS queries""" """0.04 update - added some unit tests                 added __ne__ adjuncts where required				 ensure names end in '.local.'				 timeout on receiving socket for clean shutdown""" __author__ = "Paul Scott-Murphy"__email__ = "paul at scott dash murphy dot com"__version__ = "0.12" import stringimport timeimport structimport socketimport threadingimport selectimport 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 _MDNS_ADDR = '224.0.0.251'_MDNS_PORT = 5353;_DNS_PORT = 53;_DNS_TTL = 60 * 60; # one hour default TTL _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 _FLAGS_AA = 0x0400 # Authorative answer_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_TYPE_ANY =  255 # Mapping constants to names _CLASSES = { _CLASS_IN : "in",			 _CLASS_CS : "cs",			 _CLASS_CH : "ch",			 _CLASS_HS : "hs",			 _CLASS_NONE : "none",			 _CLASS_ANY : "any" } _TYPES = { _TYPE_A : "a",		   _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" } # utility functions def currentTimeMillis():	"""Current system time in milliseconds"""	return time.time() * 1000 # Exceptions class NonLocalNameException(Exception):	pass class NonUniqueNameException(Exception):	pass class NamePartTooLongException(Exception):	pass class AbstractMethodException(Exception):	pass class BadTypeInNameException(Exception):	pass # implementation classes class DNSEntry(object):	"""A DNS entry""" 	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 	def __eq__(self, other):		"""Equality test on name, type, and class"""		if isinstance(other, DNSEntry):			return self.name == other.name and self.type == other.type and self.clazz == other.clazz		return 0 	def __ne__(self, other):		"""Non-equality test"""		return not self.__eq__(other) 	def getClazz(self, clazz):		"""Class accessor"""		try:			return _CLASSES[clazz]		except:			return "?(%s)" % (clazz) 	def getType(self, type):		"""Type accessor"""		try:			return _TYPES[type]		except:			return "?(%s)" % (type) 	def toString(self, hdr, other):		"""String representation with additional information"""		result = "%s[%s,%s" % (hdr, self.getType(self.type), self.getClazz(self.clazz))		if self.unique:			result += "-unique,"		else:			result += ","		result += self.name		if other is not None:			result += ",%s]" % (other)		else:			result += "]"		return result class DNSQuestion(DNSEntry):	"""A DNS question entry""" 	def __init__(self, name, type, clazz):		if not name.endswith(".local."):			raise NonLocalNameException(name)		DNSEntry.__init__(self, name, type, clazz) 	def answeredBy(self, rec):		"""Returns true if the question is answered by the record"""		return self.clazz == rec.clazz and (self.type == rec.type or self.type == _TYPE_ANY) and self.name == rec.name 	def __repr__(self):		"""String representation"""		return DNSEntry.toString(self, "question", None)  class DNSRecord(DNSEntry):	"""A DNS record - like a DNS entry, but has a TTL""" 	def __init__(self, name, type, clazz, ttl):		DNSEntry.__init__(self, name, type, clazz)		self.ttl = ttl		self.created = currentTimeMillis() 	def __eq__(self, other):		"""Tests equality as per DNSRecord"""		if isinstance(other, DNSRecord):			return DNSEntry.__eq__(self, other)		return 0 	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 	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 	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) 	def getRemainingTTL(self, now):		"""Returns the remaining TTL in seconds."""		return max(0, (self.getExpirationTime(100) - now) / 1000) 	def isExpired(self, now):		"""Returns true if this record has expired."""		return self.getExpirationTime(100) <= now 	def isStale(self, now):		"""Returns true if this record is at least half way expired."""		return self.getExpirationTime(50) <= now 	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 	def write(self, out):		"""Abstract method"""		raise AbstractMethodException 	def toString(self, other):		"""String representation with addtional information"""		arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other)		return DNSEntry.toString(self, "record", arg) class DNSAddress(DNSRecord):	"""A DNS address record""" 	def __init__(self, name, type, clazz, ttl, address):		DNSRecord.__init__(self, name, type, clazz, ttl)		self.address = address 	def write(self, out):		"""Used in constructing an outgoing packet"""		out.writeString(self.address, len(self.address)) 	def __eq__(self, other):		"""Tests equality on address"""		if isinstance(other, DNSAddress):			return self.address == other.address		return 0 	def __repr__(self):		"""String representation"""		try:			return socket.inet_ntoa(self.address)		except:			return self.address class DNSHinfo(DNSRecord):	"""A DNS host information record""" 	def __init__(self, name, type, clazz, ttl, cpu, os):		DNSRecord.__init__(self, name, type, clazz, ttl)		self.cpu = cpu		self.os = os 	def write(self, out):		"""Used in constructing an outgoing packet"""		out.writeString(self.cpu, len(self.cpu))		out.writeString(self.os, len(self.os)) 	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 	def __repr__(self):		"""String representation"""		return self.cpu + " " + self.os class DNSPointer(DNSRecord):	"""A DNS pointer record""" 	def __init__(self, name, type, clazz, ttl, alias):		DNSRecord.__init__(self, name, type, clazz, ttl)		self.alias = alias 	def write(self, out):		"""Used in constructing an outgoing packet"""		out.writeName(self.alias) 	def __eq__(self, other):		"""Tests equality on alias"""		if isinstance(other, DNSPointer):			return self.alias == other.alias		return 0 	def __repr__(self):		"""String representation"""		return self.toString(self.alias) class DNSText(DNSRecord):	"""A DNS text record""" 	def __init__(self, name, type, clazz, ttl, text):		DNSRecord.__init__(self, name, type, clazz, ttl)		self.text = text 	def write(self, out):		"""Used in constructing an outgoing packet"""		out.writeString(self.text, len(self.text)) 	def __eq__(self, other):		"""Tests equality on text"""		if isinstance(other, DNSText):			return self.text == other.text		return 0 	def __repr__(self):		"""String representation"""		if len(self.text) > 10:			return self.toString(self.text[:7] + "...")		else:			return self.toString(self.text) class DNSService(DNSRecord):	"""A DNS service record""" 	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 	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) 	def __eq__(self, other):		"""Tests equality on priority, weight, port and server"""		if isinstance(other, DNSService):			return self.priority == other.priority and self.weight == other.weight and self.port == other.port and self.server == other.server		return 0 	def __repr__(self):		"""String representation"""		return self.toString("%s:%s" % (self.server, self.port)) class DNSIncoming(object):	"""Object representation of an incoming DNS packet""" 	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 		self.readHeader()		self.readQuestions()		self.readOthers() 	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 		self.id = info[0]		self.flags = info[1]		self.numQuestions = info[2]		self.numAnswers = info[3]		self.numAuthorities = info[4]		self.numAdditionals = info[5] 	def readQuestions(self):		"""Reads questions section of packet"""		format = '!HH'		length = struct.calcsize(format)		for i in range(0, self.numQuestions):			name = self.readName()			info = struct.unpack(format, self.data[self.offset:self.offset+length])			self.offset += length 			try:				question = DNSQuestion(name, info[0], info[1])				self.questions.append(question)			except NonLocalNameException:				pass 

Lines 1–500 of 1573.