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