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