From f0dd1dc782e9104f232bb18e45f29f19b37339c4 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Wed, 10 Nov 2010 16:10:41 -0500 Subject: [PATCH 01/36] Support for clients which require a single-message response to a SRV query Command-line tool to test whether multicast messages are working on an interface. Setup script and MANIFEST.in to generate sdist packages (which can be easy_installed). --- .gitignore | 3 ++ MANIFEST.in | 3 ++ Zeroconf.py | 122 ++++++++++++++++++++++++++++++----------------- setup.py | 29 +++++++++++ testmulticast.py | 26 ++++++++++ 5 files changed, 140 insertions(+), 43 deletions(-) create mode 100644 .gitignore create mode 100644 MANIFEST.in create mode 100644 setup.py create mode 100755 testmulticast.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a3882abb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +MANIFEST +dist +*.pyc diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..37e3f084 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include README +include lgpl.txt +include *.py diff --git a/Zeroconf.py b/Zeroconf.py index 8efaf033..e194cb7c 100755 --- a/Zeroconf.py +++ b/Zeroconf.py @@ -85,7 +85,8 @@ import threading import select import traceback - +import logging +log = logging.getLogger(__name__) __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"] # hook for threads @@ -269,7 +270,7 @@ def answeredBy(self, rec): def __repr__(self): """String representation""" return DNSEntry.toString(self, "question", None) - + __str__ = __repr__ class DNSRecord(DNSEntry): """A DNS record - like a DNS entry, but has a TTL""" @@ -555,8 +556,9 @@ def readOthers(self): # so this is left for debugging. New types # encountered need to be parsed properly. # - #print "UNKNOWN TYPE = " + str(info[0]) - #raise BadTypeInNameException + log.warn( + "Unknown DNS query type: %s", info[0] + ) pass if rec is not None: @@ -670,7 +672,7 @@ def writeShort(self, value): def writeInt(self, value): """Writes an unsigned integer to the packet""" format = '!I' - self.data.append(struct.pack(format, value)) + self.data.append(struct.pack(format, long(value))) self.size += 4 def writeString(self, value, length): @@ -857,14 +859,15 @@ def run(self): else: try: rr, wr, er = select.select(rs, [], [], self.timeout) + except Exception, err: + log.warn( 'Select failure, ignored: %s', err ) + else: for socket in rr: try: self.readers[socket].handle_read() - except: + except Exception, err: # Ignore errors that occur on shutdown - pass - except: - pass + log.error( 'Error handling read: %s', err ) def getReaders(self): result = [] @@ -897,7 +900,6 @@ class Listener(object): It requires registration with an Engine object in order to have the read() method called when a socket is availble for reading.""" - def __init__(self, zeroconf): self.zeroconf = zeroconf self.zeroconf.engine.addReader(self, self.zeroconf.socket) @@ -912,15 +914,18 @@ def handle_read(self): if port == _MDNS_PORT: self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT) # If it's not a multicast query, reply via unicast - # and multicast # + # and multicast elif port == _DNS_PORT: self.zeroconf.handleQuery(msg, addr, port) self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT) + else: + log.error( + "Unknown port: %s", port + ) else: self.zeroconf.handleResponse(msg) - class Reaper(threading.Thread): """A Reaper is used by this module to remove cache entries that have expired.""" @@ -928,6 +933,7 @@ class Reaper(threading.Thread): def __init__(self, zeroconf): threading.Thread.__init__(self) self.zeroconf = zeroconf + self.daemon = True self.start() def run(self): @@ -955,6 +961,7 @@ def __init__(self, zeroconf, type, listener): self.zeroconf = zeroconf self.type = type self.listener = listener + self.daemon = True self.services = {} self.nextTime = currentTimeMillis() self.delay = _BROWSER_TIME @@ -1047,7 +1054,7 @@ def __init__(self, type, name, address=None, port=None, weight=0, priority=0, pr if server: self.server = server else: - self.server = name + self.server = name #'.'.join([x for x in name.split('.') if not x.startswith('_')]) self.setProperties(properties) def setProperties(self, properties): @@ -1109,8 +1116,8 @@ def setText(self, text): result[key] = value self.properties = result - except: - traceback.print_exc() + except Exception, err: + log.error( "Failure composing text: %s", traceback.format_exc() ) self.properties = None def getType(self): @@ -1226,7 +1233,9 @@ def __repr__(self): result += self.text[:17] + "..." result += "]" return result - + + + class Zeroconf(object): """Implementation of Zeroconf Multicast DNS Service Discovery @@ -1239,13 +1248,30 @@ def __init__(self, bindaddress=None): globals()['_GLOBAL_DONE'] = 0 if bindaddress is None: self.intf = socket.gethostbyname(socket.gethostname()) + bindaddress = self.intf else: self.intf = bindaddress - self.group = ('', _MDNS_PORT) - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket = self.create_socket( (bindaddress, _MDNS_PORT) ) + + self.listeners = [] + self.browsers = [] + self.services = {} + + self.cache = DNSCache() + + self.condition = threading.Condition() + + self.engine = Engine(self) + self.listener = Listener(self) + self.reaper = Reaper(self) + + @classmethod + def create_socket( cls, address ): + """Create our multicast socket for mDNS usage""" + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) except: # SO_REUSEADDR should be equivalent to SO_REUSEPORT for # multicast UDP sockets (p 731, "TCP/IP Illustrated, @@ -1257,29 +1283,26 @@ def __init__(self, bindaddress=None): # work as expected. # pass - self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255) - self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) + sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255) + sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) try: - self.socket.bind(self.group) - except: + sock.bind(('',_MDNS_PORT)) + except Exception, err: # Some versions of linux raise an exception even though # the SO_REUSE* options have been set, so ignore it # - pass - self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0')) - self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0')) - - self.listeners = [] - self.browsers = [] - self.services = {} - - self.cache = DNSCache() - - self.condition = threading.Condition() - - self.engine = Engine(self) - self.listener = Listener(self) - self.reaper = Reaper(self) + log.error('Failure binding: %s', err) + interface_ip = address[0] or socket.gethostbyname(socket.gethostname()) + sock.setsockopt( + socket.SOL_IP, socket.IP_MULTICAST_IF, + socket.inet_aton( interface_ip) + + socket.inet_aton('0.0.0.0') + ) + sock.setsockopt( + socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, + socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0') + ) + return sock def isLoopback(self): return self.intf.startswith("127.0.0.1") @@ -1477,14 +1500,21 @@ def handleQuery(self, msg, addr, port): out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0) for question in msg.questions: out.addQuestion(question) - + log.debug( 'Questions...') for question in msg.questions: + log.debug( 'Question: %s', question ) if question.type == _TYPE_PTR: for service in self.services.values(): if question.name == service.type: if out is None: out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name)) + # devices such as AAstra phones will not re-query to + # resolve the pointer, they expect the final IP to show up + # in the response + out.addAdditionalAnswer(DNSText(service.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text)) + out.addAdditionalAnswer(DNSService(service.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.priority, service.weight, service.port, service.server)) + out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address)) else: try: if out is None: @@ -1495,6 +1525,7 @@ def handleQuery(self, msg, addr, port): for service in self.services.values(): if service.server == question.name.lower(): out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address)) + service = self.services.get(question.name.lower(), None) if not service: continue @@ -1505,19 +1536,24 @@ def handleQuery(self, msg, addr, port): out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text)) if question.type == _TYPE_SRV: out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address)) - except: - traceback.print_exc() + except Exception, err: + log.error( + 'Error handling query: %s',traceback.format_exc() + ) if out is not None and out.answers: out.id = msg.id self.send(out, addr, port) + else: + log.debug( 'No answer for %s', [q for q in msg.questions] ) def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT): """Sends an outgoing packet.""" # This is a quick test to see if we can parse the packets we generate #temp = DNSIncoming(out.packet()) try: - bytes_sent = self.socket.sendto(out.packet(), 0, (addr, port)) + packet = out.packet() + bytes_sent = self.socket.sendto(packet, 0, (addr, port)) except: # Ignore this, it may be a temporary loss of network connection pass diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..5c77e33d --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +#! /usr/bin/env python +"""Install Zeroconf.py using distutils""" +from distutils.core import setup +info = {} +keys = [('__author__','author'),('__email__','author_email'),('__version__','version')] +for line in open( 'Zeroconf.py' ): + for key,inf in keys: + if line.startswith( key ): + info[inf] = line.strip().split('=')[1].strip().strip('"').strip("'") + keys.remove( (key,inf)) + if not keys: + break +if __name__ == "__main__": + setup( + name='pyzeroconf', + description='Python Zeroconf (mDNS) Library', + url='http://digitaltorque.ca', + py_modules=['Zeroconf'], + scripts=['Browser.py'], + classifiers=[ + 'Development Status :: Production', + 'License :: OSI Approved :: LGPL2', + 'Topic :: Networking', + 'Intended Audience :: Developers', + 'Operating System :: Any', + 'Environment :: Console', + ], + **info + ) diff --git a/testmulticast.py b/testmulticast.py new file mode 100755 index 00000000..5bd03108 --- /dev/null +++ b/testmulticast.py @@ -0,0 +1,26 @@ +#! /usr/bin/env python +"""This script simply tests that the Zeroconf multicast setup works on your machine""" +import Zeroconf,socket,os,sys,select,logging + +def main(ip): + """Create a multicast socket, send a message, check it comes back""" + sock = Zeroconf.Zeroconf.create_socket( (ip,Zeroconf._MDNS_PORT) ) + payload = 'hello world' + for i in range( 5 ): + sock.sendto( payload, 0, (Zeroconf._MDNS_ADDR, Zeroconf._MDNS_PORT)) + print 'Waiting for looped message receipt' + rs,wr,xs = select.select( [sock],[],[], 1.0 ) + data,(addr,port) = sock.recvfrom( 200 ) + if data == payload: + print 'Success: looped message received' + return 0 + print 'Failure: Looped message not received' + return 1 + +if __name__ == "__main__": + logging.basicConfig( level = logging.DEBUG ) + usage = 'testmulticast.py ip.address' + if not sys.argv[1:]: + print usage + sys.exit( 1 ) + sys.exit( main(*sys.argv[1:]) ) From 32fb31c588d879a78a86dbbc052fbfbc4e9ec925 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Thu, 11 Nov 2010 17:03:41 -0500 Subject: [PATCH 02/36] Provide more generic mechanism for creating the socket and joining/leaving groups --- .gitignore | 1 + Zeroconf.py | 139 +++++++++++++++++++++++++++-------------------- setup.py | 2 +- testmulticast.py | 5 +- 4 files changed, 86 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index a3882abb..07626a58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ MANIFEST dist *.pyc +*.e4? diff --git a/Zeroconf.py b/Zeroconf.py index e194cb7c..b71a50a9 100755 --- a/Zeroconf.py +++ b/Zeroconf.py @@ -19,7 +19,7 @@ You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - + """ """0.12 update - allow selection of binding interface @@ -102,7 +102,7 @@ _BROWSER_TIME = 500 # Some DNS constants - + _MDNS_ADDR = '224.0.0.251' _MDNS_PORT = 5353; _DNS_PORT = 53; @@ -209,7 +209,7 @@ class BadTypeInNameException(Exception): class DNSEntry(object): """A DNS entry""" - + def __init__(self, name, type, clazz): self.key = string.lower(name) self.name = name @@ -257,7 +257,7 @@ def toString(self, hdr, other): class DNSQuestion(DNSEntry): """A DNS question entry""" - + def __init__(self, name, type, clazz): if not name.endswith(".local."): raise NonLocalNameException @@ -274,7 +274,7 @@ def __repr__(self): class DNSRecord(DNSEntry): """A DNS record - like a DNS entry, but has a TTL""" - + def __init__(self, name, type, clazz, ttl): DNSEntry.__init__(self, name, type, clazz) self.ttl = ttl @@ -335,7 +335,7 @@ def toString(self, other): class DNSAddress(DNSRecord): """A DNS address record""" - + def __init__(self, name, type, clazz, ttl, address): DNSRecord.__init__(self, name, type, clazz, ttl) self.address = address @@ -379,10 +379,10 @@ def __eq__(self, other): def __repr__(self): """String representation""" return self.cpu + " " + self.os - + class DNSPointer(DNSRecord): """A DNS pointer record""" - + def __init__(self, name, type, clazz, ttl, alias): DNSRecord.__init__(self, name, type, clazz, ttl) self.alias = alias @@ -403,7 +403,7 @@ def __repr__(self): class DNSText(DNSRecord): """A DNS text record""" - + def __init__(self, name, type, clazz, ttl, text): DNSRecord.__init__(self, name, type, clazz, ttl) self.text = text @@ -427,7 +427,7 @@ def __repr__(self): class DNSService(DNSRecord): """A DNS service record""" - + def __init__(self, name, type, clazz, ttl, priority, weight, port, server): DNSRecord.__init__(self, name, type, clazz, ttl) self.priority = priority @@ -454,7 +454,7 @@ def __repr__(self): class DNSIncoming(object): """Object representation of an incoming DNS packet""" - + def __init__(self, data): """Constructor from string holding bytes of packet""" self.offset = 0 @@ -465,7 +465,7 @@ def __init__(self, data): self.numAnswers = 0 self.numAuthorities = 0 self.numAdditionals = 0 - + self.readHeader() self.readQuestions() self.readOthers() @@ -492,7 +492,7 @@ def readQuestions(self): name = self.readName() info = struct.unpack(format, self.data[self.offset:self.offset+length]) self.offset += length - + question = DNSQuestion(name, info[0], info[1]) self.questions.append(question) @@ -563,7 +563,7 @@ def readOthers(self): if rec is not None: self.answers.append(rec) - + def isQuery(self): """Returns true if this is a query""" return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY @@ -576,7 +576,7 @@ def readUTF(self, offset, len): """Reads a UTF-8 string of a given length from the packet""" result = self.data[offset:offset+len].decode('utf-8') return result - + def readName(self): """Reads a domain name from the packet""" result = '' @@ -609,11 +609,11 @@ def readName(self): self.offset = off return result - - + + class DNSOutgoing(object): """Object representation of an outgoing packet""" - + def __init__(self, flags, multicast = 1): self.finished = 0 self.id = 0 @@ -622,7 +622,7 @@ def __init__(self, flags, multicast = 1): self.names = {} self.data = [] self.size = 12 - + self.questions = [] self.answers = [] self.authorities = [] @@ -662,7 +662,7 @@ def insertShort(self, index, value): format = '!H' self.data.insert(index, struct.pack(format, value)) self.size += 2 - + def writeShort(self, value): """Writes an unsigned short to the packet""" format = '!H' @@ -741,7 +741,7 @@ def writeRecord(self, record, now): self.size += 2 record.write(self) self.size -= 2 - + length = len(''.join(self.data[index:])) self.insertShort(index, length) # Here is the short we adjusted for @@ -760,7 +760,7 @@ def packet(self): self.writeRecord(authority, 0) for additional in self.additionals: self.writeRecord(additional, 0) - + self.insertShort(0, len(self.additionals)) self.insertShort(0, len(self.authorities)) self.insertShort(0, len(self.answers)) @@ -775,7 +775,7 @@ def packet(self): class DNSCache(object): """A cache of DNS entries""" - + def __init__(self): self.cache = {} @@ -875,7 +875,7 @@ def getReaders(self): result = self.readers.keys() self.condition.release() return result - + def addReader(self, reader, socket): self.condition.acquire() self.readers[socket] = reader @@ -921,7 +921,7 @@ def handle_read(self): self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT) else: log.error( - "Unknown port: %s", port + "Unknown port: %s", port ) else: self.zeroconf.handleResponse(msg) @@ -929,7 +929,7 @@ def handle_read(self): class Reaper(threading.Thread): """A Reaper is used by this module to remove cache entries that have expired.""" - + def __init__(self, zeroconf): threading.Thread.__init__(self) self.zeroconf = zeroconf @@ -954,7 +954,7 @@ class ServiceBrowser(threading.Thread): The listener object will have its addService() and removeService() methods called when this browser discovers changes in the services availability.""" - + def __init__(self, zeroconf, type, listener): """Creates a browser for a specific type""" threading.Thread.__init__(self) @@ -966,7 +966,7 @@ def __init__(self, zeroconf, type, listener): self.nextTime = currentTimeMillis() self.delay = _BROWSER_TIME self.list = [] - + self.done = 0 self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN)) @@ -1026,11 +1026,11 @@ def run(self): if event is not None: event(self.zeroconf) - + class ServiceInfo(object): """Service information""" - + def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None): """Create a service description. @@ -1096,7 +1096,7 @@ def setText(self, text): index += 1 strs.append(text[index:index+length]) index += length - + for s in strs: eindex = s.find('=') if eindex == -1: @@ -1119,7 +1119,7 @@ def setText(self, text): except Exception, err: log.error( "Failure composing text: %s", traceback.format_exc() ) self.properties = None - + def getType(self): """Type accessor""" return self.type @@ -1208,7 +1208,7 @@ def request(self, zeroconf, timeout): result = 1 finally: zeroconf.removeListener(self) - + return result def __eq__(self, other): @@ -1252,6 +1252,7 @@ def __init__(self, bindaddress=None): else: self.intf = bindaddress self.socket = self.create_socket( (bindaddress, _MDNS_PORT) ) + self.join_group( self.socket, _MDNS_ADDR ) self.listeners = [] self.browsers = [] @@ -1260,14 +1261,20 @@ def __init__(self, bindaddress=None): self.cache = DNSCache() self.condition = threading.Condition() - + self.engine = Engine(self) self.listener = Listener(self) self.reaper = Reaper(self) - + @classmethod - def create_socket( cls, address ): - """Create our multicast socket for mDNS usage""" + def create_socket( cls, address, TTL=255, loop=True ): + """Create our multicast socket for mDNS usage + + Creates a multicast UDP socket with multicast address configured for the + ip in address[0], and bound on all interfaces with port address[1]. + Configures TTL and loop-back operation + + """ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -1283,26 +1290,42 @@ def create_socket( cls, address ): # work as expected. # pass - sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255) - sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) + sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, TTL) + sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, int(bool(loop))) try: - sock.bind(('',_MDNS_PORT)) + # Note: multicast is *not* working if we don't bind on all interfaces, most likely + # because the 224.* isn't getting mapped to the address of the interface... + sock.bind(('',address[1])) except Exception, err: # Some versions of linux raise an exception even though # the SO_REUSE* options have been set, so ignore it # log.error('Failure binding: %s', err) - interface_ip = address[0] or socket.gethostbyname(socket.gethostname()) + if address[0]: + # listen/send on a single interface... + interface_ip = address[0] or socket.gethostbyname(socket.gethostname()) + sock.setsockopt( + socket.SOL_IP, socket.IP_MULTICAST_IF, + socket.inet_aton( interface_ip) + + socket.inet_aton('0.0.0.0') + ) + return sock + @classmethod + def join_group( cls, sock, group ): + """Add our socket to this multicast group""" + log.info( 'Joining multicast group: %s', group ) sock.setsockopt( - socket.SOL_IP, socket.IP_MULTICAST_IF, - socket.inet_aton( interface_ip) + - socket.inet_aton('0.0.0.0') + socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, + socket.inet_aton(group) + socket.inet_aton('0.0.0.0') ) + @classmethod + def leave_group( cls, sock, group ): + """Remove our socket from this multicast group""" + log.info( 'Leaving multicast group: %s', group ) sock.setsockopt( - socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, - socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0') + socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, + socket.inet_aton(group) + socket.inet_aton('0.0.0.0') ) - return sock def isLoopback(self): return self.intf.startswith("127.0.0.1") @@ -1486,7 +1509,7 @@ def handleResponse(self, msg): record = entry else: self.cache.add(record) - + self.updateRecord(now, record) def handleQuery(self, msg, addr, port): @@ -1509,8 +1532,8 @@ def handleQuery(self, msg, addr, port): if out is None: out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name)) - # devices such as AAstra phones will not re-query to - # resolve the pointer, they expect the final IP to show up + # devices such as AAstra phones will not re-query to + # resolve the pointer, they expect the final IP to show up # in the response out.addAdditionalAnswer(DNSText(service.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text)) out.addAdditionalAnswer(DNSService(service.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.priority, service.weight, service.port, service.server)) @@ -1519,17 +1542,17 @@ def handleQuery(self, msg, addr, port): try: if out is None: out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) - + # Answer A record queries for any service addresses we know if question.type == _TYPE_A or question.type == _TYPE_ANY: for service in self.services.values(): if service.server == question.name.lower(): out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address)) - - + + service = self.services.get(question.name.lower(), None) if not service: continue - + if question.type == _TYPE_SRV or question.type == _TYPE_ANY: out.addAnswer(msg, DNSService(question.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.priority, service.weight, service.port, service.server)) if question.type == _TYPE_TXT or question.type == _TYPE_ANY: @@ -1540,7 +1563,7 @@ def handleQuery(self, msg, addr, port): log.error( 'Error handling query: %s',traceback.format_exc() ) - + if out is not None and out.answers: out.id = msg.id self.send(out, addr, port) @@ -1566,13 +1589,13 @@ def close(self): self.notifyAll() self.engine.notify() self.unregisterAllServices() - self.socket.setsockopt(socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0')) + self.leave_group( self.socket, _MDNS_ADDR ) self.socket.close() - + # Test a few module features, including service registration, service # query (for Zoe), and service unregistration. -if __name__ == '__main__': +if __name__ == '__main__': print "Multicast DNS Service Discovery for Python, version", __version__ r = Zeroconf() print "1. Testing registration of a service..." diff --git a/setup.py b/setup.py index 5c77e33d..76bbcd0e 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ description='Python Zeroconf (mDNS) Library', url='http://digitaltorque.ca', py_modules=['Zeroconf'], - scripts=['Browser.py'], + #scripts=['Browser.py'], classifiers=[ 'Development Status :: Production', 'License :: OSI Approved :: LGPL2', diff --git a/testmulticast.py b/testmulticast.py index 5bd03108..09de4d45 100755 --- a/testmulticast.py +++ b/testmulticast.py @@ -5,6 +5,7 @@ def main(ip): """Create a multicast socket, send a message, check it comes back""" sock = Zeroconf.Zeroconf.create_socket( (ip,Zeroconf._MDNS_PORT) ) + Zeroconf.Zeroconf.join_group( sock, Zeroconf._MDNS_ADDR ) payload = 'hello world' for i in range( 5 ): sock.sendto( payload, 0, (Zeroconf._MDNS_ADDR, Zeroconf._MDNS_PORT)) @@ -12,7 +13,7 @@ def main(ip): rs,wr,xs = select.select( [sock],[],[], 1.0 ) data,(addr,port) = sock.recvfrom( 200 ) if data == payload: - print 'Success: looped message received' + print 'Success: looped message received' return 0 print 'Failure: Looped message not received' return 1 @@ -21,6 +22,6 @@ def main(ip): logging.basicConfig( level = logging.DEBUG ) usage = 'testmulticast.py ip.address' if not sys.argv[1:]: - print usage + print usage sys.exit( 1 ) sys.exit( main(*sys.argv[1:]) ) From bb554dc1b714307571b867c7786548df26157237 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Fri, 12 Nov 2010 13:31:11 -0500 Subject: [PATCH 03/36] Make a few operations more robust wrt unexpected data... --- Zeroconf.py | 64 +++++++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/Zeroconf.py b/Zeroconf.py index b71a50a9..b4c90250 100755 --- a/Zeroconf.py +++ b/Zeroconf.py @@ -536,33 +536,35 @@ def readOthers(self): info = struct.unpack(format, self.data[self.offset:self.offset+length]) self.offset += length - rec = None - if info[0] == _TYPE_A: - rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4)) - elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR: - rec = DNSPointer(domain, info[0], info[1], info[2], self.readName()) - elif info[0] == _TYPE_TXT: - rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3])) - elif info[0] == _TYPE_SRV: - rec = DNSService(domain, info[0], info[1], info[2], self.readUnsignedShort(), self.readUnsignedShort(), self.readUnsignedShort(), self.readName()) - elif info[0] == _TYPE_HINFO: - rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString()) - elif info[0] == _TYPE_AAAA: - rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16)) - else: - # Try to ignore types we don't know about - # this may mean the rest of the name is - # unable to be parsed, and may show errors - # so this is left for debugging. New types - # encountered need to be parsed properly. - # - log.warn( - "Unknown DNS query type: %s", info[0] - ) - pass + try: + rec = None + if info[0] == _TYPE_A: + rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4)) + elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR: + rec = DNSPointer(domain, info[0], info[1], info[2], self.readName()) + elif info[0] == _TYPE_TXT: + rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3])) + elif info[0] == _TYPE_SRV: + rec = DNSService(domain, info[0], info[1], info[2], self.readUnsignedShort(), self.readUnsignedShort(), self.readUnsignedShort(), self.readName()) + elif info[0] == _TYPE_HINFO: + rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString()) + elif info[0] == _TYPE_AAAA: + rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16)) + else: + # Try to ignore types we don't know about + # this may mean the rest of the name is + # unable to be parsed, and may show errors + # so this is left for debugging. New types + # encountered need to be parsed properly. + # + log.warn( + "Unknown DNS query type: %s", info[0] + ) - if rec is not None: - self.answers.append(rec) + if rec is not None: + self.answers.append(rec) + except Exception, err: + log.warn( "Failure on record type %s, ignoring: %s", info[0], err ) def isQuery(self): """Returns true if this is a query""" @@ -573,9 +575,12 @@ def isResponse(self): return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE def readUTF(self, offset, len): - """Reads a UTF-8 string of a given length from the packet""" - result = self.data[offset:offset+len].decode('utf-8') - return result + """Reads a UTF-8 string of a given length from the packet + + TODO: there are cases were non-utf-8 data comes through, + we need to decide how to properly handle these. + """ + return self.data[offset:offset+len].decode('utf-8','ignore') def readName(self): """Reads a domain name from the packet""" @@ -868,6 +873,7 @@ def run(self): except Exception, err: # Ignore errors that occur on shutdown log.error( 'Error handling read: %s', err ) + log.debug( 'Traceback: %s', traceback.format_exc()) def getReaders(self): result = [] From f4d74f0a157eed50f6f8dadc7ee4ecf5fb3cda77 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Sun, 14 Nov 2010 13:10:58 -0500 Subject: [PATCH 04/36] Refactored the multicast socket setup into a separate module --- Zeroconf.py | 70 ++++-------------------------------------------- mcastsocket.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++ testmulticast.py | 32 ++++++++++++---------- 3 files changed, 92 insertions(+), 79 deletions(-) create mode 100644 mcastsocket.py diff --git a/Zeroconf.py b/Zeroconf.py index b4c90250..8ffabb2c 100755 --- a/Zeroconf.py +++ b/Zeroconf.py @@ -86,6 +86,7 @@ import select import traceback import logging +import mcastsocket log = logging.getLogger(__name__) __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"] @@ -576,7 +577,7 @@ def isResponse(self): def readUTF(self, offset, len): """Reads a UTF-8 string of a given length from the packet - + TODO: there are cases were non-utf-8 data comes through, we need to decide how to properly handle these. """ @@ -1257,8 +1258,8 @@ def __init__(self, bindaddress=None): bindaddress = self.intf else: self.intf = bindaddress - self.socket = self.create_socket( (bindaddress, _MDNS_PORT) ) - self.join_group( self.socket, _MDNS_ADDR ) + self.socket = mcastsocket.create_socket( (bindaddress, _MDNS_PORT) ) + mcastsocket.join_group( self.socket, _MDNS_ADDR ) self.listeners = [] self.browsers = [] @@ -1272,67 +1273,6 @@ def __init__(self, bindaddress=None): self.listener = Listener(self) self.reaper = Reaper(self) - @classmethod - def create_socket( cls, address, TTL=255, loop=True ): - """Create our multicast socket for mDNS usage - - Creates a multicast UDP socket with multicast address configured for the - ip in address[0], and bound on all interfaces with port address[1]. - Configures TTL and loop-back operation - - """ - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - except: - # SO_REUSEADDR should be equivalent to SO_REUSEPORT for - # multicast UDP sockets (p 731, "TCP/IP Illustrated, - # Volume 2"), but some BSD-derived systems require - # SO_REUSEPORT to be specified explicity. Also, not all - # versions of Python have SO_REUSEPORT available. So - # if you're on a BSD-based system, and haven't upgraded - # to Python 2.3 yet, you may find this library doesn't - # work as expected. - # - pass - sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, TTL) - sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, int(bool(loop))) - try: - # Note: multicast is *not* working if we don't bind on all interfaces, most likely - # because the 224.* isn't getting mapped to the address of the interface... - sock.bind(('',address[1])) - except Exception, err: - # Some versions of linux raise an exception even though - # the SO_REUSE* options have been set, so ignore it - # - log.error('Failure binding: %s', err) - if address[0]: - # listen/send on a single interface... - interface_ip = address[0] or socket.gethostbyname(socket.gethostname()) - sock.setsockopt( - socket.SOL_IP, socket.IP_MULTICAST_IF, - socket.inet_aton( interface_ip) + - socket.inet_aton('0.0.0.0') - ) - return sock - @classmethod - def join_group( cls, sock, group ): - """Add our socket to this multicast group""" - log.info( 'Joining multicast group: %s', group ) - sock.setsockopt( - socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, - socket.inet_aton(group) + socket.inet_aton('0.0.0.0') - ) - @classmethod - def leave_group( cls, sock, group ): - """Remove our socket from this multicast group""" - log.info( 'Leaving multicast group: %s', group ) - sock.setsockopt( - socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, - socket.inet_aton(group) + socket.inet_aton('0.0.0.0') - ) - def isLoopback(self): return self.intf.startswith("127.0.0.1") @@ -1595,7 +1535,7 @@ def close(self): self.notifyAll() self.engine.notify() self.unregisterAllServices() - self.leave_group( self.socket, _MDNS_ADDR ) + mcastsocket.leave_group( self.socket, _MDNS_ADDR ) self.socket.close() # Test a few module features, including service registration, service diff --git a/mcastsocket.py b/mcastsocket.py new file mode 100644 index 00000000..b96dc3cd --- /dev/null +++ b/mcastsocket.py @@ -0,0 +1,69 @@ +"""Multicast socket setup code + +This is refactored from the Zeroconf.py main module to allow for reuse within +multiple environments (e.g. multicast SIP configuration, multicast paging +groups and the like). +""" +import socket,logging +log = logging.getLogger( __name__ ) + +def create_socket( address, TTL=1, loop=True, reuse=True ): + """Create our multicast socket for mDNS usage + + Creates a multicast UDP socket with multicast address configured for the + ip in address[0], and bound on all interfaces with port address[1]. + Configures TTL and loop-back operation + + """ + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, TTL) + sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, int(bool(loop))) + if reuse: + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + except: + # SO_REUSEADDR should be equivalent to SO_REUSEPORT for + # multicast UDP sockets (p 731, "TCP/IP Illustrated, + # Volume 2"), but some BSD-derived systems require + # SO_REUSEPORT to be specified explicity. Also, not all + # versions of Python have SO_REUSEPORT available. So + # if you're on a BSD-based system, and haven't upgraded + # to Python 2.3 yet, you may find this library doesn't + # work as expected. + # + pass + try: + # Note: multicast is *not* working if we don't bind on all interfaces, most likely + # because the 224.* isn't getting mapped to the address of the interface... + sock.bind(('',address[1])) + except Exception, err: + # Some versions of linux raise an exception even though + # the SO_REUSE* options have been set, so ignore it + # + log.error('Failure binding: %s', err) + if address[0]: + # listen/send on a single interface... + interface_ip = address[0] + log.debug( 'Setting multicast to use interface of %s', address[0] ) + sock.setsockopt( + socket.SOL_IP, socket.IP_MULTICAST_IF, + socket.inet_aton( interface_ip) + + socket.inet_aton('0.0.0.0') + ) + return sock + +def join_group( sock, group ): + """Add our socket to this multicast group""" + log.info( 'Joining multicast group: %s', group ) + sock.setsockopt( + socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, + socket.inet_aton(group) + socket.inet_aton('0.0.0.0') + ) +def leave_group( sock, group ): + """Remove our socket from this multicast group""" + log.info( 'Leaving multicast group: %s', group ) + sock.setsockopt( + socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, + socket.inet_aton(group) + socket.inet_aton('0.0.0.0') + ) diff --git a/testmulticast.py b/testmulticast.py index 09de4d45..a9950ab9 100755 --- a/testmulticast.py +++ b/testmulticast.py @@ -1,22 +1,26 @@ #! /usr/bin/env python """This script simply tests that the Zeroconf multicast setup works on your machine""" -import Zeroconf,socket,os,sys,select,logging +import mcastsocket,socket,os,sys,select,logging +import Zeroconf def main(ip): """Create a multicast socket, send a message, check it comes back""" - sock = Zeroconf.Zeroconf.create_socket( (ip,Zeroconf._MDNS_PORT) ) - Zeroconf.Zeroconf.join_group( sock, Zeroconf._MDNS_ADDR ) - payload = 'hello world' - for i in range( 5 ): - sock.sendto( payload, 0, (Zeroconf._MDNS_ADDR, Zeroconf._MDNS_PORT)) - print 'Waiting for looped message receipt' - rs,wr,xs = select.select( [sock],[],[], 1.0 ) - data,(addr,port) = sock.recvfrom( 200 ) - if data == payload: - print 'Success: looped message received' - return 0 - print 'Failure: Looped message not received' - return 1 + sock = mcastsocket.create_socket( (ip,Zeroconf._MDNS_PORT) ) + mcastsocket.join_group( sock, Zeroconf._MDNS_ADDR ) + try: + payload = 'hello world' + for i in range( 5 ): + sock.sendto( payload, 0, (Zeroconf._MDNS_ADDR, Zeroconf._MDNS_PORT)) + print 'Waiting for looped message receipt' + rs,wr,xs = select.select( [sock],[],[], 1.0 ) + data,(addr,port) = sock.recvfrom( 200 ) + if data == payload: + print 'Success: looped message received' + return 0 + print 'Failure: Looped message not received' + return 1 + finally: + mcastsocket.leave_group( sock, Zeroconf._MDNS_ADDR ) if __name__ == "__main__": logging.basicConfig( level = logging.DEBUG ) From e48b6a7f0bac5b0234e527645f180b8b0765f25c Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Sun, 14 Nov 2010 13:40:05 -0500 Subject: [PATCH 05/36] Refactored the multicast code into a separate module --- mcastsocket.py | 93 +++++++++++++++++++++++++++++++++++++----------- testmulticast.py | 10 ++++-- 2 files changed, 80 insertions(+), 23 deletions(-) diff --git a/mcastsocket.py b/mcastsocket.py index b96dc3cd..28252a0b 100644 --- a/mcastsocket.py +++ b/mcastsocket.py @@ -3,6 +3,28 @@ This is refactored from the Zeroconf.py main module to allow for reuse within multiple environments (e.g. multicast SIP configuration, multicast paging groups and the like). + + Multicast DNS Service Discovery for Python, v0.12 + Copyright (C) 2003, Paul Scott-Murphy + + This module provides a framework for the use of DNS Service Discovery + using IP multicast. It has been tested against the JRendezvous + implementation from StrangeBerry, + and against the mDNSResponder from Mac OS X 10.3.8. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import socket,logging log = logging.getLogger( __name__ ) @@ -14,44 +36,73 @@ def create_socket( address, TTL=1, loop=True, reuse=True ): ip in address[0], and bound on all interfaces with port address[1]. Configures TTL and loop-back operation + address -- IP address family address ('ip',port) on which to listen/broadcast + TTL -- multicast TTL to set on the socket + loop -- whether to reflect our sent messages to our listening port + reuse -- whether to set up socket reuse parameters before binding + + returns socket.socket instance configured as specified """ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, TTL) sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, int(bool(loop))) - if reuse: - try: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - except: - # SO_REUSEADDR should be equivalent to SO_REUSEPORT for - # multicast UDP sockets (p 731, "TCP/IP Illustrated, - # Volume 2"), but some BSD-derived systems require - # SO_REUSEPORT to be specified explicity. Also, not all - # versions of Python have SO_REUSEPORT available. So - # if you're on a BSD-based system, and haven't upgraded - # to Python 2.3 yet, you may find this library doesn't - # work as expected. - # - pass + allow_reuse( sock, reuse ) try: # Note: multicast is *not* working if we don't bind on all interfaces, most likely - # because the 224.* isn't getting mapped to the address of the interface... + # because the 224.* isn't getting mapped (routed) to the address of the interface... + # to debug that case, see if {{{ip route add 224.0.0.0/4 dev br0}}} (or whatever your + # interface is) makes the route suddenly start working... sock.bind(('',address[1])) except Exception, err: # Some versions of linux raise an exception even though # the SO_REUSE* options have been set, so ignore it - # log.error('Failure binding: %s', err) - if address[0]: + limit_to_interface( sock, address[0] ) + return sock + +def limit_to_interface( sock, interface_ip ): + """Restrict multicast operation to the given interface/ip (instead of using routing) + + Sets the IP_MULTICAST_IF option on the socket to restrict multicast + operations to a particular interface. This is done without reference + to the system routing tables, so you do not need to set up a 224.0.0.0/4 + route on the system to receive multicast on the interface. + """ + if interface_ip: # listen/send on a single interface... - interface_ip = address[0] - log.debug( 'Setting multicast to use interface of %s', address[0] ) + log.debug( 'Setting multicast to use interface of %s', interface_ip ) sock.setsockopt( socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton( interface_ip) + socket.inet_aton('0.0.0.0') ) - return sock + return True + return False + +def allow_reuse( sock, reuse=True ): + """Setup reuse parameters on the given socket + + The common case where e.g. the host system has avahi or mdnsresponder + installed will mean that our mDNS or uPNP port is likely already bound. + This operation sets reuse options so that we can re-bind to the port. + + """ + if reuse: + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + except Exception, err: + # SO_REUSEADDR should be equivalent to SO_REUSEPORT for + # multicast UDP sockets (p 731, "TCP/IP Illustrated, + # Volume 2"), but some BSD-derived systems require + # SO_REUSEPORT to be specified explicity. Also, not all + # versions of Python have SO_REUSEPORT available. So + # if you're on a BSD-based system, and haven't upgraded + # to Python 2.3 yet, you may find this library doesn't + # work as expected. + log.debug( 'Ignoring likely spurious error on setting reuse options: %s', err ) + return True + return False def join_group( sock, group ): """Add our socket to this multicast group""" diff --git a/testmulticast.py b/testmulticast.py index a9950ab9..1a7a563c 100755 --- a/testmulticast.py +++ b/testmulticast.py @@ -1,11 +1,17 @@ #! /usr/bin/env python -"""This script simply tests that the Zeroconf multicast setup works on your machine""" +"""This script simply tests that the multicast setup works on your machine + +We create socket that listens on the Zeroconf mDNS port/address and then +join the mDNS multicast group and send a (malformed) message to the group, +our socket should receive that packet (because we have enabled multicast +loopback on the socket). +""" import mcastsocket,socket,os,sys,select,logging import Zeroconf def main(ip): """Create a multicast socket, send a message, check it comes back""" - sock = mcastsocket.create_socket( (ip,Zeroconf._MDNS_PORT) ) + sock = mcastsocket.create_socket( (ip,Zeroconf._MDNS_PORT), loop=True ) mcastsocket.join_group( sock, Zeroconf._MDNS_ADDR ) try: payload = 'hello world' From 11602ea7b544dfb115ddc3787f71a79e00999141 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Sun, 14 Nov 2010 14:28:09 -0500 Subject: [PATCH 06/36] Further attempts to refactor before major changes... --- Zeroconf.py | 37 ++++++++++++------------------------- mcastsocket.py | 12 +++++++++++- testmdnssd.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 26 deletions(-) create mode 100755 testmdnssd.py diff --git a/Zeroconf.py b/Zeroconf.py index 8ffabb2c..8e8dc9d4 100755 --- a/Zeroconf.py +++ b/Zeroconf.py @@ -859,8 +859,9 @@ def run(self): # No sockets to manage, but we wait for the timeout # or addition of a socket # + log.debug( 'No sockets, waiting %s', self.timeout ) self.condition.acquire() - self.condition.wait(self.timeout) + self.condition.wait(self.timeout/25.) self.condition.release() else: try: @@ -912,7 +913,11 @@ def __init__(self, zeroconf): self.zeroconf.engine.addReader(self, self.zeroconf.socket) def handle_read(self): - data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE) + try: + data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE) + except Exception, err: + log.info( 'Error on recvfrom: %s', err ) + return None self.data = data msg = DNSIncoming(data) if msg.isQuery(): @@ -945,7 +950,10 @@ def __init__(self, zeroconf): def run(self): while 1: - self.zeroconf.wait(10 * 1000) + try: + self.zeroconf.wait(10 * 1000) + except ValueError, err: + break if globals()['_GLOBAL_DONE']: return now = currentTimeMillis() @@ -1475,6 +1483,7 @@ def handleQuery(self, msg, addr, port): if question.type == _TYPE_PTR: for service in self.services.values(): if question.name == service.type: + log.info( 'Service query found %s', service.name ) if out is None: out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name)) @@ -1538,25 +1547,3 @@ def close(self): mcastsocket.leave_group( self.socket, _MDNS_ADDR ) self.socket.close() -# Test a few module features, including service registration, service -# query (for Zoe), and service unregistration. - -if __name__ == '__main__': - print "Multicast DNS Service Discovery for Python, version", __version__ - r = Zeroconf() - print "1. Testing registration of a service..." - desc = {'version':'0.10','a':'test value', 'b':'another value'} - info = ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc) - print " Registering service..." - r.registerService(info) - print " Registration done." - print "2. Testing query of service information..." - print " Getting ZOE service:", str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local.")) - print " Query done." - print "3. Testing query of own service..." - print " Getting self:", str(r.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.")) - print " Query done." - print "4. Testing unregister of service information..." - r.unregisterService(info) - print " Unregister done." - r.close() diff --git a/mcastsocket.py b/mcastsocket.py index 28252a0b..bc884eab 100644 --- a/mcastsocket.py +++ b/mcastsocket.py @@ -36,7 +36,10 @@ def create_socket( address, TTL=1, loop=True, reuse=True ): ip in address[0], and bound on all interfaces with port address[1]. Configures TTL and loop-back operation - address -- IP address family address ('ip',port) on which to listen/broadcast + address -- IP address family address ('ip',port) on which to listen/broadcast, + the port is always bound to all interfaces, but the use of an ip will cause + the IP_MULTICAST_IF option to be set in order to direct messages solely to + a given port. TTL -- multicast TTL to set on the socket loop -- whether to reflect our sent messages to our listening port reuse -- whether to set up socket reuse parameters before binding @@ -88,9 +91,16 @@ def allow_reuse( sock, reuse=True ): """ if reuse: + log.debug( 'Setting address/port reuse on mcast socket' ) try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + except AttributeError, err: + # ignore common case where SO_REUSEPORT isn't provided on Linux + if err.args[0].find('SO_REUSEPORT') > -1: + pass + else: + raise except Exception, err: # SO_REUSEADDR should be equivalent to SO_REUSEPORT for # multicast UDP sockets (p 731, "TCP/IP Illustrated, diff --git a/testmdnssd.py b/testmdnssd.py new file mode 100755 index 00000000..fe613305 --- /dev/null +++ b/testmdnssd.py @@ -0,0 +1,37 @@ +#! /usr/bin/env python +import logging,socket,sys,os,Zeroconf + +# Test a few module features, including service registration, service +# query (for Zoe), and service unregistration. + +def main(ip=None): + print "Multicast DNS Service Discovery for Python, version", Zeroconf.__version__ + r = Zeroconf.Zeroconf(ip or '') + host_ip = socket.gethostbyname( socket.gethostname()) + try: + print "1. Testing registration of a service..." + desc = {'version':'0.10','a':'test value', 'b':'another value'} + info = Zeroconf.ServiceInfo( + "_http._tcp.local.", "My Service Name._http._tcp.local.", + socket.inet_aton(host_ip), 1234, 0, 0, desc + ) + print " Registering service..." + r.registerService(info) + print " Registration done." + print "2. Testing query of service information..." + print " Getting ZOE service:", str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local.")) + print " Query done." + print "3. Testing query of own service..." + my_service = r.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.") + print " Getting self:", str(my_service) + print " Query done." + print "4. Testing unregister of service information..." + r.unregisterService(info) + print " Unregister done." + finally: + r.close() + +if __name__ == '__main__': + logging.basicConfig( level = logging.INFO ) + usage = 'testmdnssd.py [ip.address]' + sys.exit( main(*sys.argv[1:]) ) From 56cbc2c5c136291a3d915ac6d2819f37f6c8d9e7 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Sun, 14 Nov 2010 14:37:46 -0500 Subject: [PATCH 07/36] Start refactoring the threaded implementation --- Zeroconf.py | 839 +----------------------------------- zeroconf/__init__.py | 0 zeroconf/dns.py | 985 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 988 insertions(+), 836 deletions(-) create mode 100644 zeroconf/__init__.py create mode 100644 zeroconf/dns.py diff --git a/Zeroconf.py b/Zeroconf.py index 8e8dc9d4..ce78be47 100755 --- a/Zeroconf.py +++ b/Zeroconf.py @@ -78,6 +78,7 @@ __email__ = "paul at scott dash murphy dot com" __version__ = "0.12" + import string import time import struct @@ -88,6 +89,8 @@ import logging import mcastsocket log = logging.getLogger(__name__) +from zeroconf.dns import * + __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"] # hook for threads @@ -206,632 +209,6 @@ class AbstractMethodException(Exception): class BadTypeInNameException(Exception): pass -# implementation classes - -class DNSEntry(object): - """A DNS entry""" - - def __init__(self, name, type, clazz): - self.key = string.lower(name) - self.name = name - self.type = type - self.clazz = clazz & _CLASS_MASK - self.unique = (clazz & _CLASS_UNIQUE) != 0 - - def __eq__(self, other): - """Equality test on name, type, and class""" - if isinstance(other, DNSEntry): - return self.name == other.name and self.type == other.type and self.clazz == other.clazz - return 0 - - def __ne__(self, other): - """Non-equality test""" - return not self.__eq__(other) - - def getClazz(self, clazz): - """Class accessor""" - try: - return _CLASSES[clazz] - except: - return "?(%s)" % (clazz) - - def getType(self, type): - """Type accessor""" - try: - return _TYPES[type] - except: - return "?(%s)" % (type) - - def toString(self, hdr, other): - """String representation with additional information""" - result = "%s[%s,%s" % (hdr, self.getType(self.type), self.getClazz(self.clazz)) - if self.unique: - result += "-unique," - else: - result += "," - result += self.name - if other is not None: - result += ",%s]" % (other) - else: - result += "]" - return result - -class DNSQuestion(DNSEntry): - """A DNS question entry""" - - def __init__(self, name, type, clazz): - if not name.endswith(".local."): - raise NonLocalNameException - DNSEntry.__init__(self, name, type, clazz) - - def answeredBy(self, rec): - """Returns true if the question is answered by the record""" - return self.clazz == rec.clazz and (self.type == rec.type or self.type == _TYPE_ANY) and self.name == rec.name - - def __repr__(self): - """String representation""" - return DNSEntry.toString(self, "question", None) - __str__ = __repr__ - -class DNSRecord(DNSEntry): - """A DNS record - like a DNS entry, but has a TTL""" - - def __init__(self, name, type, clazz, ttl): - DNSEntry.__init__(self, name, type, clazz) - self.ttl = ttl - self.created = currentTimeMillis() - - def __eq__(self, other): - """Tests equality as per DNSRecord""" - if isinstance(other, DNSRecord): - return DNSEntry.__eq__(self, other) - return 0 - - def suppressedBy(self, msg): - """Returns true if any answer in a message can suffice for the - information held in this record.""" - for record in msg.answers: - if self.suppressedByAnswer(record): - return 1 - return 0 - - def suppressedByAnswer(self, other): - """Returns true if another record has same name, type and class, - and if its TTL is at least half of this record's.""" - if self == other and other.ttl > (self.ttl / 2): - return 1 - return 0 - - def getExpirationTime(self, percent): - """Returns the time at which this record will have expired - by a certain percentage.""" - return self.created + (percent * self.ttl * 10) - - def getRemainingTTL(self, now): - """Returns the remaining TTL in seconds.""" - return max(0, (self.getExpirationTime(100) - now) / 1000) - - def isExpired(self, now): - """Returns true if this record has expired.""" - return self.getExpirationTime(100) <= now - - def isStale(self, now): - """Returns true if this record is at least half way expired.""" - return self.getExpirationTime(50) <= now - - def resetTTL(self, other): - """Sets this record's TTL and created time to that of - another record.""" - self.created = other.created - self.ttl = other.ttl - - def write(self, out): - """Abstract method""" - raise AbstractMethodException - - def toString(self, other): - """String representation with addtional information""" - arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other) - return DNSEntry.toString(self, "record", arg) - -class DNSAddress(DNSRecord): - """A DNS address record""" - - def __init__(self, name, type, clazz, ttl, address): - DNSRecord.__init__(self, name, type, clazz, ttl) - self.address = address - - def write(self, out): - """Used in constructing an outgoing packet""" - out.writeString(self.address, len(self.address)) - - def __eq__(self, other): - """Tests equality on address""" - if isinstance(other, DNSAddress): - return self.address == other.address - return 0 - - def __repr__(self): - """String representation""" - try: - return socket.inet_ntoa(self.address) - except: - return self.address - -class DNSHinfo(DNSRecord): - """A DNS host information record""" - - def __init__(self, name, type, clazz, ttl, cpu, os): - DNSRecord.__init__(self, name, type, clazz, ttl) - self.cpu = cpu - self.os = os - - def write(self, out): - """Used in constructing an outgoing packet""" - out.writeString(self.cpu, len(self.cpu)) - out.writeString(self.os, len(self.os)) - - def __eq__(self, other): - """Tests equality on cpu and os""" - if isinstance(other, DNSHinfo): - return self.cpu == other.cpu and self.os == other.os - return 0 - - def __repr__(self): - """String representation""" - return self.cpu + " " + self.os - -class DNSPointer(DNSRecord): - """A DNS pointer record""" - - def __init__(self, name, type, clazz, ttl, alias): - DNSRecord.__init__(self, name, type, clazz, ttl) - self.alias = alias - - def write(self, out): - """Used in constructing an outgoing packet""" - out.writeName(self.alias) - - def __eq__(self, other): - """Tests equality on alias""" - if isinstance(other, DNSPointer): - return self.alias == other.alias - return 0 - - def __repr__(self): - """String representation""" - return self.toString(self.alias) - -class DNSText(DNSRecord): - """A DNS text record""" - - def __init__(self, name, type, clazz, ttl, text): - DNSRecord.__init__(self, name, type, clazz, ttl) - self.text = text - - def write(self, out): - """Used in constructing an outgoing packet""" - out.writeString(self.text, len(self.text)) - - def __eq__(self, other): - """Tests equality on text""" - if isinstance(other, DNSText): - return self.text == other.text - return 0 - - def __repr__(self): - """String representation""" - if len(self.text) > 10: - return self.toString(self.text[:7] + "...") - else: - return self.toString(self.text) - -class DNSService(DNSRecord): - """A DNS service record""" - - def __init__(self, name, type, clazz, ttl, priority, weight, port, server): - DNSRecord.__init__(self, name, type, clazz, ttl) - self.priority = priority - self.weight = weight - self.port = port - self.server = server - - def write(self, out): - """Used in constructing an outgoing packet""" - out.writeShort(self.priority) - out.writeShort(self.weight) - out.writeShort(self.port) - out.writeName(self.server) - - def __eq__(self, other): - """Tests equality on priority, weight, port and server""" - if isinstance(other, DNSService): - return self.priority == other.priority and self.weight == other.weight and self.port == other.port and self.server == other.server - return 0 - - def __repr__(self): - """String representation""" - return self.toString("%s:%s" % (self.server, self.port)) - -class DNSIncoming(object): - """Object representation of an incoming DNS packet""" - - def __init__(self, data): - """Constructor from string holding bytes of packet""" - self.offset = 0 - self.data = data - self.questions = [] - self.answers = [] - self.numQuestions = 0 - self.numAnswers = 0 - self.numAuthorities = 0 - self.numAdditionals = 0 - - self.readHeader() - self.readQuestions() - self.readOthers() - - def readHeader(self): - """Reads header portion of packet""" - format = '!HHHHHH' - length = struct.calcsize(format) - info = struct.unpack(format, self.data[self.offset:self.offset+length]) - self.offset += length - - self.id = info[0] - self.flags = info[1] - self.numQuestions = info[2] - self.numAnswers = info[3] - self.numAuthorities = info[4] - self.numAdditionals = info[5] - - def readQuestions(self): - """Reads questions section of packet""" - format = '!HH' - length = struct.calcsize(format) - for i in range(0, self.numQuestions): - name = self.readName() - info = struct.unpack(format, self.data[self.offset:self.offset+length]) - self.offset += length - - question = DNSQuestion(name, info[0], info[1]) - self.questions.append(question) - - def readInt(self): - """Reads an integer from the packet""" - format = '!I' - length = struct.calcsize(format) - info = struct.unpack(format, self.data[self.offset:self.offset+length]) - self.offset += length - return info[0] - - def readCharacterString(self): - """Reads a character string from the packet""" - length = ord(self.data[self.offset]) - self.offset += 1 - return self.readString(length) - - def readString(self, len): - """Reads a string of a given length from the packet""" - format = '!' + str(len) + 's' - length = struct.calcsize(format) - info = struct.unpack(format, self.data[self.offset:self.offset+length]) - self.offset += length - return info[0] - - def readUnsignedShort(self): - """Reads an unsigned short from the packet""" - format = '!H' - length = struct.calcsize(format) - info = struct.unpack(format, self.data[self.offset:self.offset+length]) - self.offset += length - return info[0] - - def readOthers(self): - """Reads the answers, authorities and additionals section of the packet""" - format = '!HHiH' - length = struct.calcsize(format) - n = self.numAnswers + self.numAuthorities + self.numAdditionals - for i in range(0, n): - domain = self.readName() - info = struct.unpack(format, self.data[self.offset:self.offset+length]) - self.offset += length - - try: - rec = None - if info[0] == _TYPE_A: - rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4)) - elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR: - rec = DNSPointer(domain, info[0], info[1], info[2], self.readName()) - elif info[0] == _TYPE_TXT: - rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3])) - elif info[0] == _TYPE_SRV: - rec = DNSService(domain, info[0], info[1], info[2], self.readUnsignedShort(), self.readUnsignedShort(), self.readUnsignedShort(), self.readName()) - elif info[0] == _TYPE_HINFO: - rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString()) - elif info[0] == _TYPE_AAAA: - rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16)) - else: - # Try to ignore types we don't know about - # this may mean the rest of the name is - # unable to be parsed, and may show errors - # so this is left for debugging. New types - # encountered need to be parsed properly. - # - log.warn( - "Unknown DNS query type: %s", info[0] - ) - - if rec is not None: - self.answers.append(rec) - except Exception, err: - log.warn( "Failure on record type %s, ignoring: %s", info[0], err ) - - def isQuery(self): - """Returns true if this is a query""" - return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY - - def isResponse(self): - """Returns true if this is a response""" - return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE - - def readUTF(self, offset, len): - """Reads a UTF-8 string of a given length from the packet - - TODO: there are cases were non-utf-8 data comes through, - we need to decide how to properly handle these. - """ - return self.data[offset:offset+len].decode('utf-8','ignore') - - def readName(self): - """Reads a domain name from the packet""" - result = '' - off = self.offset - next = -1 - first = off - - while 1: - len = ord(self.data[off]) - off += 1 - if len == 0: - break - t = len & 0xC0 - if t == 0x00: - result = ''.join((result, self.readUTF(off, len) + '.')) - off += len - elif t == 0xC0: - if next < 0: - next = off + 1 - off = ((len & 0x3F) << 8) | ord(self.data[off]) - if off >= first: - raise "Bad domain name (circular) at " + str(off) - first = off - else: - raise "Bad domain name at " + str(off) - - if next >= 0: - self.offset = next - else: - self.offset = off - - return result - - -class DNSOutgoing(object): - """Object representation of an outgoing packet""" - - def __init__(self, flags, multicast = 1): - self.finished = 0 - self.id = 0 - self.multicast = multicast - self.flags = flags - self.names = {} - self.data = [] - self.size = 12 - - self.questions = [] - self.answers = [] - self.authorities = [] - self.additionals = [] - - def addQuestion(self, record): - """Adds a question""" - self.questions.append(record) - - def addAnswer(self, inp, record): - """Adds an answer""" - if not record.suppressedBy(inp): - self.addAnswerAtTime(record, 0) - - def addAnswerAtTime(self, record, now): - """Adds an answer if if does not expire by a certain time""" - if record is not None: - if now == 0 or not record.isExpired(now): - self.answers.append((record, now)) - - def addAuthorativeAnswer(self, record): - """Adds an authoritative answer""" - self.authorities.append(record) - - def addAdditionalAnswer(self, record): - """Adds an additional answer""" - self.additionals.append(record) - - def writeByte(self, value): - """Writes a single byte to the packet""" - format = '!c' - self.data.append(struct.pack(format, chr(value))) - self.size += 1 - - def insertShort(self, index, value): - """Inserts an unsigned short in a certain position in the packet""" - format = '!H' - self.data.insert(index, struct.pack(format, value)) - self.size += 2 - - def writeShort(self, value): - """Writes an unsigned short to the packet""" - format = '!H' - self.data.append(struct.pack(format, value)) - self.size += 2 - - def writeInt(self, value): - """Writes an unsigned integer to the packet""" - format = '!I' - self.data.append(struct.pack(format, long(value))) - self.size += 4 - - def writeString(self, value, length): - """Writes a string to the packet""" - format = '!' + str(length) + 's' - self.data.append(struct.pack(format, value)) - self.size += length - - def writeUTF(self, s): - """Writes a UTF-8 string of a given length to the packet""" - utfstr = s.encode('utf-8') - length = len(utfstr) - if length > 64: - raise NamePartTooLongException - self.writeByte(length) - self.writeString(utfstr, length) - - def writeName(self, name): - """Writes a domain name to the packet""" - - try: - # Find existing instance of this name in packet - # - index = self.names[name] - except KeyError: - # No record of this name already, so write it - # out as normal, recording the location of the name - # for future pointers to it. - # - self.names[name] = self.size - parts = name.split('.') - if parts[-1] == '': - parts = parts[:-1] - for part in parts: - self.writeUTF(part) - self.writeByte(0) - return - - # An index was found, so write a pointer to it - # - self.writeByte((index >> 8) | 0xC0) - self.writeByte(index) - - def writeQuestion(self, question): - """Writes a question to the packet""" - self.writeName(question.name) - self.writeShort(question.type) - self.writeShort(question.clazz) - - def writeRecord(self, record, now): - """Writes a record (answer, authoritative answer, additional) to - the packet""" - self.writeName(record.name) - self.writeShort(record.type) - if record.unique and self.multicast: - self.writeShort(record.clazz | _CLASS_UNIQUE) - else: - self.writeShort(record.clazz) - if now == 0: - self.writeInt(record.ttl) - else: - self.writeInt(record.getRemainingTTL(now)) - index = len(self.data) - # Adjust size for the short we will write before this record - # - self.size += 2 - record.write(self) - self.size -= 2 - - length = len(''.join(self.data[index:])) - self.insertShort(index, length) # Here is the short we adjusted for - - def packet(self): - """Returns a string containing the packet's bytes - - No further parts should be added to the packet once this - is done.""" - if not self.finished: - self.finished = 1 - for question in self.questions: - self.writeQuestion(question) - for answer, time in self.answers: - self.writeRecord(answer, time) - for authority in self.authorities: - self.writeRecord(authority, 0) - for additional in self.additionals: - self.writeRecord(additional, 0) - - self.insertShort(0, len(self.additionals)) - self.insertShort(0, len(self.authorities)) - self.insertShort(0, len(self.answers)) - self.insertShort(0, len(self.questions)) - self.insertShort(0, self.flags) - if self.multicast: - self.insertShort(0, 0) - else: - self.insertShort(0, self.id) - return ''.join(self.data) - - -class DNSCache(object): - """A cache of DNS entries""" - - def __init__(self): - self.cache = {} - - def add(self, entry): - """Adds an entry""" - try: - list = self.cache[entry.key] - except: - list = self.cache[entry.key] = [] - list.append(entry) - - def remove(self, entry): - """Removes an entry""" - try: - list = self.cache[entry.key] - list.remove(entry) - except: - pass - - def get(self, entry): - """Gets an entry by key. Will return None if there is no - matching entry.""" - try: - list = self.cache[entry.key] - return list[list.index(entry)] - except: - return None - - def getByDetails(self, name, type, clazz): - """Gets an entry by details. Will return None if there is - no matching entry.""" - entry = DNSEntry(name, type, clazz) - return self.get(entry) - - def entriesWithName(self, name): - """Returns a list of entries whose key matches the name.""" - try: - return self.cache[name] - except: - return [] - - def entries(self): - """Returns a list of all entries""" - def add(x, y): return x+y - try: - return reduce(add, self.cache.values()) - except: - return [] - - class Engine(threading.Thread): """An engine wraps read access to sockets, allowing objects that need to receive data from sockets to be called back when the @@ -1042,216 +419,6 @@ def run(self): if event is not None: event(self.zeroconf) - -class ServiceInfo(object): - """Service information""" - - def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None): - """Create a service description. - - type: fully qualified service type name - name: fully qualified service name - address: IP address as unsigned short, network byte order - port: port that the service runs on - weight: weight of the service - priority: priority of the service - properties: dictionary of properties (or a string holding the bytes for the text field) - server: fully qualified name for service host (defaults to name)""" - - if not name.endswith(type): - raise BadTypeInNameException - self.type = type - self.name = name - self.address = address - self.port = port - self.weight = weight - self.priority = priority - if server: - self.server = server - else: - self.server = name #'.'.join([x for x in name.split('.') if not x.startswith('_')]) - self.setProperties(properties) - - def setProperties(self, properties): - """Sets properties and text of this info from a dictionary""" - if isinstance(properties, dict): - self.properties = properties - list = [] - result = '' - for key in properties: - value = properties[key] - if value is None: - suffix = ''.encode('utf-8') - elif isinstance(value, str): - suffix = value.encode('utf-8') - elif isinstance(value, int): - if value: - suffix = 'true' - else: - suffix = 'false' - else: - suffix = ''.encode('utf-8') - list.append('='.join((key, suffix))) - for item in list: - result = ''.join((result, struct.pack('!c', chr(len(item))), item)) - self.text = result - else: - self.text = properties - - def setText(self, text): - """Sets properties and text given a text field""" - self.text = text - try: - result = {} - end = len(text) - index = 0 - strs = [] - while index < end: - length = ord(text[index]) - index += 1 - strs.append(text[index:index+length]) - index += length - - for s in strs: - eindex = s.find('=') - if eindex == -1: - # No equals sign at all - key = s - value = 0 - else: - key = s[:eindex] - value = s[eindex+1:] - if value == 'true': - value = 1 - elif value == 'false' or not value: - value = 0 - - # Only update non-existent properties - if key and result.get(key) == None: - result[key] = value - - self.properties = result - except Exception, err: - log.error( "Failure composing text: %s", traceback.format_exc() ) - self.properties = None - - def getType(self): - """Type accessor""" - return self.type - - def getName(self): - """Name accessor""" - if self.type is not None and self.name.endswith("." + self.type): - return self.name[:len(self.name) - len(self.type) - 1] - return self.name - - def getAddress(self): - """Address accessor""" - return self.address - - def getPort(self): - """Port accessor""" - return self.port - - def getPriority(self): - """Pirority accessor""" - return self.priority - - def getWeight(self): - """Weight accessor""" - return self.weight - - def getProperties(self): - """Properties accessor""" - return self.properties - - def getText(self): - """Text accessor""" - return self.text - - def getServer(self): - """Server accessor""" - return self.server - - def updateRecord(self, zeroconf, now, record): - """Updates service information from a DNS record""" - if record is not None and not record.isExpired(now): - if record.type == _TYPE_A: - if record.name == self.name: - self.address = record.address - elif record.type == _TYPE_SRV: - if record.name == self.name: - self.server = record.server - self.port = record.port - self.weight = record.weight - self.priority = record.priority - self.address = None - self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN)) - elif record.type == _TYPE_TXT: - if record.name == self.name: - self.setText(record.text) - - def request(self, zeroconf, timeout): - """Returns true if the service could be discovered on the - network, and updates this object with details discovered. - """ - now = currentTimeMillis() - delay = _LISTENER_TIME - next = now + delay - last = now + timeout - result = 0 - try: - zeroconf.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN)) - while self.server is None or self.address is None or self.text is None: - if last <= now: - return 0 - if next <= now: - out = DNSOutgoing(_FLAGS_QR_QUERY) - out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN)) - out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_SRV, _CLASS_IN), now) - out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN)) - out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_TXT, _CLASS_IN), now) - if self.server is not None: - out.addQuestion(DNSQuestion(self.server, _TYPE_A, _CLASS_IN)) - out.addAnswerAtTime(zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN), now) - zeroconf.send(out) - next = now + delay - delay = delay * 2 - - zeroconf.wait(min(next, last) - now) - now = currentTimeMillis() - result = 1 - finally: - zeroconf.removeListener(self) - - return result - - def __eq__(self, other): - """Tests equality of service name""" - if isinstance(other, ServiceInfo): - return other.name == self.name - return 0 - - def __ne__(self, other): - """Non-equality test""" - return not self.__eq__(other) - - def __repr__(self): - """String representation""" - result = "service[%s,%s:%s," % (self.name, socket.inet_ntoa(self.getAddress()), self.port) - if self.text is None: - result += "None" - else: - if len(self.text) < 20: - result += self.text - else: - result += self.text[:17] + "..." - result += "]" - return result - - - - class Zeroconf(object): """Implementation of Zeroconf Multicast DNS Service Discovery diff --git a/zeroconf/__init__.py b/zeroconf/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/zeroconf/dns.py b/zeroconf/dns.py new file mode 100644 index 00000000..6a82022f --- /dev/null +++ b/zeroconf/dns.py @@ -0,0 +1,985 @@ +""" Multicast DNS Service Discovery for Python, v0.12 + Copyright (C) 2003, Paul Scott-Murphy + + This module provides a framework for the use of DNS Service Discovery + using IP multicast. It has been tested against the JRendezvous + implementation from StrangeBerry, + and against the mDNSResponder from Mac OS X 10.3.8. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +This module contains the mDNS/DNS server required to provide mDNS service +discovery +""" +import string +import time +import struct +import socket +import traceback +import logging +log = logging.getLogger(__name__) +__all__ = [ + 'ServiceInfo', + 'DNSAddress', 'DNSCache', 'DNSEntry', 'DNSHinfo', 'DNSIncoming', + 'DNSOutgoing', 'DNSPointer', 'DNSQuestion', 'DNSRecord', 'DNSService', + 'DNSText', + 'AbstractMethodException', 'BadTypeInNameException', + 'NamePartTooLongException', 'NonLocalNameException', 'NonUniqueNameException', +] + +# Some timing constants + +_UNREGISTER_TIME = 125 +_CHECK_TIME = 175 +_REGISTER_TIME = 225 +_LISTENER_TIME = 200 +_BROWSER_TIME = 500 + +# Some DNS constants + +_MDNS_ADDR = '224.0.0.251' +_MDNS_PORT = 5353; +_DNS_PORT = 53; +_DNS_TTL = 60 * 60; # one hour default TTL + +_MAX_MSG_TYPICAL = 1460 # unused +_MAX_MSG_ABSOLUTE = 8972 + +_FLAGS_QR_MASK = 0x8000 # query response mask +_FLAGS_QR_QUERY = 0x0000 # query +_FLAGS_QR_RESPONSE = 0x8000 # response + +_FLAGS_AA = 0x0400 # Authorative answer +_FLAGS_TC = 0x0200 # Truncated +_FLAGS_RD = 0x0100 # Recursion desired +_FLAGS_RA = 0x8000 # Recursion available + +_FLAGS_Z = 0x0040 # Zero +_FLAGS_AD = 0x0020 # Authentic data +_FLAGS_CD = 0x0010 # Checking disabled + +_CLASS_IN = 1 +_CLASS_CS = 2 +_CLASS_CH = 3 +_CLASS_HS = 4 +_CLASS_NONE = 254 +_CLASS_ANY = 255 +_CLASS_MASK = 0x7FFF +_CLASS_UNIQUE = 0x8000 + +_TYPE_A = 1 +_TYPE_NS = 2 +_TYPE_MD = 3 +_TYPE_MF = 4 +_TYPE_CNAME = 5 +_TYPE_SOA = 6 +_TYPE_MB = 7 +_TYPE_MG = 8 +_TYPE_MR = 9 +_TYPE_NULL = 10 +_TYPE_WKS = 11 +_TYPE_PTR = 12 +_TYPE_HINFO = 13 +_TYPE_MINFO = 14 +_TYPE_MX = 15 +_TYPE_TXT = 16 +_TYPE_AAAA = 28 +_TYPE_SRV = 33 +_TYPE_ANY = 255 + +# Mapping constants to names + +_CLASSES = { _CLASS_IN : "in", + _CLASS_CS : "cs", + _CLASS_CH : "ch", + _CLASS_HS : "hs", + _CLASS_NONE : "none", + _CLASS_ANY : "any" } + +_TYPES = { _TYPE_A : "a", + _TYPE_NS : "ns", + _TYPE_MD : "md", + _TYPE_MF : "mf", + _TYPE_CNAME : "cname", + _TYPE_SOA : "soa", + _TYPE_MB : "mb", + _TYPE_MG : "mg", + _TYPE_MR : "mr", + _TYPE_NULL : "null", + _TYPE_WKS : "wks", + _TYPE_PTR : "ptr", + _TYPE_HINFO : "hinfo", + _TYPE_MINFO : "minfo", + _TYPE_MX : "mx", + _TYPE_TXT : "txt", + _TYPE_AAAA : "quada", + _TYPE_SRV : "srv", + _TYPE_ANY : "any" } + +# utility functions + +def currentTimeMillis(): + """Current system time in milliseconds""" + return time.time() * 1000 + +# Exceptions + +class NonLocalNameException(Exception): + pass + +class NonUniqueNameException(Exception): + pass + +class NamePartTooLongException(Exception): + pass + +class AbstractMethodException(Exception): + pass + +class BadTypeInNameException(Exception): + pass + +# implementation classes + +class DNSEntry(object): + """A DNS entry""" + + def __init__(self, name, type, clazz): + self.key = string.lower(name) + self.name = name + self.type = type + self.clazz = clazz & _CLASS_MASK + self.unique = (clazz & _CLASS_UNIQUE) != 0 + + def __eq__(self, other): + """Equality test on name, type, and class""" + if isinstance(other, DNSEntry): + return self.name == other.name and self.type == other.type and self.clazz == other.clazz + return 0 + + def __ne__(self, other): + """Non-equality test""" + return not self.__eq__(other) + + def getClazz(self, clazz): + """Class accessor""" + try: + return _CLASSES[clazz] + except: + return "?(%s)" % (clazz) + + def getType(self, type): + """Type accessor""" + try: + return _TYPES[type] + except: + return "?(%s)" % (type) + + def toString(self, hdr, other): + """String representation with additional information""" + result = "%s[%s,%s" % (hdr, self.getType(self.type), self.getClazz(self.clazz)) + if self.unique: + result += "-unique," + else: + result += "," + result += self.name + if other is not None: + result += ",%s]" % (other) + else: + result += "]" + return result + +class DNSQuestion(DNSEntry): + """A DNS question entry""" + + def __init__(self, name, type, clazz): + if not name.endswith(".local."): + raise NonLocalNameException + DNSEntry.__init__(self, name, type, clazz) + + def answeredBy(self, rec): + """Returns true if the question is answered by the record""" + return self.clazz == rec.clazz and (self.type == rec.type or self.type == _TYPE_ANY) and self.name == rec.name + + def __repr__(self): + """String representation""" + return DNSEntry.toString(self, "question", None) + __str__ = __repr__ + +class DNSRecord(DNSEntry): + """A DNS record - like a DNS entry, but has a TTL""" + + def __init__(self, name, type, clazz, ttl): + DNSEntry.__init__(self, name, type, clazz) + self.ttl = ttl + self.created = currentTimeMillis() + + def __eq__(self, other): + """Tests equality as per DNSRecord""" + if isinstance(other, DNSRecord): + return DNSEntry.__eq__(self, other) + return 0 + + def suppressedBy(self, msg): + """Returns true if any answer in a message can suffice for the + information held in this record.""" + for record in msg.answers: + if self.suppressedByAnswer(record): + return 1 + return 0 + + def suppressedByAnswer(self, other): + """Returns true if another record has same name, type and class, + and if its TTL is at least half of this record's.""" + if self == other and other.ttl > (self.ttl / 2): + return 1 + return 0 + + def getExpirationTime(self, percent): + """Returns the time at which this record will have expired + by a certain percentage.""" + return self.created + (percent * self.ttl * 10) + + def getRemainingTTL(self, now): + """Returns the remaining TTL in seconds.""" + return max(0, (self.getExpirationTime(100) - now) / 1000) + + def isExpired(self, now): + """Returns true if this record has expired.""" + return self.getExpirationTime(100) <= now + + def isStale(self, now): + """Returns true if this record is at least half way expired.""" + return self.getExpirationTime(50) <= now + + def resetTTL(self, other): + """Sets this record's TTL and created time to that of + another record.""" + self.created = other.created + self.ttl = other.ttl + + def write(self, out): + """Abstract method""" + raise AbstractMethodException + + def toString(self, other): + """String representation with addtional information""" + arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other) + return DNSEntry.toString(self, "record", arg) + +class DNSAddress(DNSRecord): + """A DNS address record""" + + def __init__(self, name, type, clazz, ttl, address): + DNSRecord.__init__(self, name, type, clazz, ttl) + self.address = address + + def write(self, out): + """Used in constructing an outgoing packet""" + out.writeString(self.address, len(self.address)) + + def __eq__(self, other): + """Tests equality on address""" + if isinstance(other, DNSAddress): + return self.address == other.address + return 0 + + def __repr__(self): + """String representation""" + try: + return socket.inet_ntoa(self.address) + except: + return self.address + +class DNSHinfo(DNSRecord): + """A DNS host information record""" + + def __init__(self, name, type, clazz, ttl, cpu, os): + DNSRecord.__init__(self, name, type, clazz, ttl) + self.cpu = cpu + self.os = os + + def write(self, out): + """Used in constructing an outgoing packet""" + out.writeString(self.cpu, len(self.cpu)) + out.writeString(self.os, len(self.os)) + + def __eq__(self, other): + """Tests equality on cpu and os""" + if isinstance(other, DNSHinfo): + return self.cpu == other.cpu and self.os == other.os + return 0 + + def __repr__(self): + """String representation""" + return self.cpu + " " + self.os + +class DNSPointer(DNSRecord): + """A DNS pointer record""" + + def __init__(self, name, type, clazz, ttl, alias): + DNSRecord.__init__(self, name, type, clazz, ttl) + self.alias = alias + + def write(self, out): + """Used in constructing an outgoing packet""" + out.writeName(self.alias) + + def __eq__(self, other): + """Tests equality on alias""" + if isinstance(other, DNSPointer): + return self.alias == other.alias + return 0 + + def __repr__(self): + """String representation""" + return self.toString(self.alias) + +class DNSText(DNSRecord): + """A DNS text record""" + + def __init__(self, name, type, clazz, ttl, text): + DNSRecord.__init__(self, name, type, clazz, ttl) + self.text = text + + def write(self, out): + """Used in constructing an outgoing packet""" + out.writeString(self.text, len(self.text)) + + def __eq__(self, other): + """Tests equality on text""" + if isinstance(other, DNSText): + return self.text == other.text + return 0 + + def __repr__(self): + """String representation""" + if len(self.text) > 10: + return self.toString(self.text[:7] + "...") + else: + return self.toString(self.text) + +class DNSService(DNSRecord): + """A DNS service record""" + + def __init__(self, name, type, clazz, ttl, priority, weight, port, server): + DNSRecord.__init__(self, name, type, clazz, ttl) + self.priority = priority + self.weight = weight + self.port = port + self.server = server + + def write(self, out): + """Used in constructing an outgoing packet""" + out.writeShort(self.priority) + out.writeShort(self.weight) + out.writeShort(self.port) + out.writeName(self.server) + + def __eq__(self, other): + """Tests equality on priority, weight, port and server""" + if isinstance(other, DNSService): + return self.priority == other.priority and self.weight == other.weight and self.port == other.port and self.server == other.server + return 0 + + def __repr__(self): + """String representation""" + return self.toString("%s:%s" % (self.server, self.port)) + +class DNSIncoming(object): + """Object representation of an incoming DNS packet""" + + def __init__(self, data): + """Constructor from string holding bytes of packet""" + self.offset = 0 + self.data = data + self.questions = [] + self.answers = [] + self.numQuestions = 0 + self.numAnswers = 0 + self.numAuthorities = 0 + self.numAdditionals = 0 + + self.readHeader() + self.readQuestions() + self.readOthers() + + def readHeader(self): + """Reads header portion of packet""" + format = '!HHHHHH' + length = struct.calcsize(format) + info = struct.unpack(format, self.data[self.offset:self.offset+length]) + self.offset += length + + self.id = info[0] + self.flags = info[1] + self.numQuestions = info[2] + self.numAnswers = info[3] + self.numAuthorities = info[4] + self.numAdditionals = info[5] + + def readQuestions(self): + """Reads questions section of packet""" + format = '!HH' + length = struct.calcsize(format) + for i in range(0, self.numQuestions): + name = self.readName() + info = struct.unpack(format, self.data[self.offset:self.offset+length]) + self.offset += length + + question = DNSQuestion(name, info[0], info[1]) + self.questions.append(question) + + def readInt(self): + """Reads an integer from the packet""" + format = '!I' + length = struct.calcsize(format) + info = struct.unpack(format, self.data[self.offset:self.offset+length]) + self.offset += length + return info[0] + + def readCharacterString(self): + """Reads a character string from the packet""" + length = ord(self.data[self.offset]) + self.offset += 1 + return self.readString(length) + + def readString(self, len): + """Reads a string of a given length from the packet""" + format = '!' + str(len) + 's' + length = struct.calcsize(format) + info = struct.unpack(format, self.data[self.offset:self.offset+length]) + self.offset += length + return info[0] + + def readUnsignedShort(self): + """Reads an unsigned short from the packet""" + format = '!H' + length = struct.calcsize(format) + info = struct.unpack(format, self.data[self.offset:self.offset+length]) + self.offset += length + return info[0] + + def readOthers(self): + """Reads the answers, authorities and additionals section of the packet""" + format = '!HHiH' + length = struct.calcsize(format) + n = self.numAnswers + self.numAuthorities + self.numAdditionals + for i in range(0, n): + domain = self.readName() + info = struct.unpack(format, self.data[self.offset:self.offset+length]) + self.offset += length + + try: + rec = None + if info[0] == _TYPE_A: + rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4)) + elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR: + rec = DNSPointer(domain, info[0], info[1], info[2], self.readName()) + elif info[0] == _TYPE_TXT: + rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3])) + elif info[0] == _TYPE_SRV: + rec = DNSService(domain, info[0], info[1], info[2], self.readUnsignedShort(), self.readUnsignedShort(), self.readUnsignedShort(), self.readName()) + elif info[0] == _TYPE_HINFO: + rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString()) + elif info[0] == _TYPE_AAAA: + rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16)) + else: + # Try to ignore types we don't know about + # this may mean the rest of the name is + # unable to be parsed, and may show errors + # so this is left for debugging. New types + # encountered need to be parsed properly. + # + log.warn( + "Unknown DNS query type: %s", info[0] + ) + + if rec is not None: + self.answers.append(rec) + except Exception, err: + log.warn( "Failure on record type %s, ignoring: %s", info[0], err ) + + def isQuery(self): + """Returns true if this is a query""" + return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY + + def isResponse(self): + """Returns true if this is a response""" + return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE + + def readUTF(self, offset, len): + """Reads a UTF-8 string of a given length from the packet + + TODO: there are cases were non-utf-8 data comes through, + we need to decide how to properly handle these. + """ + return self.data[offset:offset+len].decode('utf-8','ignore') + + def readName(self): + """Reads a domain name from the packet""" + result = '' + off = self.offset + next = -1 + first = off + + while 1: + len = ord(self.data[off]) + off += 1 + if len == 0: + break + t = len & 0xC0 + if t == 0x00: + result = ''.join((result, self.readUTF(off, len) + '.')) + off += len + elif t == 0xC0: + if next < 0: + next = off + 1 + off = ((len & 0x3F) << 8) | ord(self.data[off]) + if off >= first: + raise "Bad domain name (circular) at " + str(off) + first = off + else: + raise "Bad domain name at " + str(off) + + if next >= 0: + self.offset = next + else: + self.offset = off + + return result + + +class DNSOutgoing(object): + """Object representation of an outgoing packet""" + + def __init__(self, flags, multicast = 1): + self.finished = 0 + self.id = 0 + self.multicast = multicast + self.flags = flags + self.names = {} + self.data = [] + self.size = 12 + + self.questions = [] + self.answers = [] + self.authorities = [] + self.additionals = [] + + def addQuestion(self, record): + """Adds a question""" + self.questions.append(record) + + def addAnswer(self, inp, record): + """Adds an answer""" + if not record.suppressedBy(inp): + self.addAnswerAtTime(record, 0) + + def addAnswerAtTime(self, record, now): + """Adds an answer if if does not expire by a certain time""" + if record is not None: + if now == 0 or not record.isExpired(now): + self.answers.append((record, now)) + + def addAuthorativeAnswer(self, record): + """Adds an authoritative answer""" + self.authorities.append(record) + + def addAdditionalAnswer(self, record): + """Adds an additional answer""" + self.additionals.append(record) + + def writeByte(self, value): + """Writes a single byte to the packet""" + format = '!c' + self.data.append(struct.pack(format, chr(value))) + self.size += 1 + + def insertShort(self, index, value): + """Inserts an unsigned short in a certain position in the packet""" + format = '!H' + self.data.insert(index, struct.pack(format, value)) + self.size += 2 + + def writeShort(self, value): + """Writes an unsigned short to the packet""" + format = '!H' + self.data.append(struct.pack(format, value)) + self.size += 2 + + def writeInt(self, value): + """Writes an unsigned integer to the packet""" + format = '!I' + self.data.append(struct.pack(format, long(value))) + self.size += 4 + + def writeString(self, value, length): + """Writes a string to the packet""" + format = '!' + str(length) + 's' + self.data.append(struct.pack(format, value)) + self.size += length + + def writeUTF(self, s): + """Writes a UTF-8 string of a given length to the packet""" + utfstr = s.encode('utf-8') + length = len(utfstr) + if length > 64: + raise NamePartTooLongException + self.writeByte(length) + self.writeString(utfstr, length) + + def writeName(self, name): + """Writes a domain name to the packet""" + + try: + # Find existing instance of this name in packet + # + index = self.names[name] + except KeyError: + # No record of this name already, so write it + # out as normal, recording the location of the name + # for future pointers to it. + # + self.names[name] = self.size + parts = name.split('.') + if parts[-1] == '': + parts = parts[:-1] + for part in parts: + self.writeUTF(part) + self.writeByte(0) + return + + # An index was found, so write a pointer to it + # + self.writeByte((index >> 8) | 0xC0) + self.writeByte(index) + + def writeQuestion(self, question): + """Writes a question to the packet""" + self.writeName(question.name) + self.writeShort(question.type) + self.writeShort(question.clazz) + + def writeRecord(self, record, now): + """Writes a record (answer, authoritative answer, additional) to + the packet""" + self.writeName(record.name) + self.writeShort(record.type) + if record.unique and self.multicast: + self.writeShort(record.clazz | _CLASS_UNIQUE) + else: + self.writeShort(record.clazz) + if now == 0: + self.writeInt(record.ttl) + else: + self.writeInt(record.getRemainingTTL(now)) + index = len(self.data) + # Adjust size for the short we will write before this record + # + self.size += 2 + record.write(self) + self.size -= 2 + + length = len(''.join(self.data[index:])) + self.insertShort(index, length) # Here is the short we adjusted for + + def packet(self): + """Returns a string containing the packet's bytes + + No further parts should be added to the packet once this + is done.""" + if not self.finished: + self.finished = 1 + for question in self.questions: + self.writeQuestion(question) + for answer, time in self.answers: + self.writeRecord(answer, time) + for authority in self.authorities: + self.writeRecord(authority, 0) + for additional in self.additionals: + self.writeRecord(additional, 0) + + self.insertShort(0, len(self.additionals)) + self.insertShort(0, len(self.authorities)) + self.insertShort(0, len(self.answers)) + self.insertShort(0, len(self.questions)) + self.insertShort(0, self.flags) + if self.multicast: + self.insertShort(0, 0) + else: + self.insertShort(0, self.id) + return ''.join(self.data) + + +class DNSCache(object): + """A cache of DNS entries""" + + def __init__(self): + self.cache = {} + + def add(self, entry): + """Adds an entry""" + try: + list = self.cache[entry.key] + except: + list = self.cache[entry.key] = [] + list.append(entry) + + def remove(self, entry): + """Removes an entry""" + try: + list = self.cache[entry.key] + list.remove(entry) + except: + pass + + def get(self, entry): + """Gets an entry by key. Will return None if there is no + matching entry.""" + try: + list = self.cache[entry.key] + return list[list.index(entry)] + except: + return None + + def getByDetails(self, name, type, clazz): + """Gets an entry by details. Will return None if there is + no matching entry.""" + entry = DNSEntry(name, type, clazz) + return self.get(entry) + + def entriesWithName(self, name): + """Returns a list of entries whose key matches the name.""" + try: + return self.cache[name] + except: + return [] + + def entries(self): + """Returns a list of all entries""" + def add(x, y): return x+y + try: + return reduce(add, self.cache.values()) + except: + return [] + +class ServiceInfo(object): + """Service information""" + + def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None): + """Create a service description. + + type: fully qualified service type name + name: fully qualified service name + address: IP address as unsigned short, network byte order + port: port that the service runs on + weight: weight of the service + priority: priority of the service + properties: dictionary of properties (or a string holding the bytes for the text field) + server: fully qualified name for service host (defaults to name)""" + + if not name.endswith(type): + raise BadTypeInNameException + self.type = type + self.name = name + self.address = address + self.port = port + self.weight = weight + self.priority = priority + if server: + self.server = server + else: + self.server = name #'.'.join([x for x in name.split('.') if not x.startswith('_')]) + self.setProperties(properties) + + def setProperties(self, properties): + """Sets properties and text of this info from a dictionary""" + if isinstance(properties, dict): + self.properties = properties + list = [] + result = '' + for key in properties: + value = properties[key] + if value is None: + suffix = ''.encode('utf-8') + elif isinstance(value, str): + suffix = value.encode('utf-8') + elif isinstance(value, int): + if value: + suffix = 'true' + else: + suffix = 'false' + else: + suffix = ''.encode('utf-8') + list.append('='.join((key, suffix))) + for item in list: + result = ''.join((result, struct.pack('!c', chr(len(item))), item)) + self.text = result + else: + self.text = properties + + def setText(self, text): + """Sets properties and text given a text field""" + self.text = text + try: + result = {} + end = len(text) + index = 0 + strs = [] + while index < end: + length = ord(text[index]) + index += 1 + strs.append(text[index:index+length]) + index += length + + for s in strs: + eindex = s.find('=') + if eindex == -1: + # No equals sign at all + key = s + value = 0 + else: + key = s[:eindex] + value = s[eindex+1:] + if value == 'true': + value = 1 + elif value == 'false' or not value: + value = 0 + + # Only update non-existent properties + if key and result.get(key) == None: + result[key] = value + + self.properties = result + except Exception, err: + log.error( "Failure composing text: %s", traceback.format_exc() ) + self.properties = None + + def getType(self): + """Type accessor""" + return self.type + + def getName(self): + """Name accessor""" + if self.type is not None and self.name.endswith("." + self.type): + return self.name[:len(self.name) - len(self.type) - 1] + return self.name + + def getAddress(self): + """Address accessor""" + return self.address + + def getPort(self): + """Port accessor""" + return self.port + + def getPriority(self): + """Pirority accessor""" + return self.priority + + def getWeight(self): + """Weight accessor""" + return self.weight + + def getProperties(self): + """Properties accessor""" + return self.properties + + def getText(self): + """Text accessor""" + return self.text + + def getServer(self): + """Server accessor""" + return self.server + + def updateRecord(self, zeroconf, now, record): + """Updates service information from a DNS record""" + if record is not None and not record.isExpired(now): + if record.type == _TYPE_A: + if record.name == self.name: + self.address = record.address + elif record.type == _TYPE_SRV: + if record.name == self.name: + self.server = record.server + self.port = record.port + self.weight = record.weight + self.priority = record.priority + self.address = None + self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN)) + elif record.type == _TYPE_TXT: + if record.name == self.name: + self.setText(record.text) + + def request(self, zeroconf, timeout): + """Returns true if the service could be discovered on the + network, and updates this object with details discovered. + """ + now = currentTimeMillis() + delay = _LISTENER_TIME + next = now + delay + last = now + timeout + result = 0 + try: + zeroconf.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN)) + while self.server is None or self.address is None or self.text is None: + if last <= now: + return 0 + if next <= now: + out = DNSOutgoing(_FLAGS_QR_QUERY) + out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN)) + out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_SRV, _CLASS_IN), now) + out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN)) + out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_TXT, _CLASS_IN), now) + if self.server is not None: + out.addQuestion(DNSQuestion(self.server, _TYPE_A, _CLASS_IN)) + out.addAnswerAtTime(zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN), now) + zeroconf.send(out) + next = now + delay + delay = delay * 2 + + zeroconf.wait(min(next, last) - now) + now = currentTimeMillis() + result = 1 + finally: + zeroconf.removeListener(self) + + return result + + def __eq__(self, other): + """Tests equality of service name""" + if isinstance(other, ServiceInfo): + return other.name == self.name + return 0 + + def __ne__(self, other): + """Non-equality test""" + return not self.__eq__(other) + + def __repr__(self): + """String representation""" + result = "service[%s,%s:%s," % (self.name, socket.inet_ntoa(self.getAddress()), self.port) + if self.text is None: + result += "None" + else: + if len(self.text) < 20: + result += self.text + else: + result += self.text[:17] + "..." + result += "]" + return result + From abdab14d6bd0b820d66d7c78f689fd9e5ab540a4 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Sun, 14 Nov 2010 22:55:04 -0500 Subject: [PATCH 08/36] Rename mdns implementation, make it a module instead of package-level API (API change!) --- Browser.py | 30 -- README | 4 + Zeroconf/__init__.py | 88 +++++ {zeroconf => Zeroconf}/dns.py | 11 +- mcastsocket.py => Zeroconf/mcastsocket.py | 0 Zeroconf.py => Zeroconf/mdns.py | 313 ++++------------- samplecode/Browser.py | 34 ++ testmdnssd.py => samplecode/testmdnssd.py | 3 +- .../testmulticast.py | 12 +- setup.py | 5 +- ZeroconfTest.py => test/ZeroconfTest.py | 331 +++++++++--------- ZeroconfTest2.py => test/ZeroconfTest2.py | 0 zeroconf/__init__.py | 0 13 files changed, 383 insertions(+), 448 deletions(-) delete mode 100755 Browser.py create mode 100644 Zeroconf/__init__.py rename {zeroconf => Zeroconf}/dns.py (98%) rename mcastsocket.py => Zeroconf/mcastsocket.py (100%) rename Zeroconf.py => Zeroconf/mdns.py (63%) mode change 100755 => 100644 create mode 100755 samplecode/Browser.py rename testmdnssd.py => samplecode/testmdnssd.py (95%) rename testmulticast.py => samplecode/testmulticast.py (76%) mode change 100644 => 100755 setup.py rename ZeroconfTest.py => test/ZeroconfTest.py (96%) rename ZeroconfTest2.py => test/ZeroconfTest2.py (100%) delete mode 100644 zeroconf/__init__.py diff --git a/Browser.py b/Browser.py deleted file mode 100755 index 7347f39a..00000000 --- a/Browser.py +++ /dev/null @@ -1,30 +0,0 @@ -from Zeroconf import * -import socket - -class MyListener(object): - def __init__(self): - self.r = Zeroconf() - pass - - def removeService(self, zeroconf, type, name): - print "Service", name, "removed" - - def addService(self, zeroconf, type, name): - print "Service", name, "added" - print "Type is", type - info = self.r.getServiceInfo(type, name) - print "Address is", str(socket.inet_ntoa(info.getAddress())) - print "Port is", info.getPort() - print "Weight is", info.getWeight() - print "Priority is", info.getPriority() - print "Server is", info.getServer() - print "Text is", info.getText() - print "Properties are", info.getProperties() - -if __name__ == '__main__': - print "Multicast DNS Service Discovery for Python Browser test" - r = Zeroconf() - print "1. Testing browsing for a service..." - type = "_http._tcp.local." - listener = MyListener() - browser = ServiceBrowser(r, type, listener) diff --git a/README b/README index 266b1c5b..074b2631 100644 --- a/README +++ b/README @@ -1,4 +1,8 @@ This is Multicast DNS Service Discovery for Python v0.12 by Paul Scott-Murphy. +The package zeroconf contains most of the functional code. The module +Zeroconf.py contains a threaded mDNS query/responder sub-class. The +samples/ directory contains various sample code. + Zeroconf.py is the implementation file, look at the end for examples of basic use. You can also view Browser.py to see how to browse for services. diff --git a/Zeroconf/__init__.py b/Zeroconf/__init__.py new file mode 100644 index 00000000..16a2e5cd --- /dev/null +++ b/Zeroconf/__init__.py @@ -0,0 +1,88 @@ +""" Multicast DNS Service Discovery for Python, v0.12 + Copyright (C) 2003, Paul Scott-Murphy + + This module provides a framework for the use of DNS Service Discovery + using IP multicast. It has been tested against the JRendezvous + implementation from StrangeBerry, + and against the mDNSResponder from Mac OS X 10.3.8. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" + +"""0.12 update - allow selection of binding interface + typo fix - Thanks A. M. Kuchlingi + removed all use of word 'Rendezvous' - this is an API change""" + +"""0.11 update - correction to comments for addListener method + support for new record types seen from OS X + - IPv6 address + - hostinfo + ignore unknown DNS record types + fixes to name decoding + works alongside other processes using port 5353 (e.g. on Mac OS X) + tested against Mac OS X 10.3.2's mDNSResponder + corrections to removal of list entries for service browser""" + +"""0.10 update - Jonathon Paisley contributed these corrections: + always multicast replies, even when query is unicast + correct a pointer encoding problem + can now write records in any order + traceback shown on failure + better TXT record parsing + server is now separate from name + can cancel a service browser + + modified some unit tests to accommodate these changes""" + +"""0.09 update - remove all records on service unregistration + fix DOS security problem with readName""" + +"""0.08 update - changed licensing to LGPL""" + +"""0.07 update - faster shutdown on engine + pointer encoding of outgoing names + ServiceBrowser now works + new unit tests""" + +"""0.06 update - small improvements with unit tests + added defined exception types + new style objects + fixed hostname/interface problem + fixed socket timeout problem + fixed addServiceListener() typo bug + using select() for socket reads + tested on Debian unstable with Python 2.2.2""" + +"""0.05 update - ensure case insensitivty on domain names + support for unicast DNS queries""" + +"""0.04 update - added some unit tests + added __ne__ adjuncts where required + ensure names end in '.local.' + timeout on receiving socket for clean shutdown""" + +__author__ = "Paul Scott-Murphy" +__email__ = "paul at scott dash murphy dot com" +__version__ = "0.12" + +#from Zeroconf import dns +#from Zeroconf import mcastsocket +#from Zeroconf import mdns +# +#ServiceInfo = dns.ServiceInfo +#ServiceBrowser = mdns.ServiceBrowser +#Zeroconf = mdns.Zeroconf +#__all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"] diff --git a/zeroconf/dns.py b/Zeroconf/dns.py similarity index 98% rename from zeroconf/dns.py rename to Zeroconf/dns.py index 6a82022f..071b01fa 100644 --- a/zeroconf/dns.py +++ b/Zeroconf/dns.py @@ -1,10 +1,8 @@ """ Multicast DNS Service Discovery for Python, v0.12 Copyright (C) 2003, Paul Scott-Murphy - This module provides a framework for the use of DNS Service Discovery - using IP multicast. It has been tested against the JRendezvous - implementation from StrangeBerry, - and against the mDNSResponder from Mac OS X 10.3.8. + This module provides a DNS/mDNS encoding/decoding facility which + is used by the package to communicate with mDNS servers/clients. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -19,10 +17,6 @@ You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - - -This module contains the mDNS/DNS server required to provide mDNS service -discovery """ import string import time @@ -38,6 +32,7 @@ 'DNSText', 'AbstractMethodException', 'BadTypeInNameException', 'NamePartTooLongException', 'NonLocalNameException', 'NonUniqueNameException', + 'currentTimeMillis', ] # Some timing constants diff --git a/mcastsocket.py b/Zeroconf/mcastsocket.py similarity index 100% rename from mcastsocket.py rename to Zeroconf/mcastsocket.py diff --git a/Zeroconf.py b/Zeroconf/mdns.py old mode 100755 new mode 100644 similarity index 63% rename from Zeroconf.py rename to Zeroconf/mdns.py index ce78be47..a4159030 --- a/Zeroconf.py +++ b/Zeroconf/mdns.py @@ -20,65 +20,8 @@ License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +This is the threaded mDNS responder/query-er implementation """ - -"""0.12 update - allow selection of binding interface - typo fix - Thanks A. M. Kuchlingi - removed all use of word 'Rendezvous' - this is an API change""" - -"""0.11 update - correction to comments for addListener method - support for new record types seen from OS X - - IPv6 address - - hostinfo - ignore unknown DNS record types - fixes to name decoding - works alongside other processes using port 5353 (e.g. on Mac OS X) - tested against Mac OS X 10.3.2's mDNSResponder - corrections to removal of list entries for service browser""" - -"""0.10 update - Jonathon Paisley contributed these corrections: - always multicast replies, even when query is unicast - correct a pointer encoding problem - can now write records in any order - traceback shown on failure - better TXT record parsing - server is now separate from name - can cancel a service browser - - modified some unit tests to accommodate these changes""" - -"""0.09 update - remove all records on service unregistration - fix DOS security problem with readName""" - -"""0.08 update - changed licensing to LGPL""" - -"""0.07 update - faster shutdown on engine - pointer encoding of outgoing names - ServiceBrowser now works - new unit tests""" - -"""0.06 update - small improvements with unit tests - added defined exception types - new style objects - fixed hostname/interface problem - fixed socket timeout problem - fixed addServiceListener() typo bug - using select() for socket reads - tested on Debian unstable with Python 2.2.2""" - -"""0.05 update - ensure case insensitivty on domain names - support for unicast DNS queries""" - -"""0.04 update - added some unit tests - added __ne__ adjuncts where required - ensure names end in '.local.' - timeout on receiving socket for clean shutdown""" - -__author__ = "Paul Scott-Murphy" -__email__ = "paul at scott dash murphy dot com" -__version__ = "0.12" - - import string import time import struct @@ -87,128 +30,22 @@ import select import traceback import logging -import mcastsocket log = logging.getLogger(__name__) -from zeroconf.dns import * +from Zeroconf import dns,mcastsocket,__version__ +ServiceInfo = dns.ServiceInfo __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"] # hook for threads - globals()['_GLOBAL_DONE'] = 0 # Some timing constants - _UNREGISTER_TIME = 125 _CHECK_TIME = 175 _REGISTER_TIME = 225 _LISTENER_TIME = 200 _BROWSER_TIME = 500 -# Some DNS constants - -_MDNS_ADDR = '224.0.0.251' -_MDNS_PORT = 5353; -_DNS_PORT = 53; -_DNS_TTL = 60 * 60; # one hour default TTL - -_MAX_MSG_TYPICAL = 1460 # unused -_MAX_MSG_ABSOLUTE = 8972 - -_FLAGS_QR_MASK = 0x8000 # query response mask -_FLAGS_QR_QUERY = 0x0000 # query -_FLAGS_QR_RESPONSE = 0x8000 # response - -_FLAGS_AA = 0x0400 # Authorative answer -_FLAGS_TC = 0x0200 # Truncated -_FLAGS_RD = 0x0100 # Recursion desired -_FLAGS_RA = 0x8000 # Recursion available - -_FLAGS_Z = 0x0040 # Zero -_FLAGS_AD = 0x0020 # Authentic data -_FLAGS_CD = 0x0010 # Checking disabled - -_CLASS_IN = 1 -_CLASS_CS = 2 -_CLASS_CH = 3 -_CLASS_HS = 4 -_CLASS_NONE = 254 -_CLASS_ANY = 255 -_CLASS_MASK = 0x7FFF -_CLASS_UNIQUE = 0x8000 - -_TYPE_A = 1 -_TYPE_NS = 2 -_TYPE_MD = 3 -_TYPE_MF = 4 -_TYPE_CNAME = 5 -_TYPE_SOA = 6 -_TYPE_MB = 7 -_TYPE_MG = 8 -_TYPE_MR = 9 -_TYPE_NULL = 10 -_TYPE_WKS = 11 -_TYPE_PTR = 12 -_TYPE_HINFO = 13 -_TYPE_MINFO = 14 -_TYPE_MX = 15 -_TYPE_TXT = 16 -_TYPE_AAAA = 28 -_TYPE_SRV = 33 -_TYPE_ANY = 255 - -# Mapping constants to names - -_CLASSES = { _CLASS_IN : "in", - _CLASS_CS : "cs", - _CLASS_CH : "ch", - _CLASS_HS : "hs", - _CLASS_NONE : "none", - _CLASS_ANY : "any" } - -_TYPES = { _TYPE_A : "a", - _TYPE_NS : "ns", - _TYPE_MD : "md", - _TYPE_MF : "mf", - _TYPE_CNAME : "cname", - _TYPE_SOA : "soa", - _TYPE_MB : "mb", - _TYPE_MG : "mg", - _TYPE_MR : "mr", - _TYPE_NULL : "null", - _TYPE_WKS : "wks", - _TYPE_PTR : "ptr", - _TYPE_HINFO : "hinfo", - _TYPE_MINFO : "minfo", - _TYPE_MX : "mx", - _TYPE_TXT : "txt", - _TYPE_AAAA : "quada", - _TYPE_SRV : "srv", - _TYPE_ANY : "any" } - -# utility functions - -def currentTimeMillis(): - """Current system time in milliseconds""" - return time.time() * 1000 - -# Exceptions - -class NonLocalNameException(Exception): - pass - -class NonUniqueNameException(Exception): - pass - -class NamePartTooLongException(Exception): - pass - -class AbstractMethodException(Exception): - pass - -class BadTypeInNameException(Exception): - pass - class Engine(threading.Thread): """An engine wraps read access to sockets, allowing objects that need to receive data from sockets to be called back when the @@ -291,23 +128,26 @@ def __init__(self, zeroconf): def handle_read(self): try: - data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE) + data, (addr, port) = self.zeroconf.socket.recvfrom(dns._MAX_MSG_ABSOLUTE) except Exception, err: - log.info( 'Error on recvfrom: %s', err ) + if getattr( err, 'errno', None ) == 9: # 'Bad file descriptor' during shutdown... + pass + else: + log.info( 'Error on recvfrom: %s', err ) return None self.data = data - msg = DNSIncoming(data) + msg = dns.DNSIncoming(data) if msg.isQuery(): # Always multicast responses # - if port == _MDNS_PORT: - self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT) + if port == dns._MDNS_PORT: + self.zeroconf.handleQuery(msg, dns._MDNS_ADDR, dns._MDNS_PORT) # If it's not a multicast query, reply via unicast # # and multicast - elif port == _DNS_PORT: + elif port == dns._DNS_PORT: self.zeroconf.handleQuery(msg, addr, port) - self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT) + self.zeroconf.handleQuery(msg, dns._MDNS_ADDR, dns._MDNS_PORT) else: log.error( "Unknown port: %s", port @@ -327,13 +167,15 @@ def __init__(self, zeroconf): def run(self): while 1: + if globals()['_GLOBAL_DONE']: + return try: self.zeroconf.wait(10 * 1000) except ValueError, err: break if globals()['_GLOBAL_DONE']: return - now = currentTimeMillis() + now = dns.currentTimeMillis() for record in self.zeroconf.cache.entries(): if record.isExpired(now): self.zeroconf.updateRecord(now, record) @@ -355,20 +197,20 @@ def __init__(self, zeroconf, type, listener): self.listener = listener self.daemon = True self.services = {} - self.nextTime = currentTimeMillis() + self.nextTime = dns.currentTimeMillis() self.delay = _BROWSER_TIME self.list = [] self.done = 0 - self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN)) + self.zeroconf.addListener(self, dns.DNSQuestion(self.type, dns._TYPE_PTR, dns._CLASS_IN)) self.start() def updateRecord(self, zeroconf, now, record): """Callback invoked by Zeroconf when new information arrives. Updates information required by browser in the Zeroconf cache.""" - if record.type == _TYPE_PTR and record.name == self.type: + if record.type == dns._TYPE_PTR and record.name == self.type: expired = record.isExpired(now) try: oldrecord = self.services[record.alias.lower()] @@ -396,16 +238,16 @@ def cancel(self): def run(self): while 1: event = None - now = currentTimeMillis() + now = dns.currentTimeMillis() if len(self.list) == 0 and self.nextTime > now: self.zeroconf.wait(self.nextTime - now) if globals()['_GLOBAL_DONE'] or self.done: return - now = currentTimeMillis() + now = dns.currentTimeMillis() if self.nextTime <= now: - out = DNSOutgoing(_FLAGS_QR_QUERY) - out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN)) + out = dns.DNSOutgoing(dns._FLAGS_QR_QUERY) + out.addQuestion(dns.DNSQuestion(self.type, dns._TYPE_PTR, dns._CLASS_IN)) for record in self.services.values(): if not record.isExpired(now): out.addAnswerAtTime(record, now) @@ -433,14 +275,14 @@ def __init__(self, bindaddress=None): bindaddress = self.intf else: self.intf = bindaddress - self.socket = mcastsocket.create_socket( (bindaddress, _MDNS_PORT) ) - mcastsocket.join_group( self.socket, _MDNS_ADDR ) + self.socket = mcastsocket.create_socket( (bindaddress, dns._MDNS_PORT) ) + mcastsocket.join_group( self.socket, dns._MDNS_ADDR ) self.listeners = [] self.browsers = [] self.services = {} - self.cache = DNSCache() + self.cache = dns.DNSCache() self.condition = threading.Condition() @@ -471,7 +313,7 @@ def getServiceInfo(self, type, name, timeout=3000): """Returns network's service information for a particular name and type, or None if no service matches by the timeout, which defaults to 3 seconds.""" - info = ServiceInfo(type, name) + info = dns.ServiceInfo(type, name) if info.request(self, timeout): return info return None @@ -490,27 +332,27 @@ def removeServiceListener(self, listener): browser.cancel() del(browser) - def registerService(self, info, ttl=_DNS_TTL): + def registerService(self, info, ttl=dns._DNS_TTL): """Registers service information to the network with a default TTL of 60 seconds. Zeroconf will then respond to requests for information for that service. The name of the service may be changed if needed to make it unique on the network.""" self.checkService(info) self.services[info.name.lower()] = info - now = currentTimeMillis() + now = dns.currentTimeMillis() nextTime = now i = 0 while i < 3: if now < nextTime: self.wait(nextTime - now) - now = currentTimeMillis() + now = dns.currentTimeMillis() continue - out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) - out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0) - out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, ttl, info.priority, info.weight, info.port, info.server), 0) - out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0) + out = dns.DNSOutgoing(dns._FLAGS_QR_RESPONSE | dns._FLAGS_AA) + out.addAnswerAtTime(dns.DNSPointer(info.type, dns._TYPE_PTR, dns._CLASS_IN, ttl, info.name), 0) + out.addAnswerAtTime(dns.DNSService(info.name, dns._TYPE_SRV, dns._CLASS_IN, ttl, info.priority, info.weight, info.port, info.server), 0) + out.addAnswerAtTime(dns.DNSText(info.name, dns._TYPE_TXT, dns._CLASS_IN, ttl, info.text), 0) if info.address: - out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, ttl, info.address), 0) + out.addAnswerAtTime(dns.DNSAddress(info.server, dns._TYPE_A, dns._CLASS_IN, ttl, info.address), 0) self.send(out) i += 1 nextTime += _REGISTER_TIME @@ -521,20 +363,20 @@ def unregisterService(self, info): del(self.services[info.name.lower()]) except: pass - now = currentTimeMillis() + now = dns.currentTimeMillis() nextTime = now i = 0 while i < 3: if now < nextTime: self.wait(nextTime - now) - now = currentTimeMillis() + now = dns.currentTimeMillis() continue - out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) - out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0) - out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.name), 0) - out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0) + out = dns.DNSOutgoing(dns._FLAGS_QR_RESPONSE | dns._FLAGS_AA) + out.addAnswerAtTime(dns.DNSPointer(info.type, dns._TYPE_PTR, dns._CLASS_IN, 0, info.name), 0) + out.addAnswerAtTime(dns.DNSService(info.name, dns._TYPE_SRV, dns._CLASS_IN, 0, info.priority, info.weight, info.port, info.name), 0) + out.addAnswerAtTime(dns.DNSText(info.name, dns._TYPE_TXT, dns._CLASS_IN, 0, info.text), 0) if info.address: - out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0) + out.addAnswerAtTime(dns.DNSAddress(info.server, dns._TYPE_A, dns._CLASS_IN, 0, info.address), 0) self.send(out) i += 1 nextTime += _UNREGISTER_TIME @@ -542,21 +384,21 @@ def unregisterService(self, info): def unregisterAllServices(self): """Unregister all registered services.""" if len(self.services) > 0: - now = currentTimeMillis() + now = dns.currentTimeMillis() nextTime = now i = 0 while i < 3: if now < nextTime: self.wait(nextTime - now) - now = currentTimeMillis() + now = dns.currentTimeMillis() continue - out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) + out = dns.DNSOutgoing(dns._FLAGS_QR_RESPONSE | dns._FLAGS_AA) for info in self.services.values(): - out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0) - out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.server), 0) - out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0) + out.addAnswerAtTime(dns.DNSPointer(info.type, dns._TYPE_PTR, dns._CLASS_IN, 0, info.name), 0) + out.addAnswerAtTime(dns.DNSService(info.name, dns._TYPE_SRV, dns._CLASS_IN, 0, info.priority, info.weight, info.port, info.server), 0) + out.addAnswerAtTime(dns.DNSText(info.name, dns._TYPE_TXT, dns._CLASS_IN, 0, info.text), 0) if info.address: - out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0) + out.addAnswerAtTime(dns.DNSAddress(info.server, dns._TYPE_A, dns._CLASS_IN, 0, info.address), 0) self.send(out) i += 1 nextTime += _UNREGISTER_TIME @@ -564,12 +406,12 @@ def unregisterAllServices(self): def checkService(self, info): """Checks the network for a unique service name, modifying the ServiceInfo passed in if it is not unique.""" - now = currentTimeMillis() + now = dns.currentTimeMillis() nextTime = now i = 0 while i < 3: for record in self.cache.entriesWithName(info.type): - if record.type == _TYPE_PTR and not record.isExpired(now) and record.alias == info.name: + if record.type == dns._TYPE_PTR and not record.isExpired(now) and record.alias == info.name: if (info.name.find('.') < 0): info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type self.checkService(info) @@ -577,12 +419,12 @@ def checkService(self, info): raise NonUniqueNameException if now < nextTime: self.wait(nextTime - now) - now = currentTimeMillis() + now = dns.currentTimeMillis() continue - out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA) + out = dns.DNSOutgoing(dns._FLAGS_QR_QUERY | dns._FLAGS_AA) self.debug = out - out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN)) - out.addAuthorativeAnswer(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name)) + out.addQuestion(dns.DNSQuestion(info.type, dns._TYPE_PTR, dns._CLASS_IN)) + out.addAuthorativeAnswer(dns.DNSPointer(info.type, dns._TYPE_PTR, dns._CLASS_IN, dns._DNS_TTL, info.name)) self.send(out) i += 1 nextTime += _CHECK_TIME @@ -591,7 +433,7 @@ def addListener(self, listener, question): """Adds a listener for a given question. The listener will have its updateRecord method called when information is available to answer the question.""" - now = currentTimeMillis() + now = dns.currentTimeMillis() self.listeners.append(listener) if question is not None: for record in self.cache.entriesWithName(question.name): @@ -617,7 +459,7 @@ def updateRecord(self, now, rec): def handleResponse(self, msg): """Deal with incoming response packets. All answers are held in the cache, and listeners are notified.""" - now = currentTimeMillis() + now = dns.currentTimeMillis() for record in msg.answers: expired = record.isExpired(now) if record in self.cache.entries(): @@ -640,47 +482,47 @@ def handleQuery(self, msg, addr, port): # Support unicast client responses # - if port != _MDNS_PORT: - out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0) + if port != dns._MDNS_PORT: + out = dns.DNSOutgoing(dns._FLAGS_QR_RESPONSE | dns._FLAGS_AA, 0) for question in msg.questions: out.addQuestion(question) log.debug( 'Questions...') for question in msg.questions: log.debug( 'Question: %s', question ) - if question.type == _TYPE_PTR: + if question.type == dns._TYPE_PTR: for service in self.services.values(): if question.name == service.type: log.info( 'Service query found %s', service.name ) if out is None: - out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) - out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name)) + out = dns.DNSOutgoing(dns._FLAGS_QR_RESPONSE | dns._FLAGS_AA) + out.addAnswer(msg, dns.DNSPointer(service.type, dns._TYPE_PTR, dns._CLASS_IN, dns._DNS_TTL, service.name)) # devices such as AAstra phones will not re-query to # resolve the pointer, they expect the final IP to show up # in the response - out.addAdditionalAnswer(DNSText(service.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text)) - out.addAdditionalAnswer(DNSService(service.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.priority, service.weight, service.port, service.server)) - out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address)) + out.addAdditionalAnswer(dns.DNSText(service.name, dns._TYPE_TXT, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.text)) + out.addAdditionalAnswer(dns.DNSService(service.name, dns._TYPE_SRV, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.priority, service.weight, service.port, service.server)) + out.addAdditionalAnswer(dns.DNSAddress(service.server, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) else: try: if out is None: - out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) + out = dns.DNSOutgoing(dns._FLAGS_QR_RESPONSE | dns._FLAGS_AA) # Answer A record queries for any service addresses we know - if question.type == _TYPE_A or question.type == _TYPE_ANY: + if question.type == dns._TYPE_A or question.type == dns._TYPE_ANY: for service in self.services.values(): if service.server == question.name.lower(): - out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address)) + out.addAnswer(msg, DNSAddress(question.name, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) service = self.services.get(question.name.lower(), None) if not service: continue - if question.type == _TYPE_SRV or question.type == _TYPE_ANY: - out.addAnswer(msg, DNSService(question.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.priority, service.weight, service.port, service.server)) - if question.type == _TYPE_TXT or question.type == _TYPE_ANY: - out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text)) - if question.type == _TYPE_SRV: - out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address)) + if question.type == dns._TYPE_SRV or question.type == dns._TYPE_ANY: + out.addAnswer(msg, dns.DNSService(question.name, dns._TYPE_SRV, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.priority, service.weight, service.port, service.server)) + if question.type == dns._TYPE_TXT or question.type == dns._TYPE_ANY: + out.addAnswer(msg, dns.DNSText(question.name, dns._TYPE_TXT, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.text)) + if question.type == dns._TYPE_SRV: + out.addAdditionalAnswer(dns.DNSAddress(service.server, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) except Exception, err: log.error( 'Error handling query: %s',traceback.format_exc() @@ -692,10 +534,10 @@ def handleQuery(self, msg, addr, port): else: log.debug( 'No answer for %s', [q for q in msg.questions] ) - def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT): + def send(self, out, addr = dns._MDNS_ADDR, port = dns._MDNS_PORT): """Sends an outgoing packet.""" # This is a quick test to see if we can parse the packets we generate - #temp = DNSIncoming(out.packet()) + #temp = dns.DNSIncoming(out.packet()) try: packet = out.packet() bytes_sent = self.socket.sendto(packet, 0, (addr, port)) @@ -711,6 +553,5 @@ def close(self): self.notifyAll() self.engine.notify() self.unregisterAllServices() - mcastsocket.leave_group( self.socket, _MDNS_ADDR ) + mcastsocket.leave_group( self.socket, dns._MDNS_ADDR ) self.socket.close() - diff --git a/samplecode/Browser.py b/samplecode/Browser.py new file mode 100755 index 00000000..71affd47 --- /dev/null +++ b/samplecode/Browser.py @@ -0,0 +1,34 @@ +from Zeroconf.mdns import * +import socket + +class MyListener(object): + def __init__(self): + self.r = Zeroconf() + pass + + def removeService(self, zeroconf, type, name): + print "Service", name, "removed" + + def addService(self, zeroconf, type, name): + print "Service", name, "added" + print "Type is", type + info = self.r.getServiceInfo(type, name) + print "Address is", str(socket.inet_ntoa(info.getAddress())) + print "Port is", info.getPort() + print "Weight is", info.getWeight() + print "Priority is", info.getPriority() + print "Server is", info.getServer() + print "Text is", info.getText() + print "Properties are", info.getProperties() + +if __name__ == '__main__': + print "Multicast DNS Service Discovery for Python Browser test" + r = Zeroconf() + try: + print "1. Testing browsing for a service..." + type = "_http._tcp.local." + listener = MyListener() + browser = ServiceBrowser(r, type, listener) + raw_input( 'Press to stop listening > ') + finally: + r.close() diff --git a/testmdnssd.py b/samplecode/testmdnssd.py similarity index 95% rename from testmdnssd.py rename to samplecode/testmdnssd.py index fe613305..8c173d3b 100755 --- a/testmdnssd.py +++ b/samplecode/testmdnssd.py @@ -1,5 +1,6 @@ #! /usr/bin/env python -import logging,socket,sys,os,Zeroconf +import logging,socket,sys,os +from Zeroconf import mdns as Zeroconf # Test a few module features, including service registration, service # query (for Zoe), and service unregistration. diff --git a/testmulticast.py b/samplecode/testmulticast.py similarity index 76% rename from testmulticast.py rename to samplecode/testmulticast.py index 1a7a563c..cab359a9 100755 --- a/testmulticast.py +++ b/samplecode/testmulticast.py @@ -6,17 +6,17 @@ our socket should receive that packet (because we have enabled multicast loopback on the socket). """ -import mcastsocket,socket,os,sys,select,logging -import Zeroconf +import socket,os,sys,select,logging +from Zeroconf import dns,mcastsocket,mdns def main(ip): """Create a multicast socket, send a message, check it comes back""" - sock = mcastsocket.create_socket( (ip,Zeroconf._MDNS_PORT), loop=True ) - mcastsocket.join_group( sock, Zeroconf._MDNS_ADDR ) + sock = mcastsocket.create_socket( (ip,dns._MDNS_PORT), loop=True ) + mcastsocket.join_group( sock, dns._MDNS_ADDR ) try: payload = 'hello world' for i in range( 5 ): - sock.sendto( payload, 0, (Zeroconf._MDNS_ADDR, Zeroconf._MDNS_PORT)) + sock.sendto( payload, 0, (dns._MDNS_ADDR, dns._MDNS_PORT)) print 'Waiting for looped message receipt' rs,wr,xs = select.select( [sock],[],[], 1.0 ) data,(addr,port) = sock.recvfrom( 200 ) @@ -26,7 +26,7 @@ def main(ip): print 'Failure: Looped message not received' return 1 finally: - mcastsocket.leave_group( sock, Zeroconf._MDNS_ADDR ) + mcastsocket.leave_group( sock, dns._MDNS_ADDR ) if __name__ == "__main__": logging.basicConfig( level = logging.DEBUG ) diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 76bbcd0e..130977ab --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ #! /usr/bin/env python """Install Zeroconf.py using distutils""" from distutils.core import setup +import os info = {} keys = [('__author__','author'),('__email__','author_email'),('__version__','version')] -for line in open( 'Zeroconf.py' ): +for line in open( os.path.join('Zeroconf','__init__.py') ): for key,inf in keys: if line.startswith( key ): info[inf] = line.strip().split('=')[1].strip().strip('"').strip("'") @@ -15,7 +16,7 @@ name='pyzeroconf', description='Python Zeroconf (mDNS) Library', url='http://digitaltorque.ca', - py_modules=['Zeroconf'], + packages=['zeroconf'], #scripts=['Browser.py'], classifiers=[ 'Development Status :: Production', diff --git a/ZeroconfTest.py b/test/ZeroconfTest.py similarity index 96% rename from ZeroconfTest.py rename to test/ZeroconfTest.py index 4a435bd9..198aea29 100755 --- a/ZeroconfTest.py +++ b/test/ZeroconfTest.py @@ -1,165 +1,166 @@ -""" Multicast DNS Service Discovery for Python, v0.12 - Copyright (C) 2003, Paul Scott-Murphy - - This module provides a unit test suite for the Multicast DNS - Service Discovery for Python module. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -""" - -__author__ = "Paul Scott-Murphy" -__email__ = "paul at scott dash murphy dot com" -__version__ = "0.12" - -import Zeroconf as r -import unittest - - -class PacketGeneration(unittest.TestCase): - - def testParseOwnPacketSimple(self): - generated = r.DNSOutgoing(0) - parsed = r.DNSIncoming(generated.packet()) - - def testParseOwnPacketSimpleUnicast(self): - generated = r.DNSOutgoing(0, 0) - parsed = r.DNSIncoming(generated.packet()) - - def testParseOwnPacketFlags(self): - generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) - parsed = r.DNSIncoming(generated.packet()) - - def testParseOwnPacketQuestion(self): - generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) - generated.addQuestion(r.DNSQuestion("testname.local.", r._TYPE_SRV, r._CLASS_IN)) - parsed = r.DNSIncoming(generated.packet()) - - def testMatchQuestion(self): - generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) - question = r.DNSQuestion("testname.local.", r._TYPE_SRV, r._CLASS_IN) - generated.addQuestion(question) - parsed = r.DNSIncoming(generated.packet()) - self.assertEqual(len(generated.questions), 1) - self.assertEqual(len(generated.questions), len(parsed.questions)) - self.assertEqual(question, parsed.questions[0]) - - -class PacketForm(unittest.TestCase): - - def testTransactionID(self): - """ID must be zero in a DNS-SD packet""" - generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) - bytes = generated.packet() - id = ord(bytes[0]) << 8 | ord(bytes[1]) - self.assertEqual(id, 0) - - def testQueryHeaderBits(self): - generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) - bytes = generated.packet() - flags = ord(bytes[2]) << 8 | ord(bytes[3]) - self.assertEqual(flags, 0x0) - - def testResponseHeaderBits(self): - generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) - bytes = generated.packet() - flags = ord(bytes[2]) << 8 | ord(bytes[3]) - self.assertEqual(flags, 0x8000) - - def testNumbers(self): - generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) - bytes = generated.packet() - numQuestions = ord(bytes[4]) << 8 | ord(bytes[5]) - numAnswers = ord(bytes[6]) << 8 | ord(bytes[7]) - numAuthorities = ord(bytes[8]) << 8 | ord(bytes[9]) - numAddtionals = ord(bytes[10]) << 8 | ord(bytes[11]) - self.assertEqual(numQuestions, 0) - self.assertEqual(numAnswers, 0) - self.assertEqual(numAuthorities, 0) - self.assertEqual(numAddtionals, 0) - - def testNumbersQuestions(self): - generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) - question = r.DNSQuestion("testname.local.", r._TYPE_SRV, r._CLASS_IN) - for i in range(0, 10): - generated.addQuestion(question) - bytes = generated.packet() - numQuestions = ord(bytes[4]) << 8 | ord(bytes[5]) - numAnswers = ord(bytes[6]) << 8 | ord(bytes[7]) - numAuthorities = ord(bytes[8]) << 8 | ord(bytes[9]) - numAddtionals = ord(bytes[10]) << 8 | ord(bytes[11]) - self.assertEqual(numQuestions, 10) - self.assertEqual(numAnswers, 0) - self.assertEqual(numAuthorities, 0) - self.assertEqual(numAddtionals, 0) - - def testNumbersAnswers(self): - generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) - question = r.DNSQuestion("testname.local.", r._TYPE_SRV, r._CLASS_IN) - for i in range(0, 10): - generated.addQuestion(question) - bytes = generated.packet() - numQuestions = ord(bytes[4]) << 8 | ord(bytes[5]) - numAnswers = ord(bytes[6]) << 8 | ord(bytes[7]) - numAuthorities = ord(bytes[8]) << 8 | ord(bytes[9]) - numAddtionals = ord(bytes[10]) << 8 | ord(bytes[11]) - self.assertEqual(numQuestions, 10) - self.assertEqual(numAnswers, 0) - self.assertEqual(numAuthorities, 0) - self.assertEqual(numAddtionals, 0) - - -class Names(unittest.TestCase): - - def testNonLocalName(self): - self.assertRaises(r.NonLocalNameException, r.DNSQuestion, "this.wont.work.com.", r._TYPE_SRV, r._CLASS_IN) - - def testLongName(self): - generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) - question = r.DNSQuestion("this.is.a.very.long.name.with.lots.of.parts.in.it.local.", r._TYPE_SRV, r._CLASS_IN) - generated.addQuestion(question) - parsed = r.DNSIncoming(generated.packet()) - - def testExceedinglyLongName(self): - generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) - name = "%slocal." % ("part." * 1000) - question = r.DNSQuestion(name, r._TYPE_SRV, r._CLASS_IN) - generated.addQuestion(question) - parsed = r.DNSIncoming(generated.packet()) - - def testExceedinglyLongNamePart(self): - name = "%s.local." % ("a" * 1000) - generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) - question = r.DNSQuestion(name, r._TYPE_SRV, r._CLASS_IN) - generated.addQuestion(question) - self.assertRaises(r.NamePartTooLongException, generated.packet) - - def testSameName(self): - name = "paired.local." - generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) - question = r.DNSQuestion(name, r._TYPE_SRV, r._CLASS_IN) - generated.addQuestion(question) - generated.addQuestion(question) - parsed = r.DNSIncoming(generated.packet()) - - -class Framework(unittest.TestCase): - - def testLaunchAndClose(self): - rv = r.Zeroconf() - rv.close() - -if __name__ == '__main__': - unittest.main() +""" Multicast DNS Service Discovery for Python, v0.12 + Copyright (C) 2003, Paul Scott-Murphy + + This module provides a unit test suite for the Multicast DNS + Service Discovery for Python module. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" + +__author__ = "Paul Scott-Murphy" +__email__ = "paul at scott dash murphy dot com" +__version__ = "0.12" + +from Zeroconf import dns as r +import Zeroconf +import unittest + + +class PacketGeneration(unittest.TestCase): + + def testParseOwnPacketSimple(self): + generated = r.DNSOutgoing(0) + parsed = r.DNSIncoming(generated.packet()) + + def testParseOwnPacketSimpleUnicast(self): + generated = r.DNSOutgoing(0, 0) + parsed = r.DNSIncoming(generated.packet()) + + def testParseOwnPacketFlags(self): + generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) + parsed = r.DNSIncoming(generated.packet()) + + def testParseOwnPacketQuestion(self): + generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) + generated.addQuestion(r.DNSQuestion("testname.local.", r._TYPE_SRV, r._CLASS_IN)) + parsed = r.DNSIncoming(generated.packet()) + + def testMatchQuestion(self): + generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) + question = r.DNSQuestion("testname.local.", r._TYPE_SRV, r._CLASS_IN) + generated.addQuestion(question) + parsed = r.DNSIncoming(generated.packet()) + self.assertEqual(len(generated.questions), 1) + self.assertEqual(len(generated.questions), len(parsed.questions)) + self.assertEqual(question, parsed.questions[0]) + + +class PacketForm(unittest.TestCase): + + def testTransactionID(self): + """ID must be zero in a DNS-SD packet""" + generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) + bytes = generated.packet() + id = ord(bytes[0]) << 8 | ord(bytes[1]) + self.assertEqual(id, 0) + + def testQueryHeaderBits(self): + generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) + bytes = generated.packet() + flags = ord(bytes[2]) << 8 | ord(bytes[3]) + self.assertEqual(flags, 0x0) + + def testResponseHeaderBits(self): + generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) + bytes = generated.packet() + flags = ord(bytes[2]) << 8 | ord(bytes[3]) + self.assertEqual(flags, 0x8000) + + def testNumbers(self): + generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) + bytes = generated.packet() + numQuestions = ord(bytes[4]) << 8 | ord(bytes[5]) + numAnswers = ord(bytes[6]) << 8 | ord(bytes[7]) + numAuthorities = ord(bytes[8]) << 8 | ord(bytes[9]) + numAddtionals = ord(bytes[10]) << 8 | ord(bytes[11]) + self.assertEqual(numQuestions, 0) + self.assertEqual(numAnswers, 0) + self.assertEqual(numAuthorities, 0) + self.assertEqual(numAddtionals, 0) + + def testNumbersQuestions(self): + generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) + question = r.DNSQuestion("testname.local.", r._TYPE_SRV, r._CLASS_IN) + for i in range(0, 10): + generated.addQuestion(question) + bytes = generated.packet() + numQuestions = ord(bytes[4]) << 8 | ord(bytes[5]) + numAnswers = ord(bytes[6]) << 8 | ord(bytes[7]) + numAuthorities = ord(bytes[8]) << 8 | ord(bytes[9]) + numAddtionals = ord(bytes[10]) << 8 | ord(bytes[11]) + self.assertEqual(numQuestions, 10) + self.assertEqual(numAnswers, 0) + self.assertEqual(numAuthorities, 0) + self.assertEqual(numAddtionals, 0) + + def testNumbersAnswers(self): + generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) + question = r.DNSQuestion("testname.local.", r._TYPE_SRV, r._CLASS_IN) + for i in range(0, 10): + generated.addQuestion(question) + bytes = generated.packet() + numQuestions = ord(bytes[4]) << 8 | ord(bytes[5]) + numAnswers = ord(bytes[6]) << 8 | ord(bytes[7]) + numAuthorities = ord(bytes[8]) << 8 | ord(bytes[9]) + numAddtionals = ord(bytes[10]) << 8 | ord(bytes[11]) + self.assertEqual(numQuestions, 10) + self.assertEqual(numAnswers, 0) + self.assertEqual(numAuthorities, 0) + self.assertEqual(numAddtionals, 0) + + +class Names(unittest.TestCase): + + def testNonLocalName(self): + self.assertRaises(r.NonLocalNameException, r.DNSQuestion, "this.wont.work.com.", r._TYPE_SRV, r._CLASS_IN) + + def testLongName(self): + generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) + question = r.DNSQuestion("this.is.a.very.long.name.with.lots.of.parts.in.it.local.", r._TYPE_SRV, r._CLASS_IN) + generated.addQuestion(question) + parsed = r.DNSIncoming(generated.packet()) + + def testExceedinglyLongName(self): + generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) + name = "%slocal." % ("part." * 1000) + question = r.DNSQuestion(name, r._TYPE_SRV, r._CLASS_IN) + generated.addQuestion(question) + parsed = r.DNSIncoming(generated.packet()) + + def testExceedinglyLongNamePart(self): + name = "%s.local." % ("a" * 1000) + generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) + question = r.DNSQuestion(name, r._TYPE_SRV, r._CLASS_IN) + generated.addQuestion(question) + self.assertRaises(r.NamePartTooLongException, generated.packet) + + def testSameName(self): + name = "paired.local." + generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) + question = r.DNSQuestion(name, r._TYPE_SRV, r._CLASS_IN) + generated.addQuestion(question) + generated.addQuestion(question) + parsed = r.DNSIncoming(generated.packet()) + + +class Framework(unittest.TestCase): + + def testLaunchAndClose(self): + rv = Zeroconf.Zeroconf() + rv.close() + +if __name__ == '__main__': + unittest.main() diff --git a/ZeroconfTest2.py b/test/ZeroconfTest2.py similarity index 100% rename from ZeroconfTest2.py rename to test/ZeroconfTest2.py diff --git a/zeroconf/__init__.py b/zeroconf/__init__.py deleted file mode 100644 index e69de29b..00000000 From d20227f302cebeb8350bf69af6c6e22780af220c Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Tue, 16 Nov 2010 10:43:32 -0500 Subject: [PATCH 09/36] Since we're breaking the api anyway, follow PEP8 a little more (i.e. top-level package lowercase) --- {Zeroconf => zeroconf}/__init__.py | 0 {Zeroconf => zeroconf}/dns.py | 0 {Zeroconf => zeroconf}/mcastsocket.py | 0 {Zeroconf => zeroconf}/mdns.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {Zeroconf => zeroconf}/__init__.py (100%) rename {Zeroconf => zeroconf}/dns.py (100%) rename {Zeroconf => zeroconf}/mcastsocket.py (100%) rename {Zeroconf => zeroconf}/mdns.py (100%) diff --git a/Zeroconf/__init__.py b/zeroconf/__init__.py similarity index 100% rename from Zeroconf/__init__.py rename to zeroconf/__init__.py diff --git a/Zeroconf/dns.py b/zeroconf/dns.py similarity index 100% rename from Zeroconf/dns.py rename to zeroconf/dns.py diff --git a/Zeroconf/mcastsocket.py b/zeroconf/mcastsocket.py similarity index 100% rename from Zeroconf/mcastsocket.py rename to zeroconf/mcastsocket.py diff --git a/Zeroconf/mdns.py b/zeroconf/mdns.py similarity index 100% rename from Zeroconf/mdns.py rename to zeroconf/mdns.py From f8a646d32238778e18d024c1e1efbc15d1311735 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Tue, 16 Nov 2010 23:22:00 -0500 Subject: [PATCH 10/36] Fix up the sample code, include sample code and tests in source packages --- MANIFEST.in | 2 ++ samplecode/Browser.py | 2 +- samplecode/testmdnssd.py | 2 +- samplecode/testmulticast.py | 6 ++++-- setup.py | 2 +- test/ZeroconfTest.py | 6 +++--- test/ZeroconfTest2.py | 2 +- zeroconf/mcastsocket.py | 20 +++++++++++--------- zeroconf/mdns.py | 2 +- 9 files changed, 25 insertions(+), 19 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 37e3f084..348c5377 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,5 @@ include README include lgpl.txt include *.py +recursive-include samplecode *.py +recursive-include test *.py diff --git a/samplecode/Browser.py b/samplecode/Browser.py index 71affd47..dbe8e4dc 100755 --- a/samplecode/Browser.py +++ b/samplecode/Browser.py @@ -1,4 +1,4 @@ -from Zeroconf.mdns import * +from zeroconf.mdns import * import socket class MyListener(object): diff --git a/samplecode/testmdnssd.py b/samplecode/testmdnssd.py index 8c173d3b..e882a21b 100755 --- a/samplecode/testmdnssd.py +++ b/samplecode/testmdnssd.py @@ -1,6 +1,6 @@ #! /usr/bin/env python import logging,socket,sys,os -from Zeroconf import mdns as Zeroconf +from zeroconf import mdns as Zeroconf # Test a few module features, including service registration, service # query (for Zoe), and service unregistration. diff --git a/samplecode/testmulticast.py b/samplecode/testmulticast.py index cab359a9..2dc980f2 100755 --- a/samplecode/testmulticast.py +++ b/samplecode/testmulticast.py @@ -7,7 +7,7 @@ loopback on the socket). """ import socket,os,sys,select,logging -from Zeroconf import dns,mcastsocket,mdns +from zeroconf import dns,mcastsocket,mdns def main(ip): """Create a multicast socket, send a message, check it comes back""" @@ -21,7 +21,9 @@ def main(ip): rs,wr,xs = select.select( [sock],[],[], 1.0 ) data,(addr,port) = sock.recvfrom( 200 ) if data == payload: - print 'Success: looped message received' + print 'Success: looped message received from address %s port %s'%( + addr,port, + ) return 0 print 'Failure: Looped message not received' return 1 diff --git a/setup.py b/setup.py index 130977ab..59c6ed72 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import os info = {} keys = [('__author__','author'),('__email__','author_email'),('__version__','version')] -for line in open( os.path.join('Zeroconf','__init__.py') ): +for line in open( os.path.join('zeroconf','__init__.py') ): for key,inf in keys: if line.startswith( key ): info[inf] = line.strip().split('=')[1].strip().strip('"').strip("'") diff --git a/test/ZeroconfTest.py b/test/ZeroconfTest.py index 198aea29..01dfad51 100755 --- a/test/ZeroconfTest.py +++ b/test/ZeroconfTest.py @@ -24,8 +24,8 @@ __email__ = "paul at scott dash murphy dot com" __version__ = "0.12" -from Zeroconf import dns as r -import Zeroconf +from zeroconf import dns as r +from zeroconf import mdns import unittest @@ -159,7 +159,7 @@ def testSameName(self): class Framework(unittest.TestCase): def testLaunchAndClose(self): - rv = Zeroconf.Zeroconf() + rv = mdns.Zeroconf() rv.close() if __name__ == '__main__': diff --git a/test/ZeroconfTest2.py b/test/ZeroconfTest2.py index 2071e3d6..7f760e1b 100755 --- a/test/ZeroconfTest2.py +++ b/test/ZeroconfTest2.py @@ -24,7 +24,7 @@ __email__ = "paul at scott dash murphy dot com" __version__ = "0.12" -from Zeroconf import * +from zeroconf.mdns import * import socket desc = {'path':'/~paulsm/'} diff --git a/zeroconf/mcastsocket.py b/zeroconf/mcastsocket.py index bc884eab..9999373b 100644 --- a/zeroconf/mcastsocket.py +++ b/zeroconf/mcastsocket.py @@ -47,20 +47,23 @@ def create_socket( address, TTL=1, loop=True, reuse=True ): returns socket.socket instance configured as specified """ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, TTL) - sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, int(bool(loop))) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, TTL) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, int(bool(loop))) allow_reuse( sock, reuse ) + limit_to_interface( sock, address[0] ) try: # Note: multicast is *not* working if we don't bind on all interfaces, most likely # because the 224.* isn't getting mapped (routed) to the address of the interface... # to debug that case, see if {{{ip route add 224.0.0.0/4 dev br0}}} (or whatever your # interface is) makes the route suddenly start working... +# if address[0]: +# sock.bind( address ) +# else: sock.bind(('',address[1])) except Exception, err: # Some versions of linux raise an exception even though # the SO_REUSE* options have been set, so ignore it log.error('Failure binding: %s', err) - limit_to_interface( sock, address[0] ) return sock def limit_to_interface( sock, interface_ip ): @@ -73,11 +76,10 @@ def limit_to_interface( sock, interface_ip ): """ if interface_ip: # listen/send on a single interface... - log.debug( 'Setting multicast to use interface of %s', interface_ip ) + log.debug( 'Limiting multicast to use interface of %s', interface_ip ) sock.setsockopt( - socket.SOL_IP, socket.IP_MULTICAST_IF, - socket.inet_aton( interface_ip) + - socket.inet_aton('0.0.0.0') + socket.IPPROTO_IP, socket.IP_MULTICAST_IF, + socket.inet_aton( interface_ip) # + socket.inet_aton( '0.0.0.0' ) ) return True return False @@ -118,13 +120,13 @@ def join_group( sock, group ): """Add our socket to this multicast group""" log.info( 'Joining multicast group: %s', group ) sock.setsockopt( - socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, + socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(group) + socket.inet_aton('0.0.0.0') ) def leave_group( sock, group ): """Remove our socket from this multicast group""" log.info( 'Leaving multicast group: %s', group ) sock.setsockopt( - socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, + socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, socket.inet_aton(group) + socket.inet_aton('0.0.0.0') ) diff --git a/zeroconf/mdns.py b/zeroconf/mdns.py index a4159030..c7ac3414 100644 --- a/zeroconf/mdns.py +++ b/zeroconf/mdns.py @@ -31,7 +31,7 @@ import traceback import logging log = logging.getLogger(__name__) -from Zeroconf import dns,mcastsocket,__version__ +from zeroconf import dns,mcastsocket,__version__ ServiceInfo = dns.ServiceInfo __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"] From 07f8a009cfbbe506866514a82d234f4b0bd526c3 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Wed, 17 Nov 2010 23:47:27 -0500 Subject: [PATCH 11/36] Trivial sample code to gather upnp devices on a network --- samplecode/testupnpigd.py | 47 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100755 samplecode/testupnpigd.py diff --git a/samplecode/testupnpigd.py b/samplecode/testupnpigd.py new file mode 100755 index 00000000..93c4fea0 --- /dev/null +++ b/samplecode/testupnpigd.py @@ -0,0 +1,47 @@ +#! /usr/bin/env python +"""Trivial script to handle IGD port-opening... +""" +import socket,os,sys,select,logging +from zeroconf import mcastsocket + +GROUP = '239.255.255.250' +PORT = 1900 + +query = """M-SEARCH * HTTP/1.1 +HOST: %(ip)s:%(port)s +MAN: ssdp:discover +MX: 10 +ST: ssdp:all""" + +def handle( sock, data, address ): + """Handle incoming message about service""" + print 'received from %s:%s: '%(address,) + print data + +# :schemas-upnp-org:device:InternetGatewayDevice:1 + +def main(ip): + """Create a multicast socket, send a message, check it comes back""" + port = PORT + sock = mcastsocket.create_socket( (ip,port), loop=False ) + mcastsocket.join_group( sock, GROUP ) + try: + payload = query % locals() + while True: + sock.sendto( payload, 0, (GROUP,PORT)) + print 'Waiting for responses' + rs,wr,xs = select.select( [sock],[],[], 20.0 ) + if rs: + data, addr = sock.recvfrom( 2000 ) + handle( sock, data, addr ) + return 1 + finally: + mcastsocket.leave_group( sock, GROUP ) + +if __name__ == "__main__": + logging.basicConfig( level = logging.DEBUG ) + usage = 'testupnpigd.py ip.address' + if not sys.argv[1:]: + print usage + sys.exit( 1 ) + sys.exit( main(*sys.argv[1:]) ) From 7461144ccd8b6fee9f4d0805412ebe6c363036bb Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sun, 13 Mar 2011 22:36:42 +0100 Subject: [PATCH 12/36] Ignore Python build directory, configure logger and deal with timeout issue --- .gitignore | 2 ++ samplecode/Browser.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 07626a58..9415a30e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ MANIFEST dist +build *.pyc +*.pyo *.e4? diff --git a/samplecode/Browser.py b/samplecode/Browser.py index dbe8e4dc..93a31706 100755 --- a/samplecode/Browser.py +++ b/samplecode/Browser.py @@ -13,6 +13,9 @@ def addService(self, zeroconf, type, name): print "Service", name, "added" print "Type is", type info = self.r.getServiceInfo(type, name) + if not info: + print " (timeout)" + return print "Address is", str(socket.inet_ntoa(info.getAddress())) print "Port is", info.getPort() print "Weight is", info.getWeight() @@ -22,6 +25,8 @@ def addService(self, zeroconf, type, name): print "Properties are", info.getProperties() if __name__ == '__main__': + import logging + logging.basicConfig(level=logging.WARNING) print "Multicast DNS Service Discovery for Python Browser test" r = Zeroconf() try: From 994e578c31acab3e79cb91a47fddf5f31598585f Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sun, 13 Mar 2011 22:40:15 +0100 Subject: [PATCH 13/36] Avoid using `type` which is a reserved Python keyword --- samplecode/Browser.py | 12 ++++----- zeroconf/dns.py | 58 +++++++++++++++++++++---------------------- zeroconf/mdns.py | 22 ++++++++-------- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/samplecode/Browser.py b/samplecode/Browser.py index 93a31706..f3157e74 100755 --- a/samplecode/Browser.py +++ b/samplecode/Browser.py @@ -6,13 +6,13 @@ def __init__(self): self.r = Zeroconf() pass - def removeService(self, zeroconf, type, name): + def removeService(self, zeroconf, type_, name): print "Service", name, "removed" - def addService(self, zeroconf, type, name): + def addService(self, zeroconf, type_, name): print "Service", name, "added" - print "Type is", type - info = self.r.getServiceInfo(type, name) + print "Type is", type_ + info = self.r.getServiceInfo(type_, name) if not info: print " (timeout)" return @@ -31,9 +31,9 @@ def addService(self, zeroconf, type, name): r = Zeroconf() try: print "1. Testing browsing for a service..." - type = "_http._tcp.local." + type_ = "_http._tcp.local." listener = MyListener() - browser = ServiceBrowser(r, type, listener) + browser = ServiceBrowser(r, type_, listener) raw_input( 'Press to stop listening > ') finally: r.close() diff --git a/zeroconf/dns.py b/zeroconf/dns.py index 071b01fa..247a33d3 100644 --- a/zeroconf/dns.py +++ b/zeroconf/dns.py @@ -152,15 +152,15 @@ class BadTypeInNameException(Exception): class DNSEntry(object): """A DNS entry""" - def __init__(self, name, type, clazz): + def __init__(self, name, type_, clazz): self.key = string.lower(name) self.name = name - self.type = type + self.type = type_ self.clazz = clazz & _CLASS_MASK self.unique = (clazz & _CLASS_UNIQUE) != 0 def __eq__(self, other): - """Equality test on name, type, and class""" + """Equality test on name, type_, and class""" if isinstance(other, DNSEntry): return self.name == other.name and self.type == other.type and self.clazz == other.clazz return 0 @@ -176,12 +176,12 @@ def getClazz(self, clazz): except: return "?(%s)" % (clazz) - def getType(self, type): + def getType(self, type_): """Type accessor""" try: - return _TYPES[type] + return _TYPES[type_] except: - return "?(%s)" % (type) + return "?(%s)" % (type_) def toString(self, hdr, other): """String representation with additional information""" @@ -200,10 +200,10 @@ def toString(self, hdr, other): class DNSQuestion(DNSEntry): """A DNS question entry""" - def __init__(self, name, type, clazz): + def __init__(self, name, type_, clazz): if not name.endswith(".local."): raise NonLocalNameException - DNSEntry.__init__(self, name, type, clazz) + DNSEntry.__init__(self, name, type_, clazz) def answeredBy(self, rec): """Returns true if the question is answered by the record""" @@ -217,8 +217,8 @@ def __repr__(self): class DNSRecord(DNSEntry): """A DNS record - like a DNS entry, but has a TTL""" - def __init__(self, name, type, clazz, ttl): - DNSEntry.__init__(self, name, type, clazz) + def __init__(self, name, type_, clazz, ttl): + DNSEntry.__init__(self, name, type_, clazz) self.ttl = ttl self.created = currentTimeMillis() @@ -237,7 +237,7 @@ def suppressedBy(self, msg): return 0 def suppressedByAnswer(self, other): - """Returns true if another record has same name, type and class, + """Returns true if another record has same name, type_ and class, and if its TTL is at least half of this record's.""" if self == other and other.ttl > (self.ttl / 2): return 1 @@ -278,8 +278,8 @@ def toString(self, other): class DNSAddress(DNSRecord): """A DNS address record""" - def __init__(self, name, type, clazz, ttl, address): - DNSRecord.__init__(self, name, type, clazz, ttl) + def __init__(self, name, type_, clazz, ttl, address): + DNSRecord.__init__(self, name, type_, clazz, ttl) self.address = address def write(self, out): @@ -302,8 +302,8 @@ def __repr__(self): class DNSHinfo(DNSRecord): """A DNS host information record""" - def __init__(self, name, type, clazz, ttl, cpu, os): - DNSRecord.__init__(self, name, type, clazz, ttl) + def __init__(self, name, type_, clazz, ttl, cpu, os): + DNSRecord.__init__(self, name, type_, clazz, ttl) self.cpu = cpu self.os = os @@ -325,8 +325,8 @@ def __repr__(self): class DNSPointer(DNSRecord): """A DNS pointer record""" - def __init__(self, name, type, clazz, ttl, alias): - DNSRecord.__init__(self, name, type, clazz, ttl) + def __init__(self, name, type_, clazz, ttl, alias): + DNSRecord.__init__(self, name, type_, clazz, ttl) self.alias = alias def write(self, out): @@ -346,8 +346,8 @@ def __repr__(self): class DNSText(DNSRecord): """A DNS text record""" - def __init__(self, name, type, clazz, ttl, text): - DNSRecord.__init__(self, name, type, clazz, ttl) + def __init__(self, name, type_, clazz, ttl, text): + DNSRecord.__init__(self, name, type_, clazz, ttl) self.text = text def write(self, out): @@ -370,8 +370,8 @@ def __repr__(self): class DNSService(DNSRecord): """A DNS service record""" - def __init__(self, name, type, clazz, ttl, priority, weight, port, server): - DNSRecord.__init__(self, name, type, clazz, ttl) + def __init__(self, name, type_, clazz, ttl, priority, weight, port, server): + DNSRecord.__init__(self, name, type_, clazz, ttl) self.priority = priority self.weight = weight self.port = port @@ -500,13 +500,13 @@ def readOthers(self): # encountered need to be parsed properly. # log.warn( - "Unknown DNS query type: %s", info[0] + "Unknown DNS query type_: %s", info[0] ) if rec is not None: self.answers.append(rec) except Exception, err: - log.warn( "Failure on record type %s, ignoring: %s", info[0], err ) + log.warn( "Failure on record type_ %s, ignoring: %s", info[0], err ) def isQuery(self): """Returns true if this is a query""" @@ -751,10 +751,10 @@ def get(self, entry): except: return None - def getByDetails(self, name, type, clazz): + def getByDetails(self, name, type_, clazz): """Gets an entry by details. Will return None if there is no matching entry.""" - entry = DNSEntry(name, type, clazz) + entry = DNSEntry(name, type_, clazz) return self.get(entry) def entriesWithName(self, name): @@ -775,10 +775,10 @@ def add(x, y): return x+y class ServiceInfo(object): """Service information""" - def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None): + def __init__(self, type_, name, address=None, port=None, weight=0, priority=0, properties=None, server=None): """Create a service description. - type: fully qualified service type name + type_: fully qualified service type_ name name: fully qualified service name address: IP address as unsigned short, network byte order port: port that the service runs on @@ -787,9 +787,9 @@ def __init__(self, type, name, address=None, port=None, weight=0, priority=0, pr properties: dictionary of properties (or a string holding the bytes for the text field) server: fully qualified name for service host (defaults to name)""" - if not name.endswith(type): + if not name.endswith(type_): raise BadTypeInNameException - self.type = type + self.type = type_ self.name = name self.address = address self.port = port diff --git a/zeroconf/mdns.py b/zeroconf/mdns.py index c7ac3414..9e885cab 100644 --- a/zeroconf/mdns.py +++ b/zeroconf/mdns.py @@ -183,17 +183,17 @@ def run(self): class ServiceBrowser(threading.Thread): - """Used to browse for a service of a specific type. + """Used to browse for a service of a specific type_. The listener object will have its addService() and removeService() methods called when this browser discovers changes in the services availability.""" - def __init__(self, zeroconf, type, listener): - """Creates a browser for a specific type""" + def __init__(self, zeroconf, type_, listener): + """Creates a browser for a specific type_""" threading.Thread.__init__(self) self.zeroconf = zeroconf - self.type = type + self.type = type_ self.listener = listener self.daemon = True self.services = {} @@ -309,21 +309,21 @@ def notifyAll(self): self.condition.notifyAll() self.condition.release() - def getServiceInfo(self, type, name, timeout=3000): + def getServiceInfo(self, type_, name, timeout=3000): """Returns network's service information for a particular - name and type, or None if no service matches by the timeout, + name and type_, or None if no service matches by the timeout, which defaults to 3 seconds.""" - info = dns.ServiceInfo(type, name) + info = dns.ServiceInfo(type_, name) if info.request(self, timeout): return info return None - def addServiceListener(self, type, listener): - """Adds a listener for a particular service type. This object + def addServiceListener(self, type_, listener): + """Adds a listener for a particular service type_. This object will then have its updateRecord method called when information - arrives for that type.""" + arrives for that type_.""" self.removeServiceListener(listener) - self.browsers.append(ServiceBrowser(self, type, listener)) + self.browsers.append(ServiceBrowser(self, type_, listener)) def removeServiceListener(self, listener): """Removes a listener from the set that is currently listening.""" From 9628c26a5b8d0e1b0d77a1549032262fadb508e6 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sun, 13 Mar 2011 23:44:07 +0100 Subject: [PATCH 14/36] Add debug info for NSEC and MF DNS records --- zeroconf/dns.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/zeroconf/dns.py b/zeroconf/dns.py index 247a33d3..751f21a6 100644 --- a/zeroconf/dns.py +++ b/zeroconf/dns.py @@ -93,6 +93,7 @@ _TYPE_TXT = 16 _TYPE_AAAA = 28 _TYPE_SRV = 33 +_TYPE_NSEC = 47 _TYPE_ANY = 255 # Mapping constants to names @@ -122,6 +123,7 @@ _TYPE_TXT : "txt", _TYPE_AAAA : "quada", _TYPE_SRV : "srv", + _TYPE_NSEC : "nsec", _TYPE_ANY : "any" } # utility functions @@ -492,6 +494,10 @@ def readOthers(self): rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString()) elif info[0] == _TYPE_AAAA: rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16)) + elif info[0] == _TYPE_MF: + log.debug("Obsoleted Mail Forwarding (MF) type") + elif info[0] == _TYPE_NSEC: + log.debug("Ignoring DNSSEC record type") else: # Try to ignore types we don't know about # this may mean the rest of the name is From 732997056274bba182d11593370ff135b5b39490 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Mon, 14 Mar 2011 00:28:57 +0100 Subject: [PATCH 15/36] Avoid using which is a reserved Python keyword --- zeroconf/dns.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/zeroconf/dns.py b/zeroconf/dns.py index 751f21a6..de306077 100644 --- a/zeroconf/dns.py +++ b/zeroconf/dns.py @@ -454,9 +454,9 @@ def readCharacterString(self): self.offset += 1 return self.readString(length) - def readString(self, len): + def readString(self, len_): """Reads a string of a given length from the packet""" - format = '!' + str(len) + 's' + format = '!' + str(len_) + 's' length = struct.calcsize(format) info = struct.unpack(format, self.data[self.offset:self.offset+length]) self.offset += length @@ -522,13 +522,13 @@ def isResponse(self): """Returns true if this is a response""" return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE - def readUTF(self, offset, len): + def readUTF(self, offset, len_): """Reads a UTF-8 string of a given length from the packet TODO: there are cases were non-utf-8 data comes through, we need to decide how to properly handle these. """ - return self.data[offset:offset+len].decode('utf-8','ignore') + return self.data[offset:offset+len_].decode('utf-8','ignore') def readName(self): """Reads a domain name from the packet""" @@ -538,18 +538,18 @@ def readName(self): first = off while 1: - len = ord(self.data[off]) + len_ = ord(self.data[off]) off += 1 - if len == 0: + if len_ == 0: break - t = len & 0xC0 + t = len_ & 0xC0 if t == 0x00: - result = ''.join((result, self.readUTF(off, len) + '.')) - off += len + result = ''.join((result, self.readUTF(off, len_) + '.')) + off += len_ elif t == 0xC0: if next < 0: next = off + 1 - off = ((len & 0x3F) << 8) | ord(self.data[off]) + off = ((len_ & 0x3F) << 8) | ord(self.data[off]) if off >= first: raise "Bad domain name (circular) at " + str(off) first = off From a426243b543fd9b84573038081e016537cb39001 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Fri, 6 May 2011 11:39:24 -0400 Subject: [PATCH 16/36] Use distribute if it is available --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 59c6ed72..be6351d0 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,9 @@ #! /usr/bin/env python """Install Zeroconf.py using distutils""" -from distutils.core import setup +try: + from setuptools import setup +except ImportError, err: + from distutils.core import setup import os info = {} keys = [('__author__','author'),('__email__','author_email'),('__version__','version')] From 65a5be6c859ed915af44706238e73a907b286896 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Tue, 10 May 2011 10:45:19 -0400 Subject: [PATCH 17/36] Begin a bit of hacking to allow basic negotiate-a-server-name functionality working. Also allows replying with server names on queries for A/AAAA names. --- zeroconf/dns.py | 51 +++++++++++++++++++++++++++++++++++-- zeroconf/mdns.py | 65 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/zeroconf/dns.py b/zeroconf/dns.py index 071b01fa..f173a95a 100644 --- a/zeroconf/dns.py +++ b/zeroconf/dns.py @@ -906,18 +906,24 @@ def updateRecord(self, zeroconf, now, record): """Updates service information from a DNS record""" if record is not None and not record.isExpired(now): if record.type == _TYPE_A: - if record.name == self.name: + if record.name in (self.name,self.server): + log.debug( 'Got A record for %s', record.name ) self.address = record.address + else: + log.debug( 'Got A record for %s, wanted %s', record.name, self.name ) elif record.type == _TYPE_SRV: if record.name == self.name: + log.debug( 'Got SRV record for %s', record.name ) self.server = record.server self.port = record.port self.weight = record.weight self.priority = record.priority - self.address = None + #self.address = None self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN)) + elif record.type == _TYPE_TXT: if record.name == self.name: + log.debug( 'Got TXT record for %s', record.name ) self.setText(record.text) def request(self, zeroconf, timeout): @@ -978,3 +984,44 @@ def __repr__(self): result += "]" return result + +class ServerNameWatcher( object ): + def __init__(self, name, ignore=None ): + self.name = name + self.address = None + self.ignore = ignore + def request( self, zeroconf, timeout=3000 ): + now = currentTimeMillis() + delay = _LISTENER_TIME + next = now + delay + last = now + timeout + result = 0 + try: + zeroconf.addListener(self, DNSQuestion( self.name, _TYPE_ANY, _CLASS_IN )) + while self.address is None: + if last <= now: + return 0 + if next <= now: + out = DNSOutgoing(_FLAGS_QR_QUERY) + out.addQuestion(DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN)) + zeroconf.send(out) + next = now + delay + delay = delay * 2 + + zeroconf.wait(min(next, last) - now) + now = currentTimeMillis() + result = 1 + finally: + zeroconf.removeListener(self) + return result + def updateRecord(self, zeroconf, now, record): + """Updates service information from a DNS record""" + if record is not None and not record.isExpired(now): + if record.name == self.name: + if (self.ignore and record.address in self.ignore) or (not self.ignore): + self.address = record.address + else: + log.debug( + """Ignoring own-response""" + ) + diff --git a/zeroconf/mdns.py b/zeroconf/mdns.py index c7ac3414..1f313eb6 100644 --- a/zeroconf/mdns.py +++ b/zeroconf/mdns.py @@ -308,6 +308,57 @@ def notifyAll(self): self.condition.acquire() self.condition.notifyAll() self.condition.release() + + def probeName( self, name, timeout=250 ): + """Probe host-names until we find an unoccupied one + + name -- name from which to construct the host-name, note that this + must be a .local. suffix'd name, names will be constructed as + prefix%s.suffix. where %s is a numeric suffix, with '' as the + first attempted name + + http://files.multicastdns.org/draft-cheshire-dnsext-multicastdns.txt Section 8 + + Note: this is not yet a full implementation of the protocol, + as it is missing support for pre-registering services, handling + conflicts and race conditions, etc. + + Note: you *must* advertise your services *before* you issue this + query, as race conditions will occur if two machines booted at + the same time try to resolve the same name. + """ + def names( name ): + yield name + prefix,suffix = name.split('.',1) + count = 1 + while True: + yield '%s%s.%s'%( prefix,count,suffix ) + count += 1 + if count > 2**16: + raise RuntimeError( """Unable to find an unused name %s"""%( name, )) + for failures,name in enumerate(names(name)): + for i in range(3): + address = self.getServerAddress( + name, timeout, + ignore=[self.intf] # our own address is *not* to be considered + ) + if address: + break + if failures > 5: + # mDNS requires huge slowdown after 15 failures + # to prevent flooding network... + log.warn( + "Throttling host-name configuration to prevent network flood" + ) + timeout = 5.0 + if not address: + return name + def getServerAddress(self, name, timeout=3000, ignore=None): + """Returns given server-name record or None on timeout""" + info = dns.ServerNameWatcher( name, ignore=ignore ) + if info.request( self, timeout ): + return info.address + return None def getServiceInfo(self, type, name, timeout=3000): """Returns network's service information for a particular @@ -490,8 +541,10 @@ def handleQuery(self, msg, addr, port): for question in msg.questions: log.debug( 'Question: %s', question ) if question.type == dns._TYPE_PTR: + log.debug( 'Question name: %r', question.name ) for service in self.services.values(): - if question.name == service.type: + log.debug( 'Check service: %s', service.type ) + if question.name in (service.type,service.name): log.info( 'Service query found %s', service.name ) if out is None: out = dns.DNSOutgoing(dns._FLAGS_QR_RESPONSE | dns._FLAGS_AA) @@ -511,11 +564,12 @@ def handleQuery(self, msg, addr, port): if question.type == dns._TYPE_A or question.type == dns._TYPE_ANY: for service in self.services.values(): if service.server == question.name.lower(): - out.addAnswer(msg, DNSAddress(question.name, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) - + out.addAnswer(msg, dns.DNSAddress(question.name, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) service = self.services.get(question.name.lower(), None) - if not service: continue + if not service: + continue + log.info( 'Question was matched to service' ) if question.type == dns._TYPE_SRV or question.type == dns._TYPE_ANY: out.addAnswer(msg, dns.DNSService(question.name, dns._TYPE_SRV, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.priority, service.weight, service.port, service.server)) @@ -531,6 +585,9 @@ def handleQuery(self, msg, addr, port): if out is not None and out.answers: out.id = msg.id self.send(out, addr, port) + log.info( 'Sent response' ) +# elif out is not None: +# log.debug( "Out was created, but got no answers" ) else: log.debug( 'No answer for %s', [q for q in msg.questions] ) From a3c86e187ce4a674f732aa63a6def46f80d1ebc6 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Tue, 10 May 2011 13:53:01 -0400 Subject: [PATCH 18/36] More fixes for looking up services/ptrs which may be matching on e.g. server name for ANY queries, requires an iteration over the set of services for every query, which could be optimized away with a pre-calculated mapping if performance suffers. --- zeroconf/dns.py | 13 ++++-- zeroconf/mdns.py | 105 +++++++++++++++++++++++++---------------------- 2 files changed, 67 insertions(+), 51 deletions(-) diff --git a/zeroconf/dns.py b/zeroconf/dns.py index f173a95a..e8133b25 100644 --- a/zeroconf/dns.py +++ b/zeroconf/dns.py @@ -500,7 +500,7 @@ def readOthers(self): # encountered need to be parsed properly. # log.warn( - "Unknown DNS query type: %s", info[0] + "Unknown DNS query type: %s", info ) if rec is not None: @@ -1018,8 +1018,15 @@ def updateRecord(self, zeroconf, now, record): """Updates service information from a DNS record""" if record is not None and not record.isExpired(now): if record.name == self.name: - if (self.ignore and record.address in self.ignore) or (not self.ignore): - self.address = record.address + if ( + self.ignore and getattr(record,'address',None) in self.ignore + ) or (not self.ignore): + # something is using this name, whether for a server-name or not... + if self.address in (True,None): + if getattr( record, 'address', None ): + self.address = record.address + else: + self.address = True else: log.debug( """Ignoring own-response""" diff --git a/zeroconf/mdns.py b/zeroconf/mdns.py index 1f313eb6..fb14cdf0 100644 --- a/zeroconf/mdns.py +++ b/zeroconf/mdns.py @@ -26,6 +26,7 @@ import time import struct import socket +import errno import threading import select import traceback @@ -80,12 +81,17 @@ def run(self): else: try: rr, wr, er = select.select(rs, [], [], self.timeout) + except (socket.error,select.error), err: + if err[0] in (errno.EWOULDBLOCK,errno.EINTR,errno.EAGAIN): + pass + else: + log.warn( 'Failure on select, ignoring: %s', err ) except Exception, err: log.warn( 'Select failure, ignored: %s', err ) else: - for socket in rr: + for sock in rr: try: - self.readers[socket].handle_read() + self.readers[sock].handle_read() except Exception, err: # Ignore errors that occur on shutdown log.error( 'Error handling read: %s', err ) @@ -317,6 +323,10 @@ def probeName( self, name, timeout=250 ): prefix%s.suffix. where %s is a numeric suffix, with '' as the first attempted name + TODO: maybe make this part of checkService instead? Same basic + operation, it's just looking for unique server name instead of unique + service name. + http://files.multicastdns.org/draft-cheshire-dnsext-multicastdns.txt Section 8 Note: this is not yet a full implementation of the protocol, @@ -467,7 +477,7 @@ def checkService(self, info): info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type self.checkService(info) return - raise NonUniqueNameException + raise dns.NonUniqueNameException if now < nextTime: self.wait(nextTime - now) now = dns.currentTimeMillis() @@ -540,56 +550,55 @@ def handleQuery(self, msg, addr, port): log.debug( 'Questions...') for question in msg.questions: log.debug( 'Question: %s', question ) - if question.type == dns._TYPE_PTR: - log.debug( 'Question name: %r', question.name ) - for service in self.services.values(): - log.debug( 'Check service: %s', service.type ) - if question.name in (service.type,service.name): - log.info( 'Service query found %s', service.name ) - if out is None: - out = dns.DNSOutgoing(dns._FLAGS_QR_RESPONSE | dns._FLAGS_AA) - out.addAnswer(msg, dns.DNSPointer(service.type, dns._TYPE_PTR, dns._CLASS_IN, dns._DNS_TTL, service.name)) - # devices such as AAstra phones will not re-query to - # resolve the pointer, they expect the final IP to show up - # in the response - out.addAdditionalAnswer(dns.DNSText(service.name, dns._TYPE_TXT, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.text)) - out.addAdditionalAnswer(dns.DNSService(service.name, dns._TYPE_SRV, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.priority, service.weight, service.port, service.server)) - out.addAdditionalAnswer(dns.DNSAddress(service.server, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) - else: - try: - if out is None: - out = dns.DNSOutgoing(dns._FLAGS_QR_RESPONSE | dns._FLAGS_AA) - - # Answer A record queries for any service addresses we know - if question.type == dns._TYPE_A or question.type == dns._TYPE_ANY: - for service in self.services.values(): - if service.server == question.name.lower(): - out.addAnswer(msg, dns.DNSAddress(question.name, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) - - service = self.services.get(question.name.lower(), None) - if not service: - continue - log.info( 'Question was matched to service' ) - - if question.type == dns._TYPE_SRV or question.type == dns._TYPE_ANY: - out.addAnswer(msg, dns.DNSService(question.name, dns._TYPE_SRV, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.priority, service.weight, service.port, service.server)) - if question.type == dns._TYPE_TXT or question.type == dns._TYPE_ANY: - out.addAnswer(msg, dns.DNSText(question.name, dns._TYPE_TXT, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.text)) - if question.type == dns._TYPE_SRV: - out.addAdditionalAnswer(dns.DNSAddress(service.server, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) - except Exception, err: - log.error( - 'Error handling query: %s',traceback.format_exc() - ) - + if out is None: + out = dns.DNSOutgoing(dns._FLAGS_QR_RESPONSE | dns._FLAGS_AA) + try: + self.responses( question, msg, out ) + except Exception, err: + log.error( + 'Error handling query: %s',traceback.format_exc() + ) if out is not None and out.answers: out.id = msg.id self.send(out, addr, port) log.info( 'Sent response' ) -# elif out is not None: -# log.debug( "Out was created, but got no answers" ) else: - log.debug( 'No answer for %s', [q for q in msg.questions] ) + log.debug( 'No (newer) answer for %s', [q for q in msg.questions] ) + + def responses( self, question, msg, out ): + """Adds all responses to out which match the given question + + Note that the incoming query may suppress our responses + by having cache times higher than our records. That is, + out.answers may be null even if we have the records that + match the query. + """ + log.info( 'Question: %s', question ) + for service in self.services.values(): + if question.type == dns._TYPE_PTR: + if question.name in (service.type,service.name): + log.info( 'Service query found %s', service.name ) + out.addAnswer(msg, dns.DNSPointer(question.name, dns._TYPE_PTR, dns._CLASS_IN, dns._DNS_TTL, service.name)) + # devices such as AAstra phones will not re-query to + # resolve the pointer, they expect the final IP to show up + # in the response + out.addAdditionalAnswer(dns.DNSText(service.name, dns._TYPE_TXT, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.text)) + out.addAdditionalAnswer(dns.DNSService(service.name, dns._TYPE_SRV, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.priority, service.weight, service.port, service.server)) + out.addAdditionalAnswer(dns.DNSAddress(service.server, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) + else: + if question.type in (dns._TYPE_A, dns._TYPE_AAAA): + if service.server == question.name.lower(): + out.addAnswer(msg, dns.DNSAddress(question.name, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) + if question.type in (dns._TYPE_SRV, dns._TYPE_ANY): + if question.name in (service.name,service.server,service.type): + out.addAnswer(msg, dns.DNSService(question.name, dns._TYPE_SRV, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.priority, service.weight, service.port, service.server)) + if question.type in (dns._TYPE_TXT, dns._TYPE_ANY): + if question.name in (service.name,service.server,service.type): + out.addAnswer(msg, dns.DNSText(question.name, dns._TYPE_TXT, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.text)) + if question.type in (dns._TYPE_SRV,dns._TYPE_ANY ): + # srv queries need the address for aastra-style single query + if question.name in (service.name,service.server,service.type): + out.addAdditionalAnswer(dns.DNSAddress(service.server, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) def send(self, out, addr = dns._MDNS_ADDR, port = dns._MDNS_PORT): """Sends an outgoing packet.""" From f0b18ddb27e67b0bbe8abfb2dca5bc109ae1cbeb Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Tue, 10 May 2011 13:58:16 -0400 Subject: [PATCH 19/36] Bump version for local test release --- zeroconf/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/zeroconf/__init__.py b/zeroconf/__init__.py index 16a2e5cd..97764b35 100644 --- a/zeroconf/__init__.py +++ b/zeroconf/__init__.py @@ -22,8 +22,11 @@ """ +"""0.12.1 update - support for certain broken clients (e.g. aastra phones), + partial support for negotiating host-names (probe operation).""" + """0.12 update - allow selection of binding interface - typo fix - Thanks A. M. Kuchlingi + typo fix - Thanks A. M. Kuchling removed all use of word 'Rendezvous' - this is an API change""" """0.11 update - correction to comments for addListener method @@ -76,7 +79,7 @@ __author__ = "Paul Scott-Murphy" __email__ = "paul at scott dash murphy dot com" -__version__ = "0.12" +__version__ = "0.12.1" #from Zeroconf import dns #from Zeroconf import mcastsocket From da465c0854a26153157b5398b87f6c7682fbf35b Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Tue, 10 May 2011 16:11:44 -0400 Subject: [PATCH 20/36] Log notification of suppressions, add clear for cache entries --- zeroconf/dns.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/zeroconf/dns.py b/zeroconf/dns.py index e8133b25..9b4c246b 100644 --- a/zeroconf/dns.py +++ b/zeroconf/dns.py @@ -233,6 +233,7 @@ def suppressedBy(self, msg): information held in this record.""" for record in msg.answers: if self.suppressedByAnswer(record): + log.debug( 'Suppressing %s due to query record %s', self, record ) return 1 return 0 @@ -771,6 +772,10 @@ def add(x, y): return x+y return reduce(add, self.cache.values()) except: return [] + + def clear( self ): + """Clear our cache of entries""" + self.cache.clear() class ServiceInfo(object): """Service information""" From b30d2a287db100d517297b25629bf099a11643db Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Wed, 11 May 2011 13:20:28 -0400 Subject: [PATCH 21/36] Make all raised exceptions classes, more attempts to get the .local. name probing working correctly/closer to spec. --- zeroconf/dns.py | 45 ++++++++++++++++++++------------- zeroconf/mdns.py | 66 ++++++++++++++++++++++++++---------------------- 2 files changed, 63 insertions(+), 48 deletions(-) diff --git a/zeroconf/dns.py b/zeroconf/dns.py index 05af29ea..2a34da9e 100644 --- a/zeroconf/dns.py +++ b/zeroconf/dns.py @@ -43,6 +43,10 @@ _LISTENER_TIME = 200 _BROWSER_TIME = 500 +_PROBE_TIME = 250 +_PROBE_THROTTLED_TIME = 5000 +_PROBE_TIMEOUT = 800 + # Some DNS constants _MDNS_ADDR = '224.0.0.251' @@ -134,21 +138,24 @@ def currentTimeMillis(): # Exceptions -class NonLocalNameException(Exception): - pass +class DNSError( Exception ): + """Base class for all DNS errors""" -class NonUniqueNameException(Exception): +class DNSNameError( DNSError ): + """Name/type related errors""" +class NonLocalNameException(DNSNameError): pass - -class NamePartTooLongException(Exception): +class NonUniqueNameException(DNSNameError): pass - -class AbstractMethodException(Exception): +class NamePartTooLongException(DNSNameError): + pass +class BadTypeInNameException(DNSNameError): pass -class BadTypeInNameException(Exception): +class AbstractMethodException(DNSError): pass + # implementation classes class DNSEntry(object): @@ -204,7 +211,7 @@ class DNSQuestion(DNSEntry): def __init__(self, name, type_, clazz): if not name.endswith(".local."): - raise NonLocalNameException + raise NonLocalNameException( 'No .local. suffix in %r'%(name,) ) DNSEntry.__init__(self, name, type_, clazz) def answeredBy(self, rec): @@ -271,7 +278,7 @@ def resetTTL(self, other): def write(self, out): """Abstract method""" - raise AbstractMethodException + raise AbstractMethodException( 'write' ) def toString(self, other): """String representation with addtional information""" @@ -552,10 +559,10 @@ def readName(self): next = off + 1 off = ((len_ & 0x3F) << 8) | ord(self.data[off]) if off >= first: - raise "Bad domain name (circular) at " + str(off) + raise DNSNameError( "Bad domain name (circular) at char %s", off ) first = off else: - raise "Bad domain name at " + str(off) + raise DNSNameError( "Bad domain name (unknown encoding type) at " + str(off) ) if next >= 0: self.offset = next @@ -640,7 +647,7 @@ def writeUTF(self, s): utfstr = s.encode('utf-8') length = len(utfstr) if length > 64: - raise NamePartTooLongException + raise NamePartTooLongException( utfstr ) self.writeByte(length) self.writeString(utfstr, length) @@ -799,7 +806,7 @@ def __init__(self, type_, name, address=None, port=None, weight=0, priority=0, p server: fully qualified name for service host (defaults to name)""" if not name.endswith(type_): - raise BadTypeInNameException + raise BadTypeInNameException( 'Name: %r does not end with type %r', name, type ) self.type = type_ self.name = name self.address = address @@ -1001,9 +1008,10 @@ def __init__(self, name, ignore=None ): self.name = name self.address = None self.ignore = ignore - def request( self, zeroconf, timeout=3000 ): + if ignore: + self.ignore = [socket.inet_aton( a ) for a in ignore] + def request( self, zeroconf, timeout=_PROBE_TIMEOUT, delay=_PROBE_TIME ): now = currentTimeMillis() - delay = _LISTENER_TIME next = now + delay last = now + timeout result = 0 @@ -1014,7 +1022,7 @@ def request( self, zeroconf, timeout=3000 ): return 0 if next <= now: out = DNSOutgoing(_FLAGS_QR_QUERY) - out.addQuestion(DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN)) + out.addQuestion(DNSQuestion(self.name, _TYPE_A, _CLASS_IN)) zeroconf.send(out) next = now + delay delay = delay * 2 @@ -1030,7 +1038,8 @@ def updateRecord(self, zeroconf, now, record): if record is not None and not record.isExpired(now): if record.name == self.name: if ( - self.ignore and getattr(record,'address',None) in self.ignore + self.ignore and + (getattr(record,'address',None) not in self.ignore) ) or (not self.ignore): # something is using this name, whether for a server-name or not... if self.address in (True,None): diff --git a/zeroconf/mdns.py b/zeroconf/mdns.py index 427d7286..50f0dde3 100644 --- a/zeroconf/mdns.py +++ b/zeroconf/mdns.py @@ -142,24 +142,30 @@ def handle_read(self): log.info( 'Error on recvfrom: %s', err ) return None self.data = data - msg = dns.DNSIncoming(data) - if msg.isQuery(): - # Always multicast responses - # - if port == dns._MDNS_PORT: - self.zeroconf.handleQuery(msg, dns._MDNS_ADDR, dns._MDNS_PORT) - # If it's not a multicast query, reply via unicast - # - # and multicast - elif port == dns._DNS_PORT: - self.zeroconf.handleQuery(msg, addr, port) - self.zeroconf.handleQuery(msg, dns._MDNS_ADDR, dns._MDNS_PORT) - else: - log.error( - "Unknown port: %s", port - ) + try: + msg = dns.DNSIncoming(data) + except dns.NonLocalNameException, err: + """We ignore mdns queries for non-local addresses, such as in-addr.arpa.""" + except dns.DNSError, err: + log.error( "Malformed packet from %s (%s), ignored: %r", addr, err, data ) else: - self.zeroconf.handleResponse(msg) + if msg.isQuery(): + # Always multicast responses + # + if port == dns._MDNS_PORT: + self.zeroconf.handleQuery(msg, dns._MDNS_ADDR, dns._MDNS_PORT) + # If it's not a multicast query, reply via unicast + # + # and multicast + elif port == dns._DNS_PORT: + self.zeroconf.handleQuery(msg, addr, port) + self.zeroconf.handleQuery(msg, dns._MDNS_ADDR, dns._MDNS_PORT) + else: + log.error( + "Unknown port: %s", port + ) + else: + self.zeroconf.handleResponse(msg) class Reaper(threading.Thread): """A Reaper is used by this module to remove cache entries that @@ -315,7 +321,7 @@ def notifyAll(self): self.condition.notifyAll() self.condition.release() - def probeName( self, name, timeout=250 ): + def probeName( self, name, timeout=dns._PROBE_TIMEOUT ): """Probe host-names until we find an unoccupied one name -- name from which to construct the host-name, note that this @@ -346,27 +352,27 @@ def names( name ): count += 1 if count > 2**16: raise RuntimeError( """Unable to find an unused name %s"""%( name, )) + delay = dns._PROBE_TIME for failures,name in enumerate(names(name)): - for i in range(3): - address = self.getServerAddress( - name, timeout, - ignore=[self.intf] # our own address is *not* to be considered - ) - if address: - break + address = self.getServerAddress( + name, timeout, + delay = delay, + ignore=[self.intf] # our own address is *not* to be considered + ) if failures > 5: # mDNS requires huge slowdown after 15 failures # to prevent flooding network... log.warn( "Throttling host-name configuration to prevent network flood" ) - timeout = 5.0 + delay = dns._PROBE_THROTTLED_TIME + timeout = 2.5 * delay if not address: return name - def getServerAddress(self, name, timeout=3000, ignore=None): + def getServerAddress(self, name, timeout=dns._PROBE_TIMEOUT, ignore=None, delay=dns._PROBE_TIME): """Returns given server-name record or None on timeout""" info = dns.ServerNameWatcher( name, ignore=ignore ) - if info.request( self, timeout ): + if info.request( self, timeout, delay=delay ): return info.address return None @@ -477,7 +483,7 @@ def checkService(self, info): info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type self.checkService(info) return - raise dns.NonUniqueNameException + raise dns.NonUniqueNameException( info.name ) if now < nextTime: self.wait(nextTime - now) now = dns.currentTimeMillis() @@ -586,7 +592,7 @@ def responses( self, question, msg, out ): out.addAdditionalAnswer(dns.DNSService(service.name, dns._TYPE_SRV, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.priority, service.weight, service.port, service.server)) out.addAdditionalAnswer(dns.DNSAddress(service.server, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) else: - if question.type in (dns._TYPE_A, dns._TYPE_AAAA): + if question.type in (dns._TYPE_A, ): if service.server == question.name.lower(): out.addAnswer(msg, dns.DNSAddress(question.name, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) if question.type in (dns._TYPE_SRV, dns._TYPE_ANY): From 0df59dea2fd133a0d2963b03e648d576fce03f84 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Wed, 11 May 2011 13:25:43 -0400 Subject: [PATCH 22/36] Add a trivial script to probe for an existing mdns name, reduce a logging message level --- samplecode/nameprobe.py | 16 ++++++++++++++++ zeroconf/mdns.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 samplecode/nameprobe.py diff --git a/samplecode/nameprobe.py b/samplecode/nameprobe.py new file mode 100644 index 00000000..a10a83e9 --- /dev/null +++ b/samplecode/nameprobe.py @@ -0,0 +1,16 @@ +#! /usr/bin/env python +from zeroconf import mdns, mcastsocket, dns + +def main(): + z = mdns.Zeroconf( '0.0.0.0' ) + try: + print z.probeName( 'coolserver.local.' ) + finally: + z.close() + +if __name__ == "__main__": + import logging + logging.basicConfig( + #level = logging.DEBUG + ) + main() diff --git a/zeroconf/mdns.py b/zeroconf/mdns.py index 50f0dde3..15da6844 100644 --- a/zeroconf/mdns.py +++ b/zeroconf/mdns.py @@ -362,7 +362,7 @@ def names( name ): if failures > 5: # mDNS requires huge slowdown after 15 failures # to prevent flooding network... - log.warn( + log.info( "Throttling host-name configuration to prevent network flood" ) delay = dns._PROBE_THROTTLED_TIME From bb3446d637d5e5bcf5a42c65dd2615d7d1ad1f88 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Wed, 11 May 2011 15:42:09 -0400 Subject: [PATCH 23/36] Guard against some common name issues (preceding ., .. in name), generalize ignored record types, ignore 0 query types (seen on real networks) --- test/ZeroconfTest.py | 45 +++++++++++++++++++++++++++++--------------- zeroconf/dns.py | 38 +++++++++++++++++++++++++++---------- zeroconf/mdns.py | 24 +++++++++++++++-------- 3 files changed, 74 insertions(+), 33 deletions(-) diff --git a/test/ZeroconfTest.py b/test/ZeroconfTest.py index 01dfad51..b95de148 100755 --- a/test/ZeroconfTest.py +++ b/test/ZeroconfTest.py @@ -1,22 +1,22 @@ """ Multicast DNS Service Discovery for Python, v0.12 - Copyright (C) 2003, Paul Scott-Murphy + Copyright (C) 2003, Paul Scott-Murphy - This module provides a unit test suite for the Multicast DNS - Service Discovery for Python module. + This module provides a unit test suite for the Multicast DNS + Service Discovery for Python module. - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ @@ -26,7 +26,7 @@ from zeroconf import dns as r from zeroconf import mdns -import unittest +import unittest,socket class PacketGeneration(unittest.TestCase): @@ -155,6 +155,19 @@ def testSameName(self): generated.addQuestion(question) parsed = r.DNSIncoming(generated.packet()) + def testServiceDiscoverMessage(self): + info = mdns.ServiceInfo( + '_test._tcp.local.', + 'blue._test._tcp.local.', + socket.inet_aton('127.0.0.1'), + 80, + 0, + 0, + {}, + server = 'myhost.local', + ) + out = mdns.Zeroconf.serviceAnnouncement( info ) + temp = r.DNSIncoming(out.packet()) class Framework(unittest.TestCase): @@ -163,4 +176,6 @@ def testLaunchAndClose(self): rv.close() if __name__ == '__main__': + import logging + logging.basicConfig( level=logging.DEBUG ) unittest.main() diff --git a/zeroconf/dns.py b/zeroconf/dns.py index 2a34da9e..a526059d 100644 --- a/zeroconf/dns.py +++ b/zeroconf/dns.py @@ -130,6 +130,13 @@ _TYPE_NSEC : "nsec", _TYPE_ANY : "any" } +_IGNORED_TYPES = { + 0: "Ignoring Invalid DNS query type (0)", + _TYPE_NS: "Ignoring Name Server (NS) query", + _TYPE_NSEC: "Ignoring Next Secure Record (NSEC) query", + _TYPE_MF: "Ignoring Obsoleted Mail Forwarding (MF) query", +} + # utility functions def currentTimeMillis(): @@ -230,6 +237,12 @@ def __init__(self, name, type_, clazz, ttl): DNSEntry.__init__(self, name, type_, clazz) self.ttl = ttl self.created = currentTimeMillis() + if '..' in name: + raise DNSNameError( '.. not allowed in dns names: %r'%( name )) + if name.startswith( '.' ): + raise DNSNameError( 'dns names cannot start with .: %r'%( name )) + if not type_ in _TYPES: + raise RuntimeError( type_ ) def __eq__(self, other): """Tests equality as per DNSRecord""" @@ -492,7 +505,9 @@ def readOthers(self): rec = None if info[0] == _TYPE_A: rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4)) - elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR: + elif info[0] == _TYPE_CNAME: + rec = DNSPointer(domain, info[0], info[1], info[2], domain) + elif info[0] == _TYPE_PTR: rec = DNSPointer(domain, info[0], info[1], info[2], self.readName()) elif info[0] == _TYPE_TXT: rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3])) @@ -502,10 +517,8 @@ def readOthers(self): rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString()) elif info[0] == _TYPE_AAAA: rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16)) - elif info[0] == _TYPE_MF: - log.debug("Obsoleted Mail Forwarding (MF) type") - elif info[0] == _TYPE_NSEC: - log.debug("Ignoring DNSSEC record type") + elif info[0] in _IGNORED_TYPES: + log.debug( "%s", _IGNORED_TYPES[info[0]]) else: # Try to ignore types we don't know about # this may mean the rest of the name is @@ -514,13 +527,13 @@ def readOthers(self): # encountered need to be parsed properly. # log.warn( - "Unknown DNS query type: %s", info + "Unknown DNS query type: %s %r", info, self.data ) - if rec is not None: self.answers.append(rec) except Exception, err: - log.warn( "Failure on record type_ %s, ignoring: %s", info[0], err ) + log.debug( "Failure on record type_ %s, ignoring: %s", info[0], err ) + log.debug( "%s", traceback.format_exc()) def isQuery(self): """Returns true if this is a query""" @@ -562,7 +575,7 @@ def readName(self): raise DNSNameError( "Bad domain name (circular) at char %s", off ) first = off else: - raise DNSNameError( "Bad domain name (unknown encoding type) at " + str(off) ) + raise DNSNameError( "Bad domain name (unknown encoding type %r) at %s"%( t,str(off) )) if next >= 0: self.offset = next @@ -806,7 +819,11 @@ def __init__(self, type_, name, address=None, port=None, weight=0, priority=0, p server: fully qualified name for service host (defaults to name)""" if not name.endswith(type_): - raise BadTypeInNameException( 'Name: %r does not end with type %r', name, type ) + raise BadTypeInNameException( 'Name: %r does not end with type %r', name, type_ ) + if type_.startswith( '.' ): + raise DNSNameError( 'Types cannot start with the . character %r'%( type_ )) + if '..' in type_: + raise DNSNameError( 'Types cannot contain .. %r'%( type_ )) self.type = type_ self.name = name self.address = address @@ -1023,6 +1040,7 @@ def request( self, zeroconf, timeout=_PROBE_TIMEOUT, delay=_PROBE_TIME ): if next <= now: out = DNSOutgoing(_FLAGS_QR_QUERY) out.addQuestion(DNSQuestion(self.name, _TYPE_A, _CLASS_IN)) + out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_A, _CLASS_IN), now) zeroconf.send(out) next = now + delay delay = delay * 2 diff --git a/zeroconf/mdns.py b/zeroconf/mdns.py index 15da6844..b09d410a 100644 --- a/zeroconf/mdns.py +++ b/zeroconf/mdns.py @@ -147,7 +147,10 @@ def handle_read(self): except dns.NonLocalNameException, err: """We ignore mdns queries for non-local addresses, such as in-addr.arpa.""" except dns.DNSError, err: - log.error( "Malformed packet from %s (%s), ignored: %r", addr, err, data ) + log.error( + "Malformed packet from %s (%s), ignored: %r", + addr, err, data + ) else: if msg.isQuery(): # Always multicast responses @@ -414,15 +417,20 @@ def registerService(self, info, ttl=dns._DNS_TTL): self.wait(nextTime - now) now = dns.currentTimeMillis() continue - out = dns.DNSOutgoing(dns._FLAGS_QR_RESPONSE | dns._FLAGS_AA) - out.addAnswerAtTime(dns.DNSPointer(info.type, dns._TYPE_PTR, dns._CLASS_IN, ttl, info.name), 0) - out.addAnswerAtTime(dns.DNSService(info.name, dns._TYPE_SRV, dns._CLASS_IN, ttl, info.priority, info.weight, info.port, info.server), 0) - out.addAnswerAtTime(dns.DNSText(info.name, dns._TYPE_TXT, dns._CLASS_IN, ttl, info.text), 0) - if info.address: - out.addAnswerAtTime(dns.DNSAddress(info.server, dns._TYPE_A, dns._CLASS_IN, ttl, info.address), 0) + out = self.serviceAnnouncement( info, ttl ) self.send(out) i += 1 nextTime += _REGISTER_TIME + + @classmethod + def serviceAnnouncement( cls, info, ttl=dns._DNS_TTL ): + out = dns.DNSOutgoing(dns._FLAGS_QR_RESPONSE | dns._FLAGS_AA) + out.addAnswerAtTime(dns.DNSPointer(info.type, dns._TYPE_PTR, dns._CLASS_IN, ttl, info.name), 0) + out.addAnswerAtTime(dns.DNSService(info.name, dns._TYPE_SRV, dns._CLASS_IN, ttl, info.priority, info.weight, info.port, info.server), 0) + out.addAnswerAtTime(dns.DNSText(info.name, dns._TYPE_TXT, dns._CLASS_IN, ttl, info.text), 0) + if info.address: + out.addAnswerAtTime(dns.DNSAddress(info.server, dns._TYPE_A, dns._CLASS_IN, ttl, info.address), 0) + return out def unregisterService(self, info): """Unregister a service.""" @@ -609,7 +617,7 @@ def responses( self, question, msg, out ): def send(self, out, addr = dns._MDNS_ADDR, port = dns._MDNS_PORT): """Sends an outgoing packet.""" # This is a quick test to see if we can parse the packets we generate - #temp = dns.DNSIncoming(out.packet()) + # temp = dns.DNSIncoming(out.packet()) try: packet = out.packet() bytes_sent = self.socket.sendto(packet, 0, (addr, port)) From 26e5dbd569023060f8863b197ef593a33f463b87 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Fri, 13 May 2011 10:06:28 -0400 Subject: [PATCH 24/36] Reduce logging level on a number of messages --- zeroconf/dns.py | 2 +- zeroconf/mdns.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/zeroconf/dns.py b/zeroconf/dns.py index a526059d..99fe22ab 100644 --- a/zeroconf/dns.py +++ b/zeroconf/dns.py @@ -526,7 +526,7 @@ def readOthers(self): # so this is left for debugging. New types # encountered need to be parsed properly. # - log.warn( + log.info( "Unknown DNS query type: %s %r", info, self.data ) if rec is not None: diff --git a/zeroconf/mdns.py b/zeroconf/mdns.py index b09d410a..dac1de6a 100644 --- a/zeroconf/mdns.py +++ b/zeroconf/mdns.py @@ -85,16 +85,16 @@ def run(self): if err[0] in (errno.EWOULDBLOCK,errno.EINTR,errno.EAGAIN): pass else: - log.warn( 'Failure on select, ignoring: %s', err ) + log.info( 'Failure on select, ignoring: %s', err ) except Exception, err: - log.warn( 'Select failure, ignored: %s', err ) + log.info( 'Select failure, ignored: %s', err ) else: for sock in rr: try: self.readers[sock].handle_read() except Exception, err: # Ignore errors that occur on shutdown - log.error( 'Error handling read: %s', err ) + log.info( 'Error handling read: %s', err ) log.debug( 'Traceback: %s', traceback.format_exc()) def getReaders(self): @@ -575,7 +575,7 @@ def handleQuery(self, msg, addr, port): if out is not None and out.answers: out.id = msg.id self.send(out, addr, port) - log.info( 'Sent response' ) + log.debug( 'Sent response' ) else: log.debug( 'No (newer) answer for %s', [q for q in msg.questions] ) @@ -587,11 +587,11 @@ def responses( self, question, msg, out ): out.answers may be null even if we have the records that match the query. """ - log.info( 'Question: %s', question ) + log.debug( 'Question: %s', question ) for service in self.services.values(): if question.type == dns._TYPE_PTR: if question.name in (service.type,service.name): - log.info( 'Service query found %s', service.name ) + log.debug( 'Service query found %s', service.name ) out.addAnswer(msg, dns.DNSPointer(question.name, dns._TYPE_PTR, dns._CLASS_IN, dns._DNS_TTL, service.name)) # devices such as AAstra phones will not re-query to # resolve the pointer, they expect the final IP to show up From 8b1f0b997557368696246751706f595cddace0dd Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Wed, 29 Jun 2011 16:05:42 -0400 Subject: [PATCH 25/36] Add a suppression queue to prevent flooding the network if another client requests the same data many times very rapidly (i.e. at greater than .2s frequency); basically just rate-limits the outgoing data for equal response packets --- zeroconf/mdns.py | 46 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/zeroconf/mdns.py b/zeroconf/mdns.py index dac1de6a..a4917b34 100644 --- a/zeroconf/mdns.py +++ b/zeroconf/mdns.py @@ -46,6 +46,8 @@ _REGISTER_TIME = 225 _LISTENER_TIME = 200 _BROWSER_TIME = 500 +# Minimum time before sending duplicate message... +_MINIMUM_REPEAT_TIME = 200 class Engine(threading.Thread): """An engine wraps read access to sockets, allowing objects that @@ -294,6 +296,7 @@ def __init__(self, bindaddress=None): mcastsocket.join_group( self.socket, dns._MDNS_ADDR ) self.listeners = [] + self.suppression_queue = [] self.browsers = [] self.services = {} @@ -334,7 +337,9 @@ def probeName( self, name, timeout=dns._PROBE_TIMEOUT ): TODO: maybe make this part of checkService instead? Same basic operation, it's just looking for unique server name instead of unique - service name. + service name. The spec explicitly seems to expect that, but the + current checkService isn't doing most of the probe operations as + defined. http://files.multicastdns.org/draft-cheshire-dnsext-multicastdns.txt Section 8 @@ -615,15 +620,35 @@ def responses( self, question, msg, out ): out.addAdditionalAnswer(dns.DNSAddress(service.server, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) def send(self, out, addr = dns._MDNS_ADDR, port = dns._MDNS_PORT): - """Sends an outgoing packet.""" - # This is a quick test to see if we can parse the packets we generate - # temp = dns.DNSIncoming(out.packet()) - try: - packet = out.packet() - bytes_sent = self.socket.sendto(packet, 0, (addr, port)) - except: - # Ignore this, it may be a temporary loss of network connection - pass + """Sends an outgoing packet. + + Note: this method is instrumented to provide low-level + prevention of packet floods by throttling same-message + sending to once per _MINIMUM_REPEAT_TIME ms. That will + fail for a "regular" DNS server, which should also use + the addr/port combo... + """ + current = dns.currentTimeMillis() + log.info( '%s messages in suppression_queue', len(self.suppression_queue)) + while self.suppression_queue and self.suppression_queue[0][0] < current: + log.debug( 'Removing...' ) + self.suppression_queue.pop(0) + packet = out.packet() + sent = False + for i,(expire,old_packet) in enumerate(self.suppression_queue[:]): + if old_packet == packet: + log.warn( 'Dropping to prevent flood' ) + sent = True + if not sent: + try: + sent = self.socket.sendto(packet, 0, (addr, port)) + except: + # Ignore this, it may be a temporary loss of network connection + pass + self.suppression_queue.append( + (current + _MINIMUM_REPEAT_TIME, packet ) + ) + return sent def close(self): """Ends the background threads, and prevent this instance from @@ -635,3 +660,4 @@ def close(self): self.unregisterAllServices() mcastsocket.leave_group( self.socket, dns._MDNS_ADDR ) self.socket.close() + From 946eba2bbf8721aee7d580c2b67de71885e03ad4 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Wed, 24 Aug 2011 16:23:46 -0400 Subject: [PATCH 26/36] Make browser demo listen on all interfaces and allow for querying other services from the command-line --- samplecode/Browser.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/samplecode/Browser.py b/samplecode/Browser.py index f3157e74..c5ced322 100755 --- a/samplecode/Browser.py +++ b/samplecode/Browser.py @@ -3,7 +3,7 @@ class MyListener(object): def __init__(self): - self.r = Zeroconf() + self.r = Zeroconf('') pass def removeService(self, zeroconf, type_, name): @@ -12,28 +12,37 @@ def removeService(self, zeroconf, type_, name): def addService(self, zeroconf, type_, name): print "Service", name, "added" print "Type is", type_ - info = self.r.getServiceInfo(type_, name) - if not info: - print " (timeout)" - return + info = None + retries = 0 + while not info and retries < 10: + info = self.r.getServiceInfo(type_, name) + if not info: + print " (timeout)" + retries += 1 print "Address is", str(socket.inet_ntoa(info.getAddress())) print "Port is", info.getPort() print "Weight is", info.getWeight() print "Priority is", info.getPriority() print "Server is", info.getServer() - print "Text is", info.getText() + print "Text is", repr(info.getText()) print "Properties are", info.getProperties() if __name__ == '__main__': - import logging + import logging, sys logging.basicConfig(level=logging.WARNING) print "Multicast DNS Service Discovery for Python Browser test" + if sys.argv[1:]: + type_ = sys.argv[1] + else: + type_ = "_http._tcp.local." r = Zeroconf() try: - print "1. Testing browsing for a service..." - type_ = "_http._tcp.local." - listener = MyListener() - browser = ServiceBrowser(r, type_, listener) - raw_input( 'Press to stop listening > ') + print "1. Testing browsing for a service (ctrl-c to stop)..." + try: + listener = MyListener() + browser = ServiceBrowser(r, type_, listener) + raw_input( 'Press to exit' ) + except KeyboardInterrupt, err: + print 'Exiting' finally: r.close() From c9503b6421143e95a58b5b00362f59fb67b5275a Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Wed, 24 Aug 2011 16:24:33 -0400 Subject: [PATCH 27/36] Make all name comparisons use lowercasing/samecasing --- zeroconf/__init__.py | 2 +- zeroconf/dns.py | 4 ++-- zeroconf/mdns.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/zeroconf/__init__.py b/zeroconf/__init__.py index 97764b35..8d90bee1 100644 --- a/zeroconf/__init__.py +++ b/zeroconf/__init__.py @@ -79,7 +79,7 @@ __author__ = "Paul Scott-Murphy" __email__ = "paul at scott dash murphy dot com" -__version__ = "0.12.1" +__version__ = "0.12.2" #from Zeroconf import dns #from Zeroconf import mcastsocket diff --git a/zeroconf/dns.py b/zeroconf/dns.py index 99fe22ab..f353bc78 100644 --- a/zeroconf/dns.py +++ b/zeroconf/dns.py @@ -1033,13 +1033,13 @@ def request( self, zeroconf, timeout=_PROBE_TIMEOUT, delay=_PROBE_TIME ): last = now + timeout result = 0 try: - zeroconf.addListener(self, DNSQuestion( self.name, _TYPE_ANY, _CLASS_IN )) + zeroconf.addListener(self, DNSQuestion( self.name.lower(), _TYPE_ANY, _CLASS_IN )) while self.address is None: if last <= now: return 0 if next <= now: out = DNSOutgoing(_FLAGS_QR_QUERY) - out.addQuestion(DNSQuestion(self.name, _TYPE_A, _CLASS_IN)) + out.addQuestion(DNSQuestion(self.name.lower(), _TYPE_A, _CLASS_IN)) out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_A, _CLASS_IN), now) zeroconf.send(out) next = now + delay diff --git a/zeroconf/mdns.py b/zeroconf/mdns.py index a4917b34..2edb93d1 100644 --- a/zeroconf/mdns.py +++ b/zeroconf/mdns.py @@ -595,7 +595,7 @@ def responses( self, question, msg, out ): log.debug( 'Question: %s', question ) for service in self.services.values(): if question.type == dns._TYPE_PTR: - if question.name in (service.type,service.name): + if question.name.lower() in (service.type.lower(),service.name.lower()): log.debug( 'Service query found %s', service.name ) out.addAnswer(msg, dns.DNSPointer(question.name, dns._TYPE_PTR, dns._CLASS_IN, dns._DNS_TTL, service.name)) # devices such as AAstra phones will not re-query to From 4ad686bcf2089735a0381b7b6794884031eed935 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Wed, 24 Aug 2011 16:25:52 -0400 Subject: [PATCH 28/36] Add note on the change --- zeroconf/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zeroconf/__init__.py b/zeroconf/__init__.py index 8d90bee1..4dfcc66b 100644 --- a/zeroconf/__init__.py +++ b/zeroconf/__init__.py @@ -21,6 +21,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ +"""0.12.2 update - fix name matching being case-sensitive for certain queries""" """0.12.1 update - support for certain broken clients (e.g. aastra phones), partial support for negotiating host-names (probe operation).""" From 70feb1858c34639f2ecfe3612731f3e301f4ef87 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Wed, 24 Aug 2011 16:26:47 -0400 Subject: [PATCH 29/36] Fix referenced version for that fix --- zeroconf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zeroconf/__init__.py b/zeroconf/__init__.py index 4dfcc66b..f1250f43 100644 --- a/zeroconf/__init__.py +++ b/zeroconf/__init__.py @@ -21,7 +21,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -"""0.12.2 update - fix name matching being case-sensitive for certain queries""" +"""0.12.3 update - fix name matching being case-sensitive for certain queries""" """0.12.1 update - support for certain broken clients (e.g. aastra phones), partial support for negotiating host-names (probe operation).""" From 0ab36bd6af2c1274c08b8787ed4f290b37fb80fc Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Wed, 24 Aug 2011 16:27:19 -0400 Subject: [PATCH 30/36] Sigh, also make the version == that version --- zeroconf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zeroconf/__init__.py b/zeroconf/__init__.py index f1250f43..fd8227f2 100644 --- a/zeroconf/__init__.py +++ b/zeroconf/__init__.py @@ -80,7 +80,7 @@ __author__ = "Paul Scott-Murphy" __email__ = "paul at scott dash murphy dot com" -__version__ = "0.12.2" +__version__ = "0.12.3" #from Zeroconf import dns #from Zeroconf import mcastsocket From 81f69f7a0b0d1064c08c2d2eed0bcc16facdcf2a Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Tue, 17 May 2011 19:41:18 -0400 Subject: [PATCH 31/36] More hacking on upnp igd sample code --- samplecode/testupnpigd.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/samplecode/testupnpigd.py b/samplecode/testupnpigd.py index 93c4fea0..8c8da681 100755 --- a/samplecode/testupnpigd.py +++ b/samplecode/testupnpigd.py @@ -3,6 +3,22 @@ """ import socket,os,sys,select,logging from zeroconf import mcastsocket +try: + from lxml import etree +except ImportError: + try: + # Python 2.5 + import xml.etree.cElementTree as etree + except ImportError: + try: + # Python 2.5 + import xml.etree.ElementTree as etree + except ImportError: + try: + # normal cElementTree install + import cElementTree as etree + except ImportError: + import elementtree.ElementTree as etree GROUP = '239.255.255.250' PORT = 1900 @@ -12,10 +28,28 @@ MAN: ssdp:discover MX: 10 ST: ssdp:all""" +query = """M-SEARCH * HTTP/1.1 +HOST: %(ip)s:%(port)s +MAN: ssdp:discover +MX: 10 +ST: upnp:rootdevice""" + +def describe_device( record, indent = '' ): + print 'Found: ', record.find( 'friendlyName' ).text + for service in record.find( 'serviceList' ): + print indent, 'Service:', service.find( 'serviceType' ).text + if record.find( 'deviceList' ): + for device in record.find( 'deviceList' ): + describe_device( device, indent + ' ' ) + + +def parse( result ): + root = etree.fromstring( result ) + describe_device( root.find( 'device' ) ) def handle( sock, data, address ): """Handle incoming message about service""" - print 'received from %s:%s: '%(address,) + print 'received from %s: '%(address,) print data # :schemas-upnp-org:device:InternetGatewayDevice:1 From 02b30c3959ada73c17f910963a168d3c7bc1b61c Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Thu, 25 Aug 2011 08:47:09 -0400 Subject: [PATCH 32/36] Work on an explicit probe-watcher to follow spec'd rules --- zeroconf/dns.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++- zeroconf/mdns.py | 14 ++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/zeroconf/dns.py b/zeroconf/dns.py index f353bc78..1c171c9c 100644 --- a/zeroconf/dns.py +++ b/zeroconf/dns.py @@ -1019,7 +1019,54 @@ def __repr__(self): result += "]" return result - +class ProbeWatcher( object ): + """Watches for response to a probe (query) + + + """ + def __init__( self, query ): + """Probe for any conflict in query (DNSOutgoing)""" + assert hasattr( query, 'questions' ), query + self.query = query + self.found = False + self.records = [] + def request( self, zeroconf, timeout=_PROBE_TIMEOUT, delay=_PROBE_TIME ): + """Perform probe operations for our query""" + now = currentTimeMillis() + next = now + delay + last = now + timeout + result = 0 + try: + for question in self.query.questions: + zeroconf.addListener(self) + while not self.found: + if last <= now: + break + if next <= now: + out = DNSOutgoing(_FLAGS_QR_QUERY) + for question in self.query.questions: + out.addQuestion( question ) + out.addAnswerAtTime( + zeroconf.cache.getByDetails(question.name, question.type, question.clazz), + now + ) + zeroconf.send(out) + next = now + delay + # Note: non-standard, should here delay by delay without expanding for 3 queries *only* + delay = delay * 2 + zeroconf.wait(min(next, last) - now) + now = currentTimeMillis() + finally: + zeroconf.removeListener( self ) + return self.records + def updateRecord(self, zeroconf, now, record): + """Updates service information from a DNS record""" + if record is not None and not record.isExpired(now): + for question in self.query.questions: + if question.answeredBy( record ): + self.found = True + self.records.append( record ) + class ServerNameWatcher( object ): def __init__(self, name, ignore=None ): self.name = name diff --git a/zeroconf/mdns.py b/zeroconf/mdns.py index 2edb93d1..0403826e 100644 --- a/zeroconf/mdns.py +++ b/zeroconf/mdns.py @@ -350,6 +350,20 @@ def probeName( self, name, timeout=dns._PROBE_TIMEOUT ): Note: you *must* advertise your services *before* you issue this query, as race conditions will occur if two machines booted at the same time try to resolve the same name. + + Steps: + + wait( random.randint( 0,250 ) ) # msg + probe( question ) # 3 queries at 0, 250, 500 then wait another 250 for response + if response: # conflict + mutate( question ) + if conflict_count > 15: # within ten seconds + wait( 5000 ) + else: + wait( random.randint( 0, 1000)) + else: + return query + """ def names( name ): yield name From 9bc41d4ea2c6b2f87a28fc8225aa5faed9a4d5f6 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Thu, 25 Aug 2011 09:45:26 -0400 Subject: [PATCH 33/36] Make the name-probe demo a little more useful (i.e. actually reserve the name so that you can see the negotiation work) --- samplecode/nameprobe.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/samplecode/nameprobe.py b/samplecode/nameprobe.py index a10a83e9..30b04be6 100644 --- a/samplecode/nameprobe.py +++ b/samplecode/nameprobe.py @@ -1,16 +1,38 @@ #! /usr/bin/env python from zeroconf import mdns, mcastsocket, dns -def main(): +fake_type = '_test-server.local.' + +def main( base_name='coolserver.local.'): z = mdns.Zeroconf( '0.0.0.0' ) try: - print z.probeName( 'coolserver.local.' ) + name = '%s.%s'%( base_name.split('.')[0], fake_type ) + s = dns.ServiceInfo( + fake_type, + name, + server = base_name, + address = '127.0.0.1', + port = 8080, + properties = {}, + ) + z.registerService( s ) + name = z.probeName( 'coolserver.local.' ) + z.unregisterService( s ) + print 'Negotiated name:', name + s.server = name + z.checkService( s ) + z.registerService( s ) + raw_input( 'Press to release name > ' ) finally: z.close() if __name__ == "__main__": - import logging + import logging, sys logging.basicConfig( #level = logging.DEBUG ) - main() + if sys.argv[1:]: + name = sys.argv[1] + else: + name = 'coolserver.local.' + main(name) From a87975dc143891f20637484d4f839568dc2dfd38 Mon Sep 17 00:00:00 2001 From: "Mike C. Fletcher" Date: Thu, 25 Aug 2011 11:31:25 -0400 Subject: [PATCH 34/36] Force all checks to lower, again, to guard against multi-case values; should revisit for efficiency. Reduce warning level on expected message discards. --- zeroconf/dns.py | 6 +++--- zeroconf/mdns.py | 55 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/zeroconf/dns.py b/zeroconf/dns.py index 1c171c9c..7ba35b7c 100644 --- a/zeroconf/dns.py +++ b/zeroconf/dns.py @@ -941,13 +941,13 @@ def updateRecord(self, zeroconf, now, record): """Updates service information from a DNS record""" if record is not None and not record.isExpired(now): if record.type == _TYPE_A: - if record.name in (self.name,self.server): + if record.name.lower() in (self.name.lower(),self.server.lower()): log.debug( 'Got A record for %s', record.name ) self.address = record.address else: log.debug( 'Got A record for %s, wanted %s', record.name, self.name ) elif record.type == _TYPE_SRV: - if record.name == self.name: + if record.name.lower() == self.name.lower(): log.debug( 'Got SRV record for %s', record.name ) self.server = record.server self.port = record.port @@ -957,7 +957,7 @@ def updateRecord(self, zeroconf, now, record): self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN)) elif record.type == _TYPE_TXT: - if record.name == self.name: + if record.name.lower() == self.name.lower(): log.debug( 'Got TXT record for %s', record.name ) self.setText(record.text) diff --git a/zeroconf/mdns.py b/zeroconf/mdns.py index 0403826e..e096a606 100644 --- a/zeroconf/mdns.py +++ b/zeroconf/mdns.py @@ -594,7 +594,7 @@ def handleQuery(self, msg, addr, port): if out is not None and out.answers: out.id = msg.id self.send(out, addr, port) - log.debug( 'Sent response' ) + log.debug( 'Sent response: %s', out.answers ) else: log.debug( 'No (newer) answer for %s', [q for q in msg.questions] ) @@ -615,23 +615,52 @@ def responses( self, question, msg, out ): # devices such as AAstra phones will not re-query to # resolve the pointer, they expect the final IP to show up # in the response - out.addAdditionalAnswer(dns.DNSText(service.name, dns._TYPE_TXT, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.text)) - out.addAdditionalAnswer(dns.DNSService(service.name, dns._TYPE_SRV, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.priority, service.weight, service.port, service.server)) - out.addAdditionalAnswer(dns.DNSAddress(service.server, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) + out.addAdditionalAnswer(dns.DNSText( + service.name, dns._TYPE_TXT, + dns._CLASS_IN | dns._CLASS_UNIQUE, + dns._DNS_TTL, service.text + )) + out.addAdditionalAnswer(dns.DNSService( + service.name, dns._TYPE_SRV, + dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, + service.priority, service.weight, service.port, service.server + )) + out.addAdditionalAnswer(dns.DNSAddress( + service.server, dns._TYPE_A, + dns._CLASS_IN | dns._CLASS_UNIQUE, + dns._DNS_TTL, service.address + )) else: if question.type in (dns._TYPE_A, ): - if service.server == question.name.lower(): - out.addAnswer(msg, dns.DNSAddress(question.name, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) + if service.server.lower() == question.name.lower(): + out.addAnswer(msg, dns.DNSAddress( + question.name, dns._TYPE_A, + dns._CLASS_IN | dns._CLASS_UNIQUE, + dns._DNS_TTL, service.address + )) if question.type in (dns._TYPE_SRV, dns._TYPE_ANY): - if question.name in (service.name,service.server,service.type): - out.addAnswer(msg, dns.DNSService(question.name, dns._TYPE_SRV, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.priority, service.weight, service.port, service.server)) + if question.name.lower() in (service.name.lower(),service.server.lower(),service.type.lower()): + out.addAnswer(msg, dns.DNSService( + question.name, dns._TYPE_SRV, + dns._CLASS_IN | dns._CLASS_UNIQUE, + dns._DNS_TTL, service.priority, + service.weight, service.port, service.server + )) if question.type in (dns._TYPE_TXT, dns._TYPE_ANY): - if question.name in (service.name,service.server,service.type): - out.addAnswer(msg, dns.DNSText(question.name, dns._TYPE_TXT, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.text)) + if question.name.lower() in (service.name.lower(),service.server.lower(),service.type.lower()): + out.addAnswer(msg, dns.DNSText( + question.name, dns._TYPE_TXT, + dns._CLASS_IN | dns._CLASS_UNIQUE, + dns._DNS_TTL, service.text + )) if question.type in (dns._TYPE_SRV,dns._TYPE_ANY ): # srv queries need the address for aastra-style single query - if question.name in (service.name,service.server,service.type): - out.addAdditionalAnswer(dns.DNSAddress(service.server, dns._TYPE_A, dns._CLASS_IN | dns._CLASS_UNIQUE, dns._DNS_TTL, service.address)) + if question.name.lower() in (service.name.lower(),service.server.lower(),service.type.lower()): + out.addAdditionalAnswer(dns.DNSAddress( + service.server, dns._TYPE_A, + dns._CLASS_IN | dns._CLASS_UNIQUE, + dns._DNS_TTL, service.address + )) def send(self, out, addr = dns._MDNS_ADDR, port = dns._MDNS_PORT): """Sends an outgoing packet. @@ -651,7 +680,7 @@ def send(self, out, addr = dns._MDNS_ADDR, port = dns._MDNS_PORT): sent = False for i,(expire,old_packet) in enumerate(self.suppression_queue[:]): if old_packet == packet: - log.warn( 'Dropping to prevent flood' ) + log.debug( 'Dropping to prevent flood' ) sent = True if not sent: try: From 85893c59fb01d754532ed09ab9fa614e47000ebd Mon Sep 17 00:00:00 2001 From: "Craig R. Hughes" Date: Fri, 18 Nov 2011 11:06:16 -0800 Subject: [PATCH 35/36] Try and locate the internet-routing interface so we get that instead of 127.0.0.1 by default --- zeroconf/mdns.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/zeroconf/mdns.py b/zeroconf/mdns.py index e096a606..75b587f9 100644 --- a/zeroconf/mdns.py +++ b/zeroconf/mdns.py @@ -288,7 +288,14 @@ def __init__(self, bindaddress=None): multicast communications, listening and reaping threads.""" globals()['_GLOBAL_DONE'] = 0 if bindaddress is None: - self.intf = socket.gethostbyname(socket.gethostname()) + try: + """Try to find the internet-routing interface so we don't get 127.0.0.1""" + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('www.google.com',80)) + self.intf = s.getsockname()[0] + s.close() + except: + self.intf = socket.gethostbyname(socket.gethostname()) bindaddress = self.intf else: self.intf = bindaddress From 3cd0c284393f6f5063485f236ce9bea36ed36557 Mon Sep 17 00:00:00 2001 From: "Craig R. Hughes" Date: Fri, 18 Nov 2011 11:10:46 -0800 Subject: [PATCH 36/36] Stringify DNSAddress more better --- zeroconf/dns.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/zeroconf/dns.py b/zeroconf/dns.py index 7ba35b7c..6f3bb126 100644 --- a/zeroconf/dns.py +++ b/zeroconf/dns.py @@ -318,9 +318,12 @@ def __eq__(self, other): def __repr__(self): """String representation""" try: - return socket.inet_ntoa(self.address) + return self.toString(socket.inet_ntoa(self.address)) except: - return self.address + try: + return self.toString(socket.inet_ntop(socket.AF_INET6, self.address)) + except: + return self.address class DNSHinfo(DNSRecord): """A DNS host information record"""