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