diff -r 31e63e25b4cc -r 64beb9e9c749 util/Zeroconf.py --- a/util/Zeroconf.py Mon Aug 21 20:17:19 2017 +0000 +++ b/util/Zeroconf.py Mon Aug 21 23:22:58 2017 +0300 @@ -1,82 +1,78 @@ -""" 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" +# 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 import string import time @@ -86,6 +82,12 @@ import select import traceback + +__author__ = "Paul Scott-Murphy" +__email__ = "paul at scott dash murphy dot com" +__version__ = "0.12" + + __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"] # hook for threads @@ -101,27 +103,27 @@ _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 +_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 +_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 @@ -150,65 +152,76 @@ _TYPE_TXT = 16 _TYPE_AAAA = 28 _TYPE_SRV = 33 -_TYPE_ANY = 255 +_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" } +_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 @@ -230,14 +243,14 @@ """Class accessor""" try: return _CLASSES[clazz] - except: + except Exception: return "?(%s)" % (clazz) def getType(self, type): """Type accessor""" try: return _TYPES[type] - except: + except Exception: return "?(%s)" % (type) def toString(self, hdr, other): @@ -254,9 +267,10 @@ result += "]" return result + class DNSQuestion(DNSEntry): """A DNS question entry""" - + def __init__(self, name, type, clazz): if not name.endswith(".local."): raise NonLocalNameException @@ -273,7 +287,7 @@ 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 @@ -332,9 +346,10 @@ 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 @@ -353,9 +368,10 @@ """String representation""" try: return socket.inet_ntoa(self.address) - except: + except Exception: return self.address + class DNSHinfo(DNSRecord): """A DNS host information record""" @@ -378,10 +394,11 @@ 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 @@ -400,9 +417,10 @@ """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 @@ -424,9 +442,10 @@ 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 @@ -451,9 +470,10 @@ """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 @@ -464,7 +484,7 @@ self.numAnswers = 0 self.numAuthorities = 0 self.numAdditionals = 0 - + self.readHeader() self.readQuestions() self.readOthers() @@ -491,7 +511,7 @@ 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) @@ -512,7 +532,7 @@ def readString(self, len): """Reads a string of a given length from the packet""" format = '!' + str(len) + 's' - length = struct.calcsize(format) + length = struct.calcsize(format) info = struct.unpack(format, self.data[self.offset:self.offset+length]) self.offset += length return info[0] @@ -555,13 +575,13 @@ # so this is left for debugging. New types # encountered need to be parsed properly. # - #print "UNKNOWN TYPE = " + str(info[0]) - #raise BadTypeInNameException + # print "UNKNOWN TYPE = " + str(info[0]) + # raise BadTypeInNameException pass 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 @@ -574,7 +594,7 @@ """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 = '' @@ -607,12 +627,12 @@ self.offset = off return result - - + + class DNSOutgoing(object): """Object representation of an outgoing packet""" - - def __init__(self, flags, multicast = 1): + + def __init__(self, flags, multicast=1): self.finished = 0 self.id = 0 self.multicast = multicast @@ -620,7 +640,7 @@ self.names = {} self.data = [] self.size = 12 - + self.questions = [] self.answers = [] self.authorities = [] @@ -660,7 +680,7 @@ 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' @@ -739,9 +759,9 @@ 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 + self.insertShort(index, length) # Here is the short we adjusted for def packet(self): """Returns a string containing the packet's bytes @@ -758,7 +778,7 @@ 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)) @@ -773,7 +793,7 @@ class DNSCache(object): """A cache of DNS entries""" - + def __init__(self): self.cache = {} @@ -781,7 +801,7 @@ """Adds an entry""" try: list = self.cache[entry.key] - except: + except Exception: list = self.cache[entry.key] = [] list.append(entry) @@ -790,7 +810,7 @@ try: list = self.cache[entry.key] list.remove(entry) - except: + except Exception: pass def get(self, entry): @@ -799,7 +819,7 @@ try: list = self.cache[entry.key] return list[list.index(entry)] - except: + except Exception: return None def getByDetails(self, name, type, clazz): @@ -812,7 +832,7 @@ """Returns a list of entries whose key matches the name.""" try: return self.cache[name] - except: + except Exception: return [] def entries(self): @@ -820,7 +840,7 @@ def add(x, y): return x+y try: return reduce(add, self.cache.values()) - except: + except Exception: return [] @@ -839,7 +859,7 @@ def __init__(self, zeroconf): threading.Thread.__init__(self) self.zeroconf = zeroconf - self.readers = {} # maps socket to reader + self.readers = {} # maps socket to reader self.timeout = 5 self.condition = threading.Condition() self.start() @@ -860,10 +880,10 @@ for socket in rr: try: self.readers[socket].handle_read() - except: + except Exception: # Ignore errors that occur on shutdown pass - except: + except Exception: pass def getReaders(self): @@ -872,7 +892,7 @@ result = self.readers.keys() self.condition.release() return result - + def addReader(self, reader, socket): self.condition.acquire() self.readers[socket] = reader @@ -890,6 +910,7 @@ self.condition.notify() self.condition.release() + class Listener(object): """A Listener is used by this module to listen on the multicast group to which DNS messages are sent, allowing the implementation @@ -897,7 +918,7 @@ 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) @@ -924,7 +945,7 @@ 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 @@ -948,7 +969,7 @@ 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) @@ -959,7 +980,7 @@ self.nextTime = currentTimeMillis() self.delay = _BROWSER_TIME self.list = [] - + self.done = 0 self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN)) @@ -976,14 +997,16 @@ if not expired: oldrecord.resetTTL(record) else: + def callback(x): + return self.listener.removeService(x, self.type, record.alias) del(self.services[record.alias.lower()]) - callback = lambda x: self.listener.removeService(x, self.type, record.alias) self.list.append(callback) return - except: + except Exception: if not expired: + def callback(x): + return self.listener.addService(x, self.type, record.alias) self.services[record.alias.lower()] = record - callback = lambda x: self.listener.addService(x, self.type, record.alias) self.list.append(callback) expires = record.getExpirationTime(75) @@ -1019,11 +1042,11 @@ 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. @@ -1089,7 +1112,7 @@ index += 1 strs.append(text[index:index+length]) index += length - + for s in strs: eindex = s.find('=') if eindex == -1: @@ -1105,14 +1128,14 @@ value = 0 # Only update non-existent properties - if key and result.get(key) == None: + if key and result.get(key) is None: result[key] = value self.properties = result - except: + except Exception: traceback.print_exc() self.properties = None - + def getType(self): """Type accessor""" return self.type @@ -1200,7 +1223,7 @@ result = 1 finally: zeroconf.removeListener(self) - + return result def __eq__(self, other): @@ -1225,7 +1248,7 @@ result += self.text[:17] + "..." result += "]" return result - + class Zeroconf(object): """Implementation of Zeroconf Multicast DNS Service Discovery @@ -1242,7 +1265,7 @@ try: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - except: + except Exception: # 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 @@ -1257,7 +1280,7 @@ self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) try: self.socket.bind(self.group) - except: + except Exception: # Some versions of linux raise an exception even though # the SO_REUSE* options have been set, so ignore it # @@ -1274,7 +1297,7 @@ self.cache = DNSCache() self.condition = threading.Condition() - + self.engine = Engine(self) self.listener = Listener(self) self.reaper = Reaper(self) @@ -1354,7 +1377,7 @@ """Unregister a service.""" try: del(self.services[info.name.lower()]) - except: + except Exception: pass now = currentTimeMillis() nextTime = now @@ -1439,7 +1462,7 @@ try: self.listeners.remove(listener) self.notifyAll() - except: + except Exception: pass def updateRecord(self, now, rec): @@ -1465,7 +1488,7 @@ record = entry else: self.cache.add(record) - + self.updateRecord(now, record) def handleQuery(self, msg, addr, port): @@ -1479,7 +1502,7 @@ out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0) for question in msg.questions: out.addQuestion(question) - + for question in msg.questions: if question.type == _TYPE_PTR: for service in self.services.values(): @@ -1491,36 +1514,37 @@ 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 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)) - except: + except Exception: traceback.print_exc() - + if out is not None and out.answers: out.id = msg.id self.send(out, addr, port) - def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT): + 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()) + # temp = DNSIncoming(out.packet()) try: bytes_sent = self.socket.sendto(out.packet(), 0, (addr, port)) - except: + except Exception: # Ignore this, it may be a temporary loss of network connection pass @@ -1534,15 +1558,16 @@ 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.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..." - desc = {'version':'0.10','a':'test value', 'b':'another value'} + 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)