eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/zeroconf/Zeroconf.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     1 """ Multicast DNS Service Discovery for Python, v0.12
       
     2     Copyright (C) 2003, Paul Scott-Murphy
       
     3 
       
     4     This module provides a framework for the use of DNS Service Discovery
       
     5     using IP multicast.  It has been tested against the JRendezvous
       
     6     implementation from <a href="http://strangeberry.com">StrangeBerry</a>,
       
     7     and against the mDNSResponder from Mac OS X 10.3.8.
       
     8 
       
     9     This library is free software; you can redistribute it and/or
       
    10     modify it under the terms of the GNU Lesser General Public
       
    11     License as published by the Free Software Foundation; either
       
    12     version 2.1 of the License, or (at your option) any later version.
       
    13 
       
    14     This library is distributed in the hope that it will be useful,
       
    15     but WITHOUT ANY WARRANTY; without even the implied warranty of
       
    16     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
       
    17     Lesser General Public License for more details.
       
    18 
       
    19     You should have received a copy of the GNU Lesser General Public
       
    20     License along with this library; if not, write to the Free Software
       
    21     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
       
    22 
       
    23 """
       
    24 
       
    25 """0.12 update - allow selection of binding interface
       
    26 		 typo fix - Thanks A. M. Kuchlingi
       
    27 		 removed all use of word 'Rendezvous' - this is an API change"""
       
    28 
       
    29 """0.11 update - correction to comments for addListener method
       
    30                  support for new record types seen from OS X
       
    31 				  - IPv6 address
       
    32 				  - hostinfo
       
    33 				 ignore unknown DNS record types
       
    34 				 fixes to name decoding
       
    35 				 works alongside other processes using port 5353 (e.g. on Mac OS X)
       
    36 				 tested against Mac OS X 10.3.2's mDNSResponder
       
    37 				 corrections to removal of list entries for service browser"""
       
    38 
       
    39 """0.10 update - Jonathon Paisley contributed these corrections:
       
    40                  always multicast replies, even when query is unicast
       
    41 				 correct a pointer encoding problem
       
    42 				 can now write records in any order
       
    43 				 traceback shown on failure
       
    44 				 better TXT record parsing
       
    45 				 server is now separate from name
       
    46 				 can cancel a service browser
       
    47 
       
    48 				 modified some unit tests to accommodate these changes"""
       
    49 
       
    50 """0.09 update - remove all records on service unregistration
       
    51                  fix DOS security problem with readName"""
       
    52 
       
    53 """0.08 update - changed licensing to LGPL"""
       
    54 
       
    55 """0.07 update - faster shutdown on engine
       
    56                  pointer encoding of outgoing names
       
    57 				 ServiceBrowser now works
       
    58 				 new unit tests"""
       
    59 
       
    60 """0.06 update - small improvements with unit tests
       
    61                  added defined exception types
       
    62 				 new style objects
       
    63 				 fixed hostname/interface problem
       
    64 				 fixed socket timeout problem
       
    65 				 fixed addServiceListener() typo bug
       
    66 				 using select() for socket reads
       
    67 				 tested on Debian unstable with Python 2.2.2"""
       
    68 
       
    69 """0.05 update - ensure case insensitivty on domain names
       
    70                  support for unicast DNS queries"""
       
    71 
       
    72 """0.04 update - added some unit tests
       
    73                  added __ne__ adjuncts where required
       
    74 				 ensure names end in '.local.'
       
    75 				 timeout on receiving socket for clean shutdown"""
       
    76 
       
    77 __author__ = "Paul Scott-Murphy"
       
    78 __email__ = "paul at scott dash murphy dot com"
       
    79 __version__ = "0.12"
       
    80 
       
    81 import string
       
    82 import time
       
    83 import struct
       
    84 import socket
       
    85 import threading
       
    86 import select
       
    87 import traceback
       
    88 
       
    89 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
       
    90 
       
    91 # hook for threads
       
    92 
       
    93 globals()['_GLOBAL_DONE'] = 0
       
    94 
       
    95 # Some timing constants
       
    96 
       
    97 _UNREGISTER_TIME = 125
       
    98 _CHECK_TIME = 175
       
    99 _REGISTER_TIME = 225
       
   100 _LISTENER_TIME = 200
       
   101 _BROWSER_TIME = 500
       
   102 
       
   103 # Some DNS constants
       
   104 
       
   105 _MDNS_ADDR = '224.0.0.251'
       
   106 _MDNS_PORT = 5353;
       
   107 _DNS_PORT = 53;
       
   108 _DNS_TTL = 60 * 60; # one hour default TTL
       
   109 
       
   110 _MAX_MSG_TYPICAL = 1460 # unused
       
   111 _MAX_MSG_ABSOLUTE = 8972
       
   112 
       
   113 _FLAGS_QR_MASK = 0x8000 # query response mask
       
   114 _FLAGS_QR_QUERY = 0x0000 # query
       
   115 _FLAGS_QR_RESPONSE = 0x8000 # response
       
   116 
       
   117 _FLAGS_AA = 0x0400 # Authorative answer
       
   118 _FLAGS_TC = 0x0200 # Truncated
       
   119 _FLAGS_RD = 0x0100 # Recursion desired
       
   120 _FLAGS_RA = 0x8000 # Recursion available
       
   121 
       
   122 _FLAGS_Z = 0x0040 # Zero
       
   123 _FLAGS_AD = 0x0020 # Authentic data
       
   124 _FLAGS_CD = 0x0010 # Checking disabled
       
   125 
       
   126 _CLASS_IN = 1
       
   127 _CLASS_CS = 2
       
   128 _CLASS_CH = 3
       
   129 _CLASS_HS = 4
       
   130 _CLASS_NONE = 254
       
   131 _CLASS_ANY = 255
       
   132 _CLASS_MASK = 0x7FFF
       
   133 _CLASS_UNIQUE = 0x8000
       
   134 
       
   135 _TYPE_A = 1
       
   136 _TYPE_NS = 2
       
   137 _TYPE_MD = 3
       
   138 _TYPE_MF = 4
       
   139 _TYPE_CNAME = 5
       
   140 _TYPE_SOA = 6
       
   141 _TYPE_MB = 7
       
   142 _TYPE_MG = 8
       
   143 _TYPE_MR = 9
       
   144 _TYPE_NULL = 10
       
   145 _TYPE_WKS = 11
       
   146 _TYPE_PTR = 12
       
   147 _TYPE_HINFO = 13
       
   148 _TYPE_MINFO = 14
       
   149 _TYPE_MX = 15
       
   150 _TYPE_TXT = 16
       
   151 _TYPE_AAAA = 28
       
   152 _TYPE_SRV = 33
       
   153 _TYPE_ANY =  255
       
   154 
       
   155 # Mapping constants to names
       
   156 
       
   157 _CLASSES = { _CLASS_IN : "in",
       
   158 			 _CLASS_CS : "cs",
       
   159 			 _CLASS_CH : "ch",
       
   160 			 _CLASS_HS : "hs",
       
   161 			 _CLASS_NONE : "none",
       
   162 			 _CLASS_ANY : "any" }
       
   163 
       
   164 _TYPES = { _TYPE_A : "a",
       
   165 		   _TYPE_NS : "ns",
       
   166 		   _TYPE_MD : "md",
       
   167 		   _TYPE_MF : "mf",
       
   168 		   _TYPE_CNAME : "cname",
       
   169 		   _TYPE_SOA : "soa",
       
   170 		   _TYPE_MB : "mb",
       
   171 		   _TYPE_MG : "mg",
       
   172 		   _TYPE_MR : "mr",
       
   173 		   _TYPE_NULL : "null",
       
   174 		   _TYPE_WKS : "wks",
       
   175 		   _TYPE_PTR : "ptr",
       
   176 		   _TYPE_HINFO : "hinfo",
       
   177 		   _TYPE_MINFO : "minfo",
       
   178 		   _TYPE_MX : "mx",
       
   179 		   _TYPE_TXT : "txt",
       
   180 		   _TYPE_AAAA : "quada",
       
   181 		   _TYPE_SRV : "srv",
       
   182 		   _TYPE_ANY : "any" }
       
   183 
       
   184 # utility functions
       
   185 
       
   186 def currentTimeMillis():
       
   187 	"""Current system time in milliseconds"""
       
   188 	return time.time() * 1000
       
   189 
       
   190 # Exceptions
       
   191 
       
   192 class NonLocalNameException(Exception):
       
   193 	pass
       
   194 
       
   195 class NonUniqueNameException(Exception):
       
   196 	pass
       
   197 
       
   198 class NamePartTooLongException(Exception):
       
   199 	pass
       
   200 
       
   201 class AbstractMethodException(Exception):
       
   202 	pass
       
   203 
       
   204 class BadTypeInNameException(Exception):
       
   205 	pass
       
   206 
       
   207 class BadDomainName(Exception):
       
   208 	def __init__(self, pos):
       
   209 		Exception.__init__(self, "at position %s" % pos)
       
   210 
       
   211 class BadDomainNameCircular(BadDomainName):
       
   212 	pass
       
   213 
       
   214 # implementation classes
       
   215 
       
   216 class DNSEntry(object):
       
   217 	"""A DNS entry"""
       
   218 
       
   219 	def __init__(self, name, type, clazz):
       
   220 		self.key = string.lower(name)
       
   221 		self.name = name
       
   222 		self.type = type
       
   223 		self.clazz = clazz & _CLASS_MASK
       
   224 		self.unique = (clazz & _CLASS_UNIQUE) != 0
       
   225 
       
   226 	def __eq__(self, other):
       
   227 		"""Equality test on name, type, and class"""
       
   228 		if isinstance(other, DNSEntry):
       
   229 			return self.name == other.name and self.type == other.type and self.clazz == other.clazz
       
   230 		return 0
       
   231 
       
   232 	def __ne__(self, other):
       
   233 		"""Non-equality test"""
       
   234 		return not self.__eq__(other)
       
   235 
       
   236 	def getClazz(self, clazz):
       
   237 		"""Class accessor"""
       
   238 		try:
       
   239 			return _CLASSES[clazz]
       
   240 		except:
       
   241 			return "?(%s)" % (clazz)
       
   242 
       
   243 	def getType(self, type):
       
   244 		"""Type accessor"""
       
   245 		try:
       
   246 			return _TYPES[type]
       
   247 		except:
       
   248 			return "?(%s)" % (type)
       
   249 
       
   250 	def toString(self, hdr, other):
       
   251 		"""String representation with additional information"""
       
   252 		result = "%s[%s,%s" % (hdr, self.getType(self.type), self.getClazz(self.clazz))
       
   253 		if self.unique:
       
   254 			result += "-unique,"
       
   255 		else:
       
   256 			result += ","
       
   257 		result += self.name
       
   258 		if other is not None:
       
   259 			result += ",%s]" % (other)
       
   260 		else:
       
   261 			result += "]"
       
   262 		return result
       
   263 
       
   264 class DNSQuestion(DNSEntry):
       
   265 	"""A DNS question entry"""
       
   266 
       
   267 	def __init__(self, name, type, clazz):
       
   268 		if not name.endswith(".local."):
       
   269 			raise NonLocalNameException(name)
       
   270 		DNSEntry.__init__(self, name, type, clazz)
       
   271 
       
   272 	def answeredBy(self, rec):
       
   273 		"""Returns true if the question is answered by the record"""
       
   274 		return self.clazz == rec.clazz and (self.type == rec.type or self.type == _TYPE_ANY) and self.name == rec.name
       
   275 
       
   276 	def __repr__(self):
       
   277 		"""String representation"""
       
   278 		return DNSEntry.toString(self, "question", None)
       
   279 
       
   280 
       
   281 class DNSRecord(DNSEntry):
       
   282 	"""A DNS record - like a DNS entry, but has a TTL"""
       
   283 
       
   284 	def __init__(self, name, type, clazz, ttl):
       
   285 		DNSEntry.__init__(self, name, type, clazz)
       
   286 		self.ttl = ttl
       
   287 		self.created = currentTimeMillis()
       
   288 
       
   289 	def __eq__(self, other):
       
   290 		"""Tests equality as per DNSRecord"""
       
   291 		if isinstance(other, DNSRecord):
       
   292 			return DNSEntry.__eq__(self, other)
       
   293 		return 0
       
   294 
       
   295 	def suppressedBy(self, msg):
       
   296 		"""Returns true if any answer in a message can suffice for the
       
   297 		information held in this record."""
       
   298 		for record in msg.answers:
       
   299 			if self.suppressedByAnswer(record):
       
   300 				return 1
       
   301 		return 0
       
   302 
       
   303 	def suppressedByAnswer(self, other):
       
   304 		"""Returns true if another record has same name, type and class,
       
   305 		and if its TTL is at least half of this record's."""
       
   306 		if self == other and other.ttl > (self.ttl / 2):
       
   307 			return 1
       
   308 		return 0
       
   309 
       
   310 	def getExpirationTime(self, percent):
       
   311 		"""Returns the time at which this record will have expired
       
   312 		by a certain percentage."""
       
   313 		return self.created + (percent * self.ttl * 10)
       
   314 
       
   315 	def getRemainingTTL(self, now):
       
   316 		"""Returns the remaining TTL in seconds."""
       
   317 		return max(0, (self.getExpirationTime(100) - now) / 1000)
       
   318 
       
   319 	def isExpired(self, now):
       
   320 		"""Returns true if this record has expired."""
       
   321 		return self.getExpirationTime(100) <= now
       
   322 
       
   323 	def isStale(self, now):
       
   324 		"""Returns true if this record is at least half way expired."""
       
   325 		return self.getExpirationTime(50) <= now
       
   326 
       
   327 	def resetTTL(self, other):
       
   328 		"""Sets this record's TTL and created time to that of
       
   329 		another record."""
       
   330 		self.created = other.created
       
   331 		self.ttl = other.ttl
       
   332 
       
   333 	def write(self, out):
       
   334 		"""Abstract method"""
       
   335 		raise AbstractMethodException
       
   336 
       
   337 	def toString(self, other):
       
   338 		"""String representation with addtional information"""
       
   339 		arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other)
       
   340 		return DNSEntry.toString(self, "record", arg)
       
   341 
       
   342 class DNSAddress(DNSRecord):
       
   343 	"""A DNS address record"""
       
   344 
       
   345 	def __init__(self, name, type, clazz, ttl, address):
       
   346 		DNSRecord.__init__(self, name, type, clazz, ttl)
       
   347 		self.address = address
       
   348 
       
   349 	def write(self, out):
       
   350 		"""Used in constructing an outgoing packet"""
       
   351 		out.writeString(self.address, len(self.address))
       
   352 
       
   353 	def __eq__(self, other):
       
   354 		"""Tests equality on address"""
       
   355 		if isinstance(other, DNSAddress):
       
   356 			return self.address == other.address
       
   357 		return 0
       
   358 
       
   359 	def __repr__(self):
       
   360 		"""String representation"""
       
   361 		try:
       
   362 			return socket.inet_ntoa(self.address)
       
   363 		except:
       
   364 			return self.address
       
   365 
       
   366 class DNSHinfo(DNSRecord):
       
   367 	"""A DNS host information record"""
       
   368 
       
   369 	def __init__(self, name, type, clazz, ttl, cpu, os):
       
   370 		DNSRecord.__init__(self, name, type, clazz, ttl)
       
   371 		self.cpu = cpu
       
   372 		self.os = os
       
   373 
       
   374 	def write(self, out):
       
   375 		"""Used in constructing an outgoing packet"""
       
   376 		out.writeString(self.cpu, len(self.cpu))
       
   377 		out.writeString(self.os, len(self.os))
       
   378 
       
   379 	def __eq__(self, other):
       
   380 		"""Tests equality on cpu and os"""
       
   381 		if isinstance(other, DNSHinfo):
       
   382 			return self.cpu == other.cpu and self.os == other.os
       
   383 		return 0
       
   384 
       
   385 	def __repr__(self):
       
   386 		"""String representation"""
       
   387 		return self.cpu + " " + self.os
       
   388 
       
   389 class DNSPointer(DNSRecord):
       
   390 	"""A DNS pointer record"""
       
   391 
       
   392 	def __init__(self, name, type, clazz, ttl, alias):
       
   393 		DNSRecord.__init__(self, name, type, clazz, ttl)
       
   394 		self.alias = alias
       
   395 
       
   396 	def write(self, out):
       
   397 		"""Used in constructing an outgoing packet"""
       
   398 		out.writeName(self.alias)
       
   399 
       
   400 	def __eq__(self, other):
       
   401 		"""Tests equality on alias"""
       
   402 		if isinstance(other, DNSPointer):
       
   403 			return self.alias == other.alias
       
   404 		return 0
       
   405 
       
   406 	def __repr__(self):
       
   407 		"""String representation"""
       
   408 		return self.toString(self.alias)
       
   409 
       
   410 class DNSText(DNSRecord):
       
   411 	"""A DNS text record"""
       
   412 
       
   413 	def __init__(self, name, type, clazz, ttl, text):
       
   414 		DNSRecord.__init__(self, name, type, clazz, ttl)
       
   415 		self.text = text
       
   416 
       
   417 	def write(self, out):
       
   418 		"""Used in constructing an outgoing packet"""
       
   419 		out.writeString(self.text, len(self.text))
       
   420 
       
   421 	def __eq__(self, other):
       
   422 		"""Tests equality on text"""
       
   423 		if isinstance(other, DNSText):
       
   424 			return self.text == other.text
       
   425 		return 0
       
   426 
       
   427 	def __repr__(self):
       
   428 		"""String representation"""
       
   429 		if len(self.text) > 10:
       
   430 			return self.toString(self.text[:7] + "...")
       
   431 		else:
       
   432 			return self.toString(self.text)
       
   433 
       
   434 class DNSService(DNSRecord):
       
   435 	"""A DNS service record"""
       
   436 
       
   437 	def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
       
   438 		DNSRecord.__init__(self, name, type, clazz, ttl)
       
   439 		self.priority = priority
       
   440 		self.weight = weight
       
   441 		self.port = port
       
   442 		self.server = server
       
   443 
       
   444 	def write(self, out):
       
   445 		"""Used in constructing an outgoing packet"""
       
   446 		out.writeShort(self.priority)
       
   447 		out.writeShort(self.weight)
       
   448 		out.writeShort(self.port)
       
   449 		out.writeName(self.server)
       
   450 
       
   451 	def __eq__(self, other):
       
   452 		"""Tests equality on priority, weight, port and server"""
       
   453 		if isinstance(other, DNSService):
       
   454 			return self.priority == other.priority and self.weight == other.weight and self.port == other.port and self.server == other.server
       
   455 		return 0
       
   456 
       
   457 	def __repr__(self):
       
   458 		"""String representation"""
       
   459 		return self.toString("%s:%s" % (self.server, self.port))
       
   460 
       
   461 class DNSIncoming(object):
       
   462 	"""Object representation of an incoming DNS packet"""
       
   463 
       
   464 	def __init__(self, data):
       
   465 		"""Constructor from string holding bytes of packet"""
       
   466 		self.offset = 0
       
   467 		self.data = data
       
   468 		self.questions = []
       
   469 		self.answers = []
       
   470 		self.numQuestions = 0
       
   471 		self.numAnswers = 0
       
   472 		self.numAuthorities = 0
       
   473 		self.numAdditionals = 0
       
   474 
       
   475 		self.readHeader()
       
   476 		self.readQuestions()
       
   477 		self.readOthers()
       
   478 
       
   479 	def readHeader(self):
       
   480 		"""Reads header portion of packet"""
       
   481 		format = '!HHHHHH'
       
   482 		length = struct.calcsize(format)
       
   483 		info = struct.unpack(format, self.data[self.offset:self.offset+length])
       
   484 		self.offset += length
       
   485 
       
   486 		self.id = info[0]
       
   487 		self.flags = info[1]
       
   488 		self.numQuestions = info[2]
       
   489 		self.numAnswers = info[3]
       
   490 		self.numAuthorities = info[4]
       
   491 		self.numAdditionals = info[5]
       
   492 
       
   493 	def readQuestions(self):
       
   494 		"""Reads questions section of packet"""
       
   495 		format = '!HH'
       
   496 		length = struct.calcsize(format)
       
   497 		for i in range(0, self.numQuestions):
       
   498 			name = self.readName()
       
   499 			info = struct.unpack(format, self.data[self.offset:self.offset+length])
       
   500 			self.offset += length
       
   501 
       
   502 			try:
       
   503 				question = DNSQuestion(name, info[0], info[1])
       
   504 				self.questions.append(question)
       
   505 			except NonLocalNameException:
       
   506 				pass
       
   507 
       
   508 	def readInt(self):
       
   509 		"""Reads an integer from the packet"""
       
   510 		format = '!I'
       
   511 		length = struct.calcsize(format)
       
   512 		info = struct.unpack(format, self.data[self.offset:self.offset+length])
       
   513 		self.offset += length
       
   514 		return info[0]
       
   515 
       
   516 	def readCharacterString(self):
       
   517 		"""Reads a character string from the packet"""
       
   518 		length = ord(self.data[self.offset])
       
   519 		self.offset += 1
       
   520 		return self.readString(length)
       
   521 
       
   522 	def readString(self, len):
       
   523 		"""Reads a string of a given length from the packet"""
       
   524 		format = '!' + str(len) + 's'
       
   525 		length =  struct.calcsize(format)
       
   526 		info = struct.unpack(format, self.data[self.offset:self.offset+length])
       
   527 		self.offset += length
       
   528 		return info[0]
       
   529 
       
   530 	def readUnsignedShort(self):
       
   531 		"""Reads an unsigned short from the packet"""
       
   532 		format = '!H'
       
   533 		length = struct.calcsize(format)
       
   534 		info = struct.unpack(format, self.data[self.offset:self.offset+length])
       
   535 		self.offset += length
       
   536 		return info[0]
       
   537 
       
   538 	def readOthers(self):
       
   539 		"""Reads the answers, authorities and additionals section of the packet"""
       
   540 		format = '!HHiH'
       
   541 		length = struct.calcsize(format)
       
   542 		n = self.numAnswers + self.numAuthorities + self.numAdditionals
       
   543 		for i in range(0, n):
       
   544 			domain = self.readName()
       
   545 			info = struct.unpack(format, self.data[self.offset:self.offset+length])
       
   546 			self.offset += length
       
   547 
       
   548 			rec = None
       
   549 			if info[0] == _TYPE_A:
       
   550 				rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4))
       
   551 			elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR:
       
   552 				rec = DNSPointer(domain, info[0], info[1], info[2], self.readName())
       
   553 			elif info[0] == _TYPE_TXT:
       
   554 				rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3]))
       
   555 			elif info[0] == _TYPE_SRV:
       
   556 				rec = DNSService(domain, info[0], info[1], info[2], self.readUnsignedShort(), self.readUnsignedShort(), self.readUnsignedShort(), self.readName())
       
   557 			elif info[0] == _TYPE_HINFO:
       
   558 				rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString())
       
   559 			elif info[0] == _TYPE_AAAA:
       
   560 				rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16))
       
   561 			else:
       
   562 				# Try to ignore types we don't know about
       
   563 				# this may mean the rest of the name is
       
   564 				# unable to be parsed, and may show errors
       
   565 				# so this is left for debugging.  New types
       
   566 				# encountered need to be parsed properly.
       
   567 				#
       
   568 				#print "UNKNOWN TYPE = " + str(info[0])
       
   569 				#raise BadTypeInNameException
       
   570 				self.offset += info[3]
       
   571 
       
   572 			if rec is not None:
       
   573 				self.answers.append(rec)
       
   574 
       
   575 	def isQuery(self):
       
   576 		"""Returns true if this is a query"""
       
   577 		return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
       
   578 
       
   579 	def isResponse(self):
       
   580 		"""Returns true if this is a response"""
       
   581 		return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
       
   582 
       
   583 	def readUTF(self, offset, len):
       
   584 		"""Reads a UTF-8 string of a given length from the packet"""
       
   585 		return self.data[offset:offset+len].decode('utf-8')
       
   586 
       
   587 	def readName(self):
       
   588 		"""Reads a domain name from the packet"""
       
   589 		result = ''
       
   590 		off = self.offset
       
   591 		next = -1
       
   592 		first = off
       
   593 
       
   594 		while 1:
       
   595 			len = ord(self.data[off])
       
   596 			off += 1
       
   597 			if len == 0:
       
   598 				break
       
   599 			t = len & 0xC0
       
   600 			if t == 0x00:
       
   601 				result = ''.join((result, self.readUTF(off, len) + '.'))
       
   602 				off += len
       
   603 			elif t == 0xC0:
       
   604 				if next < 0:
       
   605 					next = off + 1
       
   606 				off = ((len & 0x3F) << 8) | ord(self.data[off])
       
   607 				if off >= first:
       
   608 					raise BadDomainNameCircular(off)
       
   609 				first = off
       
   610 			else:
       
   611 				raise BadDomainName(off)
       
   612 
       
   613 		if next >= 0:
       
   614 			self.offset = next
       
   615 		else:
       
   616 			self.offset = off
       
   617 
       
   618 		return result
       
   619 
       
   620 
       
   621 class DNSOutgoing(object):
       
   622 	"""Object representation of an outgoing packet"""
       
   623 
       
   624 	def __init__(self, flags, multicast = 1):
       
   625 		self.finished = 0
       
   626 		self.id = 0
       
   627 		self.multicast = multicast
       
   628 		self.flags = flags
       
   629 		self.names = {}
       
   630 		self.data = []
       
   631 		self.size = 12
       
   632 
       
   633 		self.questions = []
       
   634 		self.answers = []
       
   635 		self.authorities = []
       
   636 		self.additionals = []
       
   637 
       
   638 	def addQuestion(self, record):
       
   639 		"""Adds a question"""
       
   640 		self.questions.append(record)
       
   641 
       
   642 	def addAnswer(self, inp, record):
       
   643 		"""Adds an answer"""
       
   644 		if not record.suppressedBy(inp):
       
   645 			self.addAnswerAtTime(record, 0)
       
   646 
       
   647 	def addAnswerAtTime(self, record, now):
       
   648 		"""Adds an answer if if does not expire by a certain time"""
       
   649 		if record is not None:
       
   650 			if now == 0 or not record.isExpired(now):
       
   651 				self.answers.append((record, now))
       
   652 
       
   653 	def addAuthorativeAnswer(self, record):
       
   654 		"""Adds an authoritative answer"""
       
   655 		self.authorities.append(record)
       
   656 
       
   657 	def addAdditionalAnswer(self, record):
       
   658 		"""Adds an additional answer"""
       
   659 		self.additionals.append(record)
       
   660 
       
   661 	def writeByte(self, value):
       
   662 		"""Writes a single byte to the packet"""
       
   663 		format = '!c'
       
   664 		self.data.append(struct.pack(format, chr(value)))
       
   665 		self.size += 1
       
   666 
       
   667 	def insertShort(self, index, value):
       
   668 		"""Inserts an unsigned short in a certain position in the packet"""
       
   669 		format = '!H'
       
   670 		self.data.insert(index, struct.pack(format, value))
       
   671 		self.size += 2
       
   672 
       
   673 	def writeShort(self, value):
       
   674 		"""Writes an unsigned short to the packet"""
       
   675 		format = '!H'
       
   676 		self.data.append(struct.pack(format, value))
       
   677 		self.size += 2
       
   678 
       
   679 	def writeInt(self, value):
       
   680 		"""Writes an unsigned integer to the packet"""
       
   681 		format = '!I'
       
   682 		self.data.append(struct.pack(format, int(value)))
       
   683 		self.size += 4
       
   684 
       
   685 	def writeString(self, value, length):
       
   686 		"""Writes a string to the packet"""
       
   687 		format = '!' + str(length) + 's'
       
   688 		self.data.append(struct.pack(format, value))
       
   689 		self.size += length
       
   690 
       
   691 	def writeUTF(self, s):
       
   692 		"""Writes a UTF-8 string of a given length to the packet"""
       
   693 		utfstr = s.encode('utf-8')
       
   694 		length = len(utfstr)
       
   695 		if length > 64:
       
   696 			raise NamePartTooLongException
       
   697 		self.writeByte(length)
       
   698 		self.writeString(utfstr, length)
       
   699 
       
   700 	def writeName(self, name):
       
   701 		"""Writes a domain name to the packet"""
       
   702 
       
   703 		try:
       
   704 			# Find existing instance of this name in packet
       
   705 			#
       
   706 			index = self.names[name]
       
   707 		except KeyError:
       
   708 			# No record of this name already, so write it
       
   709 			# out as normal, recording the location of the name
       
   710 			# for future pointers to it.
       
   711 			#
       
   712 			self.names[name] = self.size
       
   713 			parts = name.split('.')
       
   714 			if parts[-1] == '':
       
   715 				parts = parts[:-1]
       
   716 			for part in parts:
       
   717 				self.writeUTF(part)
       
   718 			self.writeByte(0)
       
   719 			return
       
   720 
       
   721 		# An index was found, so write a pointer to it
       
   722 		#
       
   723 		self.writeByte((index >> 8) | 0xC0)
       
   724 		self.writeByte(index)
       
   725 
       
   726 	def writeQuestion(self, question):
       
   727 		"""Writes a question to the packet"""
       
   728 		self.writeName(question.name)
       
   729 		self.writeShort(question.type)
       
   730 		self.writeShort(question.clazz)
       
   731 
       
   732 	def writeRecord(self, record, now):
       
   733 		"""Writes a record (answer, authoritative answer, additional) to
       
   734 		the packet"""
       
   735 		self.writeName(record.name)
       
   736 		self.writeShort(record.type)
       
   737 		if record.unique and self.multicast:
       
   738 			self.writeShort(record.clazz | _CLASS_UNIQUE)
       
   739 		else:
       
   740 			self.writeShort(record.clazz)
       
   741 		if now == 0:
       
   742 			self.writeInt(record.ttl)
       
   743 		else:
       
   744 			self.writeInt(record.getRemainingTTL(now))
       
   745 		index = len(self.data)
       
   746 		# Adjust size for the short we will write before this record
       
   747 		#
       
   748 		self.size += 2
       
   749 		record.write(self)
       
   750 		self.size -= 2
       
   751 
       
   752 		length = len(''.join(self.data[index:]))
       
   753 		self.insertShort(index, length) # Here is the short we adjusted for
       
   754 
       
   755 	def packet(self):
       
   756 		"""Returns a string containing the packet's bytes
       
   757 
       
   758 		No further parts should be added to the packet once this
       
   759 		is done."""
       
   760 		if not self.finished:
       
   761 			self.finished = 1
       
   762 			for question in self.questions:
       
   763 				self.writeQuestion(question)
       
   764 			for answer, time in self.answers:
       
   765 				self.writeRecord(answer, time)
       
   766 			for authority in self.authorities:
       
   767 				self.writeRecord(authority, 0)
       
   768 			for additional in self.additionals:
       
   769 				self.writeRecord(additional, 0)
       
   770 
       
   771 			self.insertShort(0, len(self.additionals))
       
   772 			self.insertShort(0, len(self.authorities))
       
   773 			self.insertShort(0, len(self.answers))
       
   774 			self.insertShort(0, len(self.questions))
       
   775 			self.insertShort(0, self.flags)
       
   776 			if self.multicast:
       
   777 				self.insertShort(0, 0)
       
   778 			else:
       
   779 				self.insertShort(0, self.id)
       
   780 		return ''.join(self.data)
       
   781 
       
   782 
       
   783 class DNSCache(object):
       
   784 	"""A cache of DNS entries"""
       
   785 
       
   786 	def __init__(self):
       
   787 		self.cache = {}
       
   788 
       
   789 	def add(self, entry):
       
   790 		"""Adds an entry"""
       
   791 		try:
       
   792 			list = self.cache[entry.key]
       
   793 		except:
       
   794 			list = self.cache[entry.key] = []
       
   795 		list.append(entry)
       
   796 
       
   797 	def remove(self, entry):
       
   798 		"""Removes an entry"""
       
   799 		try:
       
   800 			list = self.cache[entry.key]
       
   801 			list.remove(entry)
       
   802 		except:
       
   803 			pass
       
   804 
       
   805 	def get(self, entry):
       
   806 		"""Gets an entry by key.  Will return None if there is no
       
   807 		matching entry."""
       
   808 		try:
       
   809 			list = self.cache[entry.key]
       
   810 			return list[list.index(entry)]
       
   811 		except:
       
   812 			return None
       
   813 
       
   814 	def getByDetails(self, name, type, clazz):
       
   815 		"""Gets an entry by details.  Will return None if there is
       
   816 		no matching entry."""
       
   817 		entry = DNSEntry(name, type, clazz)
       
   818 		return self.get(entry)
       
   819 
       
   820 	def entriesWithName(self, name):
       
   821 		"""Returns a list of entries whose key matches the name."""
       
   822 		try:
       
   823 			return self.cache[name]
       
   824 		except:
       
   825 			return []
       
   826 
       
   827 	def entries(self):
       
   828 		"""Returns a list of all entries"""
       
   829 		def add(x, y): return x+y
       
   830 		try:
       
   831 			return reduce(add, self.cache.values())
       
   832 		except:
       
   833 			return []
       
   834 
       
   835 
       
   836 class Engine(threading.Thread):
       
   837 	"""An engine wraps read access to sockets, allowing objects that
       
   838 	need to receive data from sockets to be called back when the
       
   839 	sockets are ready.
       
   840 
       
   841 	A reader needs a handle_read() method, which is called when the socket
       
   842 	it is interested in is ready for reading.
       
   843 
       
   844 	Writers are not implemented here, because we only send short
       
   845 	packets.
       
   846 	"""
       
   847 
       
   848 	def __init__(self, zeroconf):
       
   849 		threading.Thread.__init__(self)
       
   850 		self.zeroconf = zeroconf
       
   851 		self.readers = {} # maps socket to reader
       
   852 		self.timeout = 5
       
   853 		self.condition = threading.Condition()
       
   854 		self.start()
       
   855 
       
   856 	def run(self):
       
   857 		while not globals()['_GLOBAL_DONE']:
       
   858 			rs = self.getReaders()
       
   859 			if len(rs) == 0:
       
   860 				# No sockets to manage, but we wait for the timeout
       
   861 				# or addition of a socket
       
   862 				#
       
   863 				self.condition.acquire()
       
   864 				self.condition.wait(self.timeout)
       
   865 				self.condition.release()
       
   866 			else:
       
   867 				try:
       
   868 					rr, wr, er = select.select(rs, [], [], self.timeout)
       
   869 					for socket in rr:
       
   870 						try:
       
   871 							self.readers[socket].handle_read()
       
   872 						except:
       
   873 							if not globals()['_GLOBAL_DONE']:
       
   874 								traceback.print_exc()
       
   875 				except:
       
   876 					pass
       
   877 
       
   878 	def getReaders(self):
       
   879 		self.condition.acquire()
       
   880 		result = self.readers.keys()
       
   881 		self.condition.release()
       
   882 		return result
       
   883 
       
   884 	def addReader(self, reader, socket):
       
   885 		self.condition.acquire()
       
   886 		self.readers[socket] = reader
       
   887 		self.condition.notify()
       
   888 		self.condition.release()
       
   889 
       
   890 	def delReader(self, socket):
       
   891 		self.condition.acquire()
       
   892 		del(self.readers[socket])
       
   893 		self.condition.notify()
       
   894 		self.condition.release()
       
   895 
       
   896 	def notify(self):
       
   897 		self.condition.acquire()
       
   898 		self.condition.notify()
       
   899 		self.condition.release()
       
   900 
       
   901 class Listener(object):
       
   902 	"""A Listener is used by this module to listen on the multicast
       
   903 	group to which DNS messages are sent, allowing the implementation
       
   904 	to cache information as it arrives.
       
   905 
       
   906 	It requires registration with an Engine object in order to have
       
   907 	the read() method called when a socket is availble for reading."""
       
   908 
       
   909 	def __init__(self, zeroconf):
       
   910 		self.zeroconf = zeroconf
       
   911 		self.zeroconf.engine.addReader(self, self.zeroconf.socket)
       
   912 
       
   913 	def handle_read(self):
       
   914 		data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE)
       
   915 		self.data = data
       
   916 		msg = DNSIncoming(data)
       
   917 		if msg.isQuery():
       
   918 			# Always multicast responses
       
   919 			#
       
   920 			if port == _MDNS_PORT:
       
   921 				self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
       
   922 			# If it's not a multicast query, reply via unicast
       
   923 			# and multicast
       
   924 			#
       
   925 			elif port == _DNS_PORT:
       
   926 				self.zeroconf.handleQuery(msg, addr, port)
       
   927 				self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
       
   928 		else:
       
   929 			self.zeroconf.handleResponse(msg)
       
   930 
       
   931 
       
   932 class Reaper(threading.Thread):
       
   933 	"""A Reaper is used by this module to remove cache entries that
       
   934 	have expired."""
       
   935 
       
   936 	def __init__(self, zeroconf):
       
   937 		threading.Thread.__init__(self)
       
   938 		self.zeroconf = zeroconf
       
   939 		self.start()
       
   940 
       
   941 	def run(self):
       
   942 		while 1:
       
   943 			self.zeroconf.wait(10 * 1000)
       
   944 			if globals()['_GLOBAL_DONE']:
       
   945 				return
       
   946 			now = currentTimeMillis()
       
   947 			for record in self.zeroconf.cache.entries():
       
   948 				if record.isExpired(now):
       
   949 					self.zeroconf.updateRecord(now, record)
       
   950 					self.zeroconf.cache.remove(record)
       
   951 
       
   952 
       
   953 class ServiceBrowser(threading.Thread):
       
   954 	"""Used to browse for a service of a specific type.
       
   955 
       
   956 	The listener object will have its addService() and
       
   957 	removeService() methods called when this browser
       
   958 	discovers changes in the services availability."""
       
   959 
       
   960 	def __init__(self, zeroconf, type, listener):
       
   961 		"""Creates a browser for a specific type"""
       
   962 		threading.Thread.__init__(self)
       
   963 		self.zeroconf = zeroconf
       
   964 		self.type = type
       
   965 		self.listener = listener
       
   966 		self.services = {}
       
   967 		self.nextTime = currentTimeMillis()
       
   968 		self.delay = _BROWSER_TIME
       
   969 		self.list = []
       
   970 
       
   971 		self.done = 0
       
   972 
       
   973 		self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
       
   974 		self.start()
       
   975 
       
   976 	def updateRecord(self, zeroconf, now, record):
       
   977 		"""Callback invoked by Zeroconf when new information arrives.
       
   978 
       
   979 		Updates information required by browser in the Zeroconf cache."""
       
   980 		if record.type == _TYPE_PTR and record.name == self.type:
       
   981 			expired = record.isExpired(now)
       
   982 			try:
       
   983 				oldrecord = self.services[record.alias.lower()]
       
   984 				if not expired:
       
   985 					oldrecord.resetTTL(record)
       
   986 				else:
       
   987 					del(self.services[record.alias.lower()])
       
   988 					callback = lambda x: self.listener.removeService(x, self.type, record.alias)
       
   989 					self.list.append(callback)
       
   990 					return
       
   991 			except:
       
   992 				if not expired:
       
   993 					self.services[record.alias.lower()] = record
       
   994 					callback = lambda x: self.listener.addService(x, self.type, record.alias)
       
   995 					self.list.append(callback)
       
   996 
       
   997 			expires = record.getExpirationTime(75)
       
   998 			if expires < self.nextTime:
       
   999 				self.nextTime = expires
       
  1000 
       
  1001 	def cancel(self):
       
  1002 		self.done = 1
       
  1003 		self.zeroconf.notifyAll()
       
  1004 
       
  1005 	def run(self):
       
  1006 		while 1:
       
  1007 			event = None
       
  1008 			now = currentTimeMillis()
       
  1009 			if len(self.list) == 0 and self.nextTime > now:
       
  1010 				self.zeroconf.wait(self.nextTime - now)
       
  1011 			if globals()['_GLOBAL_DONE'] or self.done:
       
  1012 				return
       
  1013 			now = currentTimeMillis()
       
  1014 
       
  1015 			if self.nextTime <= now:
       
  1016 				out = DNSOutgoing(_FLAGS_QR_QUERY)
       
  1017 				out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
       
  1018 				for record in self.services.values():
       
  1019 					if not record.isExpired(now):
       
  1020 						out.addAnswerAtTime(record, now)
       
  1021 				self.zeroconf.send(out)
       
  1022 				self.nextTime = now + self.delay
       
  1023 				self.delay = min(20 * 1000, self.delay * 2)
       
  1024 
       
  1025 			if len(self.list) > 0:
       
  1026 				event = self.list.pop(0)
       
  1027 
       
  1028 			if event is not None:
       
  1029 				event(self.zeroconf)
       
  1030 
       
  1031 
       
  1032 class ServiceInfo(object):
       
  1033 	"""Service information"""
       
  1034 
       
  1035 	def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None):
       
  1036 		"""Create a service description.
       
  1037 
       
  1038 		type: fully qualified service type name
       
  1039 		name: fully qualified service name
       
  1040 		address: IP address as unsigned short, network byte order
       
  1041 		port: port that the service runs on
       
  1042 		weight: weight of the service
       
  1043 		priority: priority of the service
       
  1044 		properties: dictionary of properties (or a string holding the bytes for the text field)
       
  1045 		server: fully qualified name for service host (defaults to name)"""
       
  1046 
       
  1047 		if not name.endswith(type):
       
  1048 			raise BadTypeInNameException
       
  1049 		self.type = type
       
  1050 		self.name = name
       
  1051 		self.address = address
       
  1052 		self.port = port
       
  1053 		self.weight = weight
       
  1054 		self.priority = priority
       
  1055 		if server:
       
  1056 			self.server = server
       
  1057 		else:
       
  1058 			self.server = name
       
  1059 		self.setProperties(properties)
       
  1060 
       
  1061 	def setProperties(self, properties):
       
  1062 		"""Sets properties and text of this info from a dictionary"""
       
  1063 		if isinstance(properties, dict):
       
  1064 			self.properties = properties
       
  1065 			list = []
       
  1066 			result = ''
       
  1067 			for key in properties:
       
  1068 				value = properties[key]
       
  1069 				if value is None:
       
  1070 					suffix = ''
       
  1071 				elif isinstance(value, str):
       
  1072 					suffix = value
       
  1073 				elif isinstance(value, int):
       
  1074 					if value:
       
  1075 						suffix = 'true'
       
  1076 					else:
       
  1077 						suffix = 'false'
       
  1078 				else:
       
  1079 					suffix = ''
       
  1080 				list.append('='.join((key, suffix)))
       
  1081 			for item in list:
       
  1082 				result = ''.join((result, struct.pack('!c', chr(len(item))), item))
       
  1083 			self.text = result
       
  1084 		else:
       
  1085 			self.text = properties
       
  1086 
       
  1087 	def setText(self, text):
       
  1088 		"""Sets properties and text given a text field"""
       
  1089 		self.text = text
       
  1090 		try:
       
  1091 			result = {}
       
  1092 			end = len(text)
       
  1093 			index = 0
       
  1094 			strs = []
       
  1095 			while index < end:
       
  1096 				length = ord(text[index])
       
  1097 				index += 1
       
  1098 				strs.append(text[index:index+length])
       
  1099 				index += length
       
  1100 
       
  1101 			for s in strs:
       
  1102 				eindex = s.find('=')
       
  1103 				if eindex == -1:
       
  1104 					# No equals sign at all
       
  1105 					key = s
       
  1106 					value = 0
       
  1107 				else:
       
  1108 					key = s[:eindex]
       
  1109 					value = s[eindex+1:]
       
  1110 					if value == 'true':
       
  1111 						value = 1
       
  1112 					elif value == 'false' or not value:
       
  1113 						value = 0
       
  1114 
       
  1115 				# Only update non-existent properties
       
  1116 				if key and result.get(key) == None:
       
  1117 					result[key] = value
       
  1118 
       
  1119 			self.properties = result
       
  1120 		except:
       
  1121 			traceback.print_exc()
       
  1122 			self.properties = None
       
  1123 
       
  1124 	def getType(self):
       
  1125 		"""Type accessor"""
       
  1126 		return self.type
       
  1127 
       
  1128 	def getName(self):
       
  1129 		"""Name accessor"""
       
  1130 		if self.type is not None and self.name.endswith("." + self.type):
       
  1131 			return self.name[:len(self.name) - len(self.type) - 1]
       
  1132 		return self.name
       
  1133 
       
  1134 	def getAddress(self):
       
  1135 		"""Address accessor"""
       
  1136 		return self.address
       
  1137 
       
  1138 	def getPort(self):
       
  1139 		"""Port accessor"""
       
  1140 		return self.port
       
  1141 
       
  1142 	def getPriority(self):
       
  1143 		"""Pirority accessor"""
       
  1144 		return self.priority
       
  1145 
       
  1146 	def getWeight(self):
       
  1147 		"""Weight accessor"""
       
  1148 		return self.weight
       
  1149 
       
  1150 	def getProperties(self):
       
  1151 		"""Properties accessor"""
       
  1152 		return self.properties
       
  1153 
       
  1154 	def getText(self):
       
  1155 		"""Text accessor"""
       
  1156 		return self.text
       
  1157 
       
  1158 	def getServer(self):
       
  1159 		"""Server accessor"""
       
  1160 		return self.server
       
  1161 
       
  1162 	def updateRecord(self, zeroconf, now, record):
       
  1163 		"""Updates service information from a DNS record"""
       
  1164 		if record is not None and not record.isExpired(now):
       
  1165 			if record.type == _TYPE_A:
       
  1166 				#if record.name == self.name:
       
  1167 				if record.name == self.server:
       
  1168 					self.address = record.address
       
  1169 			elif record.type == _TYPE_SRV:
       
  1170 				if record.name == self.name:
       
  1171 					self.server = record.server
       
  1172 					self.port = record.port
       
  1173 					self.weight = record.weight
       
  1174 					self.priority = record.priority
       
  1175 					#self.address = None
       
  1176 					self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
       
  1177 			elif record.type == _TYPE_TXT:
       
  1178 				if record.name == self.name:
       
  1179 					self.setText(record.text)
       
  1180 
       
  1181 	def request(self, zeroconf, timeout):
       
  1182 		"""Returns true if the service could be discovered on the
       
  1183 		network, and updates this object with details discovered.
       
  1184 		"""
       
  1185 		now = currentTimeMillis()
       
  1186 		delay = _LISTENER_TIME
       
  1187 		next = now + delay
       
  1188 		last = now + timeout
       
  1189 		result = 0
       
  1190 		try:
       
  1191 			zeroconf.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
       
  1192 			while self.server is None or self.address is None or self.text is None:
       
  1193 				if last <= now:
       
  1194 					return 0
       
  1195 				if next <= now:
       
  1196 					out = DNSOutgoing(_FLAGS_QR_QUERY)
       
  1197 					out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
       
  1198 					out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_SRV, _CLASS_IN), now)
       
  1199 					out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
       
  1200 					out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_TXT, _CLASS_IN), now)
       
  1201 					if self.server is not None:
       
  1202 						out.addQuestion(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
       
  1203 						out.addAnswerAtTime(zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN), now)
       
  1204 					zeroconf.send(out)
       
  1205 					next = now + delay
       
  1206 					delay = delay * 2
       
  1207 
       
  1208 				zeroconf.wait(min(next, last) - now)
       
  1209 				now = currentTimeMillis()
       
  1210 			result = 1
       
  1211 		finally:
       
  1212 			zeroconf.removeListener(self)
       
  1213 
       
  1214 		return result
       
  1215 
       
  1216 	def __eq__(self, other):
       
  1217 		"""Tests equality of service name"""
       
  1218 		if isinstance(other, ServiceInfo):
       
  1219 			return other.name == self.name
       
  1220 		return 0
       
  1221 
       
  1222 	def __ne__(self, other):
       
  1223 		"""Non-equality test"""
       
  1224 		return not self.__eq__(other)
       
  1225 
       
  1226 	def __repr__(self):
       
  1227 		"""String representation"""
       
  1228 		result = "service[%s,%s:%s," % (self.name, socket.inet_ntoa(self.getAddress()), self.port)
       
  1229 		if self.text is None:
       
  1230 			result += "None"
       
  1231 		else:
       
  1232 			if len(self.text) < 20:
       
  1233 				result += self.text
       
  1234 			else:
       
  1235 				result += self.text[:17] + "..."
       
  1236 		result += "]"
       
  1237 		return result
       
  1238 
       
  1239 
       
  1240 class Zeroconf(object):
       
  1241 	"""Implementation of Zeroconf Multicast DNS Service Discovery
       
  1242 
       
  1243 	Supports registration, unregistration, queries and browsing.
       
  1244 	"""
       
  1245 	def __init__(self, bindaddress=None):
       
  1246 		"""Creates an instance of the Zeroconf class, establishing
       
  1247 		multicast communications, listening and reaping threads."""
       
  1248 		globals()['_GLOBAL_DONE'] = 0
       
  1249 		if bindaddress is None:
       
  1250 			self.intf = socket.gethostbyname(socket.gethostname())
       
  1251 		else:
       
  1252 			self.intf = bindaddress
       
  1253 		self.group = ('', _MDNS_PORT)
       
  1254 		self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
       
  1255 		try:
       
  1256 			self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
       
  1257 			self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
       
  1258 		except:
       
  1259 			# SO_REUSEADDR should be equivalent to SO_REUSEPORT for
       
  1260 			# multicast UDP sockets (p 731, "TCP/IP Illustrated,
       
  1261 			# Volume 2"), but some BSD-derived systems require
       
  1262 			# SO_REUSEPORT to be specified explicity.  Also, not all
       
  1263 			# versions of Python have SO_REUSEPORT available.  So
       
  1264 			# if you're on a BSD-based system, and haven't upgraded
       
  1265 			# to Python 2.3 yet, you may find this library doesn't
       
  1266 			# work as expected.
       
  1267 			#
       
  1268 			pass
       
  1269 		self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
       
  1270 		self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
       
  1271 		try:
       
  1272 			self.socket.bind(self.group)
       
  1273 		except:
       
  1274 			# Some versions of linux raise an exception even though
       
  1275 			# the SO_REUSE* options have been set, so ignore it
       
  1276 			#
       
  1277 			pass
       
  1278 		#self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
       
  1279 		self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
       
  1280 
       
  1281 		self.listeners = []
       
  1282 		self.browsers = []
       
  1283 		self.services = {}
       
  1284 		self.servicetypes = {}
       
  1285 
       
  1286 		self.cache = DNSCache()
       
  1287 
       
  1288 		self.condition = threading.Condition()
       
  1289 
       
  1290 		self.engine = Engine(self)
       
  1291 		self.listener = Listener(self)
       
  1292 		self.reaper = Reaper(self)
       
  1293 
       
  1294 	def isLoopback(self):
       
  1295 		return self.intf.startswith("127.0.0.1")
       
  1296 
       
  1297 	def isLinklocal(self):
       
  1298 		return self.intf.startswith("169.254.")
       
  1299 
       
  1300 	def wait(self, timeout):
       
  1301 		"""Calling thread waits for a given number of milliseconds or
       
  1302 		until notified."""
       
  1303 		self.condition.acquire()
       
  1304 		self.condition.wait(timeout/1000)
       
  1305 		self.condition.release()
       
  1306 
       
  1307 	def notifyAll(self):
       
  1308 		"""Notifies all waiting threads"""
       
  1309 		self.condition.acquire()
       
  1310 		self.condition.notifyAll()
       
  1311 		self.condition.release()
       
  1312 
       
  1313 	def getServiceInfo(self, type, name, timeout=3000):
       
  1314 		"""Returns network's service information for a particular
       
  1315 		name and type, or None if no service matches by the timeout,
       
  1316 		which defaults to 3 seconds."""
       
  1317 		info = ServiceInfo(type, name)
       
  1318 		if info.request(self, timeout):
       
  1319 			return info
       
  1320 		return None
       
  1321 
       
  1322 	def addServiceListener(self, type, listener):
       
  1323 		"""Adds a listener for a particular service type.  This object
       
  1324 		will then have its updateRecord method called when information
       
  1325 		arrives for that type."""
       
  1326 		self.removeServiceListener(listener)
       
  1327 		self.browsers.append(ServiceBrowser(self, type, listener))
       
  1328 
       
  1329 	def removeServiceListener(self, listener):
       
  1330 		"""Removes a listener from the set that is currently listening."""
       
  1331 		for browser in self.browsers:
       
  1332 			if browser.listener == listener:
       
  1333 				browser.cancel()
       
  1334 				del(browser)
       
  1335 
       
  1336 	def registerService(self, info, ttl=_DNS_TTL):
       
  1337 		"""Registers service information to the network with a default TTL
       
  1338 		of 60 seconds.  Zeroconf will then respond to requests for
       
  1339 		information for that service.  The name of the service may be
       
  1340 		changed if needed to make it unique on the network."""
       
  1341 		self.checkService(info)
       
  1342 		self.services[info.name.lower()] = info
       
  1343 		if self.servicetypes.has_key(info.type):
       
  1344 			self.servicetypes[info.type]+=1
       
  1345 		else:
       
  1346 			self.servicetypes[info.type]=1
       
  1347 		now = currentTimeMillis()
       
  1348 		nextTime = now
       
  1349 		i = 0
       
  1350 		while i < 3:
       
  1351 			if now < nextTime:
       
  1352 				self.wait(nextTime - now)
       
  1353 				now = currentTimeMillis()
       
  1354 				continue
       
  1355 			out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
       
  1356 			out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0)
       
  1357 			out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, ttl, info.priority, info.weight, info.port, info.server), 0)
       
  1358 			out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0)
       
  1359 			if info.address:
       
  1360 				out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, ttl, info.address), 0)
       
  1361 			self.send(out)
       
  1362 			i += 1
       
  1363 			nextTime += _REGISTER_TIME
       
  1364 
       
  1365 	def unregisterService(self, info):
       
  1366 		"""Unregister a service."""
       
  1367 		try:
       
  1368 			del(self.services[info.name.lower()])
       
  1369 			if self.servicetypes[info.type]>1:
       
  1370 				self.servicetypes[info.type]-=1
       
  1371 			else:
       
  1372 				del self.servicetypes[info.type]
       
  1373 		except:
       
  1374 			pass
       
  1375 		now = currentTimeMillis()
       
  1376 		nextTime = now
       
  1377 		i = 0
       
  1378 		while i < 3:
       
  1379 			if now < nextTime:
       
  1380 				self.wait(nextTime - now)
       
  1381 				now = currentTimeMillis()
       
  1382 				continue
       
  1383 			out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
       
  1384 			out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
       
  1385 			out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.name), 0)
       
  1386 			out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
       
  1387 			if info.address:
       
  1388 				out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
       
  1389 			self.send(out)
       
  1390 			i += 1
       
  1391 			nextTime += _UNREGISTER_TIME
       
  1392 
       
  1393 	def unregisterAllServices(self):
       
  1394 		"""Unregister all registered services."""
       
  1395 		if len(self.services) > 0:
       
  1396 			now = currentTimeMillis()
       
  1397 			nextTime = now
       
  1398 			i = 0
       
  1399 			while i < 3:
       
  1400 				if now < nextTime:
       
  1401 					self.wait(nextTime - now)
       
  1402 					now = currentTimeMillis()
       
  1403 					continue
       
  1404 				out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
       
  1405 				for info in self.services.values():
       
  1406 					out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
       
  1407 					out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.server), 0)
       
  1408 					out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
       
  1409 					if info.address:
       
  1410 						out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
       
  1411 				self.send(out)
       
  1412 				i += 1
       
  1413 				nextTime += _UNREGISTER_TIME
       
  1414 
       
  1415 	def checkService(self, info):
       
  1416 		"""Checks the network for a unique service name, modifying the
       
  1417 		ServiceInfo passed in if it is not unique."""
       
  1418 		now = currentTimeMillis()
       
  1419 		nextTime = now
       
  1420 		i = 0
       
  1421 		while i < 3:
       
  1422 			for record in self.cache.entriesWithName(info.type):
       
  1423 				if record.type == _TYPE_PTR and not record.isExpired(now) and record.alias == info.name:
       
  1424 					if (info.name.find('.') < 0):
       
  1425 						info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type
       
  1426 						self.checkService(info)
       
  1427 						return
       
  1428 					raise NonUniqueNameException
       
  1429 			if now < nextTime:
       
  1430 				self.wait(nextTime - now)
       
  1431 				now = currentTimeMillis()
       
  1432 				continue
       
  1433 			out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
       
  1434 			self.debug = out
       
  1435 			out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
       
  1436 			out.addAuthorativeAnswer(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name))
       
  1437 			self.send(out)
       
  1438 			i += 1
       
  1439 			nextTime += _CHECK_TIME
       
  1440 
       
  1441 	def addListener(self, listener, question):
       
  1442 		"""Adds a listener for a given question.  The listener will have
       
  1443 		its updateRecord method called when information is available to
       
  1444 		answer the question."""
       
  1445 		now = currentTimeMillis()
       
  1446 		self.listeners.append(listener)
       
  1447 		if question is not None:
       
  1448 			for record in self.cache.entriesWithName(question.name):
       
  1449 				if question.answeredBy(record) and not record.isExpired(now):
       
  1450 					listener.updateRecord(self, now, record)
       
  1451 		self.notifyAll()
       
  1452 
       
  1453 	def removeListener(self, listener):
       
  1454 		"""Removes a listener."""
       
  1455 		try:
       
  1456 			self.listeners.remove(listener)
       
  1457 			self.notifyAll()
       
  1458 		except:
       
  1459 			pass
       
  1460 
       
  1461 	def updateRecord(self, now, rec):
       
  1462 		"""Used to notify listeners of new information that has updated
       
  1463 		a record."""
       
  1464 		for listener in self.listeners:
       
  1465 			listener.updateRecord(self, now, rec)
       
  1466 		self.notifyAll()
       
  1467 
       
  1468 	def handleResponse(self, msg):
       
  1469 		"""Deal with incoming response packets.  All answers
       
  1470 		are held in the cache, and listeners are notified."""
       
  1471 		now = currentTimeMillis()
       
  1472 		for record in msg.answers:
       
  1473 			expired = record.isExpired(now)
       
  1474 			if record in self.cache.entries():
       
  1475 				if expired:
       
  1476 					self.cache.remove(record)
       
  1477 				else:
       
  1478 					entry = self.cache.get(record)
       
  1479 					if entry is not None:
       
  1480 						entry.resetTTL(record)
       
  1481 						record = entry
       
  1482 			else:
       
  1483 				self.cache.add(record)
       
  1484 
       
  1485 			self.updateRecord(now, record)
       
  1486 
       
  1487 	def handleQuery(self, msg, addr, port):
       
  1488 		"""Deal with incoming query packets.  Provides a response if
       
  1489 		possible."""
       
  1490 		out = None
       
  1491 
       
  1492 		# Support unicast client responses
       
  1493 		#
       
  1494 		if port != _MDNS_PORT:
       
  1495 			out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
       
  1496 			for question in msg.questions:
       
  1497 				out.addQuestion(question)
       
  1498 
       
  1499 		for question in msg.questions:
       
  1500 			if question.type == _TYPE_PTR:
       
  1501 				if question.name == "_services._dns-sd._udp.local.":
       
  1502 					for stype in self.servicetypes.keys():
       
  1503 						if out is None:
       
  1504 							out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
       
  1505 						out.addAnswer(msg, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
       
  1506 				for service in self.services.values():
       
  1507 					if question.name == service.type:
       
  1508 						if out is None:
       
  1509 							out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
       
  1510 						out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name))
       
  1511 			else:
       
  1512 				try:
       
  1513 					if out is None:
       
  1514 						out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
       
  1515 
       
  1516 					# Answer A record queries for any service addresses we know
       
  1517 					if question.type == _TYPE_A or question.type == _TYPE_ANY:
       
  1518 						for service in self.services.values():
       
  1519 							if service.server == question.name.lower():
       
  1520 								out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
       
  1521 
       
  1522 					service = self.services.get(question.name.lower(), None)
       
  1523 					if not service: continue
       
  1524 
       
  1525 					if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
       
  1526 						out.addAnswer(msg, DNSService(question.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.priority, service.weight, service.port, service.server))
       
  1527 					if question.type == _TYPE_TXT or question.type == _TYPE_ANY:
       
  1528 						out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text))
       
  1529 					if question.type == _TYPE_SRV:
       
  1530 						out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
       
  1531 				except:
       
  1532 					traceback.print_exc()
       
  1533 
       
  1534 		if out is not None and out.answers:
       
  1535 			out.id = msg.id
       
  1536 			self.send(out, addr, port)
       
  1537 
       
  1538 	def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
       
  1539 		"""Sends an outgoing packet."""
       
  1540 		# This is a quick test to see if we can parse the packets we generate
       
  1541 		#temp = DNSIncoming(out.packet())
       
  1542 		try:
       
  1543 			self.socket.sendto(out.packet(), 0, (addr, port))
       
  1544 		except:
       
  1545 			# Ignore this, it may be a temporary loss of network connection
       
  1546 			pass
       
  1547 
       
  1548 	def close(self):
       
  1549 		"""Ends the background threads, and prevent this instance from
       
  1550 		servicing further queries."""
       
  1551 		if globals()['_GLOBAL_DONE'] == 0:
       
  1552 			globals()['_GLOBAL_DONE'] = 1
       
  1553 			self.notifyAll()
       
  1554 			self.engine.notify()
       
  1555 			self.unregisterAllServices()
       
  1556 			self.socket.setsockopt(socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
       
  1557 			self.socket.close()
       
  1558 
       
  1559 # Test a few module features, including service registration, service
       
  1560 # query (for Zoe), and service unregistration.
       
  1561 
       
  1562 if __name__ == '__main__':
       
  1563 	print "Multicast DNS Service Discovery for Python, version", __version__
       
  1564 	r = Zeroconf()
       
  1565 	print "1. Testing registration of a service..."
       
  1566 	desc = {'version':'0.10','a':'test value', 'b':'another value'}
       
  1567 	info = ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
       
  1568 	print "   Registering service..."
       
  1569 	r.registerService(info)
       
  1570 	print "   Registration done."
       
  1571 	print "2. Testing query of service information..."
       
  1572 	print "   Getting ZOE service:", str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
       
  1573 	print "   Query done."
       
  1574 	print "3. Testing query of own service..."
       
  1575 	print "   Getting self:", str(r.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local."))
       
  1576 	print "   Query done."
       
  1577 	print "4. Testing unregister of service information..."
       
  1578 	r.unregisterService(info)
       
  1579 	print "   Unregister done."
       
  1580 	r.close()
       
  1581 
       
  1582 # no-check-code