diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..9415a30e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+MANIFEST
+dist
+build
+*.pyc
+*.pyo
+*.e4?
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/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 00000000..348c5377
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,5 @@
+include README
+include lgpl.txt
+include *.py
+recursive-include samplecode *.py
+recursive-include test *.py
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.py b/Zeroconf.py
deleted file mode 100755
index 8efaf033..00000000
--- a/Zeroconf.py
+++ /dev/null
@@ -1,1557 +0,0 @@
-""" 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"
-
-import string
-import time
-import struct
-import socket
-import threading
-import select
-import traceback
-
-__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
-
-# 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)
-
-
-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
-
- 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.
- #
- #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
-
- 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"""
- result = self.data[offset:offset+len].decode('utf-8')
- return result
-
- 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, 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
- sockets are ready.
-
- A reader needs a handle_read() method, which is called when the socket
- it is interested in is ready for reading.
-
- Writers are not implemented here, because we only send short
- packets.
- """
-
- def __init__(self, zeroconf):
- threading.Thread.__init__(self)
- self.zeroconf = zeroconf
- self.readers = {} # maps socket to reader
- self.timeout = 5
- self.condition = threading.Condition()
- self.start()
-
- def run(self):
- while not globals()['_GLOBAL_DONE']:
- rs = self.getReaders()
- if len(rs) == 0:
- # No sockets to manage, but we wait for the timeout
- # or addition of a socket
- #
- self.condition.acquire()
- self.condition.wait(self.timeout)
- self.condition.release()
- else:
- try:
- rr, wr, er = select.select(rs, [], [], self.timeout)
- for socket in rr:
- try:
- self.readers[socket].handle_read()
- except:
- # Ignore errors that occur on shutdown
- pass
- except:
- pass
-
- def getReaders(self):
- result = []
- self.condition.acquire()
- result = self.readers.keys()
- self.condition.release()
- return result
-
- def addReader(self, reader, socket):
- self.condition.acquire()
- self.readers[socket] = reader
- self.condition.notify()
- self.condition.release()
-
- def delReader(self, socket):
- self.condition.acquire()
- del(self.readers[socket])
- self.condition.notify()
- self.condition.release()
-
- def notify(self):
- self.condition.acquire()
- 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
- to cache information as it arrives.
-
- 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)
-
- def handle_read(self):
- data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE)
- self.data = data
- msg = DNSIncoming(data)
- if msg.isQuery():
- # Always multicast responses
- #
- if port == _MDNS_PORT:
- self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
- # If it's not a multicast query, reply via unicast
- # and multicast
- #
- elif port == _DNS_PORT:
- self.zeroconf.handleQuery(msg, addr, port)
- self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
- else:
- self.zeroconf.handleResponse(msg)
-
-
-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
- self.start()
-
- def run(self):
- while 1:
- self.zeroconf.wait(10 * 1000)
- if globals()['_GLOBAL_DONE']:
- return
- now = currentTimeMillis()
- for record in self.zeroconf.cache.entries():
- if record.isExpired(now):
- self.zeroconf.updateRecord(now, record)
- self.zeroconf.cache.remove(record)
-
-
-class ServiceBrowser(threading.Thread):
- """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"""
- threading.Thread.__init__(self)
- self.zeroconf = zeroconf
- self.type = type
- self.listener = listener
- self.services = {}
- self.nextTime = currentTimeMillis()
- self.delay = _BROWSER_TIME
- self.list = []
-
- self.done = 0
-
- self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _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:
- expired = record.isExpired(now)
- try:
- oldrecord = self.services[record.alias.lower()]
- if not expired:
- oldrecord.resetTTL(record)
- else:
- del(self.services[record.alias.lower()])
- callback = lambda x: self.listener.removeService(x, self.type, record.alias)
- self.list.append(callback)
- return
- except:
- if not expired:
- 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)
- if expires < self.nextTime:
- self.nextTime = expires
-
- def cancel(self):
- self.done = 1
- self.zeroconf.notifyAll()
-
- def run(self):
- while 1:
- event = None
- now = 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()
-
- if self.nextTime <= now:
- out = DNSOutgoing(_FLAGS_QR_QUERY)
- out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
- for record in self.services.values():
- if not record.isExpired(now):
- out.addAnswerAtTime(record, now)
- self.zeroconf.send(out)
- self.nextTime = now + self.delay
- self.delay = min(20 * 1000, self.delay * 2)
-
- if len(self.list) > 0:
- event = self.list.pop(0)
-
- 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
- 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:
- traceback.print_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
-
- Supports registration, unregistration, queries and browsing.
- """
- def __init__(self, bindaddress=None):
- """Creates an instance of the Zeroconf class, establishing
- multicast communications, listening and reaping threads."""
- globals()['_GLOBAL_DONE'] = 0
- if bindaddress is None:
- self.intf = socket.gethostbyname(socket.gethostname())
- else:
- self.intf = bindaddress
- self.group = ('', _MDNS_PORT)
- self.socket = 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)
- 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
- self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
- self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
- try:
- self.socket.bind(self.group)
- except:
- # 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)
-
- def isLoopback(self):
- return self.intf.startswith("127.0.0.1")
-
- def isLinklocal(self):
- return self.intf.startswith("169.254.")
-
- def wait(self, timeout):
- """Calling thread waits for a given number of milliseconds or
- until notified."""
- self.condition.acquire()
- self.condition.wait(timeout/1000)
- self.condition.release()
-
- def notifyAll(self):
- """Notifies all waiting threads"""
- self.condition.acquire()
- self.condition.notifyAll()
- self.condition.release()
-
- 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)
- if info.request(self, timeout):
- return info
- return None
-
- 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."""
- self.removeServiceListener(listener)
- self.browsers.append(ServiceBrowser(self, type, listener))
-
- def removeServiceListener(self, listener):
- """Removes a listener from the set that is currently listening."""
- for browser in self.browsers:
- if browser.listener == listener:
- browser.cancel()
- del(browser)
-
- def registerService(self, info, ttl=_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()
- nextTime = now
- i = 0
- while i < 3:
- if now < nextTime:
- self.wait(nextTime - now)
- now = 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)
- if info.address:
- out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, ttl, info.address), 0)
- self.send(out)
- i += 1
- nextTime += _REGISTER_TIME
-
- def unregisterService(self, info):
- """Unregister a service."""
- try:
- del(self.services[info.name.lower()])
- except:
- pass
- now = currentTimeMillis()
- nextTime = now
- i = 0
- while i < 3:
- if now < nextTime:
- self.wait(nextTime - now)
- now = 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)
- if info.address:
- out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
- self.send(out)
- i += 1
- nextTime += _UNREGISTER_TIME
-
- def unregisterAllServices(self):
- """Unregister all registered services."""
- if len(self.services) > 0:
- now = currentTimeMillis()
- nextTime = now
- i = 0
- while i < 3:
- if now < nextTime:
- self.wait(nextTime - now)
- now = currentTimeMillis()
- continue
- out = DNSOutgoing(_FLAGS_QR_RESPONSE | _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)
- if info.address:
- out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
- self.send(out)
- i += 1
- nextTime += _UNREGISTER_TIME
-
- def checkService(self, info):
- """Checks the network for a unique service name, modifying the
- ServiceInfo passed in if it is not unique."""
- now = 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 (info.name.find('.') < 0):
- info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type
- self.checkService(info)
- return
- raise NonUniqueNameException
- if now < nextTime:
- self.wait(nextTime - now)
- now = currentTimeMillis()
- continue
- out = DNSOutgoing(_FLAGS_QR_QUERY | _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))
- self.send(out)
- i += 1
- nextTime += _CHECK_TIME
-
- 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()
- self.listeners.append(listener)
- if question is not None:
- for record in self.cache.entriesWithName(question.name):
- if question.answeredBy(record) and not record.isExpired(now):
- listener.updateRecord(self, now, record)
- self.notifyAll()
-
- def removeListener(self, listener):
- """Removes a listener."""
- try:
- self.listeners.remove(listener)
- self.notifyAll()
- except:
- pass
-
- def updateRecord(self, now, rec):
- """Used to notify listeners of new information that has updated
- a record."""
- for listener in self.listeners:
- listener.updateRecord(self, now, rec)
- self.notifyAll()
-
- def handleResponse(self, msg):
- """Deal with incoming response packets. All answers
- are held in the cache, and listeners are notified."""
- now = currentTimeMillis()
- for record in msg.answers:
- expired = record.isExpired(now)
- if record in self.cache.entries():
- if expired:
- self.cache.remove(record)
- else:
- entry = self.cache.get(record)
- if entry is not None:
- entry.resetTTL(record)
- record = entry
- else:
- self.cache.add(record)
-
- self.updateRecord(now, record)
-
- def handleQuery(self, msg, addr, port):
- """Deal with incoming query packets. Provides a response if
- possible."""
- out = None
-
- # Support unicast client responses
- #
- if port != _MDNS_PORT:
- 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():
- 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))
- else:
- 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:
- 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()
-
- 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):
- """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))
- except:
- # Ignore this, it may be a temporary loss of network connection
- pass
-
- def close(self):
- """Ends the background threads, and prevent this instance from
- servicing further queries."""
- if globals()['_GLOBAL_DONE'] == 0:
- globals()['_GLOBAL_DONE'] = 1
- 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.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/samplecode/Browser.py b/samplecode/Browser.py
new file mode 100755
index 00000000..c5ced322
--- /dev/null
+++ b/samplecode/Browser.py
@@ -0,0 +1,48 @@
+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 = 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", repr(info.getText())
+ print "Properties are", info.getProperties()
+
+if __name__ == '__main__':
+ 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 (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()
diff --git a/samplecode/nameprobe.py b/samplecode/nameprobe.py
new file mode 100644
index 00000000..30b04be6
--- /dev/null
+++ b/samplecode/nameprobe.py
@@ -0,0 +1,38 @@
+#! /usr/bin/env python
+from zeroconf import mdns, mcastsocket, dns
+
+fake_type = '_test-server.local.'
+
+def main( base_name='coolserver.local.'):
+ z = mdns.Zeroconf( '0.0.0.0' )
+ try:
+ 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, sys
+ logging.basicConfig(
+ #level = logging.DEBUG
+ )
+ if sys.argv[1:]:
+ name = sys.argv[1]
+ else:
+ name = 'coolserver.local.'
+ main(name)
diff --git a/samplecode/testmdnssd.py b/samplecode/testmdnssd.py
new file mode 100755
index 00000000..e882a21b
--- /dev/null
+++ b/samplecode/testmdnssd.py
@@ -0,0 +1,38 @@
+#! /usr/bin/env python
+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.
+
+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:]) )
diff --git a/samplecode/testmulticast.py b/samplecode/testmulticast.py
new file mode 100755
index 00000000..2dc980f2
--- /dev/null
+++ b/samplecode/testmulticast.py
@@ -0,0 +1,39 @@
+#! /usr/bin/env python
+"""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 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,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, (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 )
+ if data == payload:
+ print 'Success: looped message received from address %s port %s'%(
+ addr,port,
+ )
+ return 0
+ print 'Failure: Looped message not received'
+ return 1
+ finally:
+ mcastsocket.leave_group( sock, dns._MDNS_ADDR )
+
+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:]) )
diff --git a/samplecode/testupnpigd.py b/samplecode/testupnpigd.py
new file mode 100755
index 00000000..8c8da681
--- /dev/null
+++ b/samplecode/testupnpigd.py
@@ -0,0 +1,81 @@
+#! /usr/bin/env python
+"""Trivial script to handle IGD port-opening...
+"""
+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
+
+query = """M-SEARCH * HTTP/1.1
+HOST: %(ip)s:%(port)s
+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: '%(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:]) )
diff --git a/setup.py b/setup.py
new file mode 100755
index 00000000..be6351d0
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,33 @@
+#! /usr/bin/env python
+"""Install Zeroconf.py using distutils"""
+try:
+ from setuptools import setup
+except ImportError, err:
+ from distutils.core import setup
+import os
+info = {}
+keys = [('__author__','author'),('__email__','author_email'),('__version__','version')]
+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("'")
+ keys.remove( (key,inf))
+ if not keys:
+ break
+if __name__ == "__main__":
+ setup(
+ name='pyzeroconf',
+ description='Python Zeroconf (mDNS) Library',
+ url='http://digitaltorque.ca',
+ packages=['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/ZeroconfTest.py b/test/ZeroconfTest.py
similarity index 78%
rename from ZeroconfTest.py
rename to test/ZeroconfTest.py
index 4a435bd9..b95de148 100755
--- a/ZeroconfTest.py
+++ b/test/ZeroconfTest.py
@@ -1,165 +1,181 @@
-""" 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
+from zeroconf import mdns
+import unittest,socket
+
+
+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())
+
+ 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):
+
+ def testLaunchAndClose(self):
+ rv = mdns.Zeroconf()
+ rv.close()
+
+if __name__ == '__main__':
+ import logging
+ logging.basicConfig( level=logging.DEBUG )
+ unittest.main()
diff --git a/ZeroconfTest2.py b/test/ZeroconfTest2.py
similarity index 95%
rename from ZeroconfTest2.py
rename to test/ZeroconfTest2.py
index 2071e3d6..7f760e1b 100755
--- a/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/__init__.py b/zeroconf/__init__.py
new file mode 100644
index 00000000..fd8227f2
--- /dev/null
+++ b/zeroconf/__init__.py
@@ -0,0 +1,92 @@
+""" 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.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)."""
+
+"""0.12 update - allow selection of binding interface
+ 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
+ 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.3"
+
+#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
new file mode 100644
index 00000000..6f3bb126
--- /dev/null
+++ b/zeroconf/dns.py
@@ -0,0 +1,1122 @@
+""" Multicast DNS Service Discovery for Python, v0.12
+ Copyright (C) 2003, Paul Scott-Murphy
+
+ 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
+ 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 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',
+ 'currentTimeMillis',
+]
+
+# Some timing constants
+
+_UNREGISTER_TIME = 125
+_CHECK_TIME = 175
+_REGISTER_TIME = 225
+_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'
+_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_NSEC = 47
+_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_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():
+ """Current system time in milliseconds"""
+ return time.time() * 1000
+
+# Exceptions
+
+class DNSError( Exception ):
+ """Base class for all DNS errors"""
+
+class DNSNameError( DNSError ):
+ """Name/type related errors"""
+class NonLocalNameException(DNSNameError):
+ pass
+class NonUniqueNameException(DNSNameError):
+ pass
+class NamePartTooLongException(DNSNameError):
+ pass
+class BadTypeInNameException(DNSNameError):
+ pass
+
+class AbstractMethodException(DNSError):
+ 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( 'No .local. suffix in %r'%(name,) )
+ 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()
+ 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"""
+ 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):
+ log.debug( 'Suppressing %s due to query record %s', self, 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( 'write' )
+
+ 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 self.toString(socket.inet_ntoa(self.address))
+ except:
+ try:
+ return self.toString(socket.inet_ntop(socket.AF_INET6, 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:
+ 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]))
+ 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))
+ 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
+ # unable to be parsed, and may show errors
+ # so this is left for debugging. New types
+ # encountered need to be parsed properly.
+ #
+ log.info(
+ "Unknown DNS query type: %s %r", info, self.data
+ )
+ if rec is not None:
+ self.answers.append(rec)
+ except Exception, 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"""
+ 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 DNSNameError( "Bad domain name (circular) at char %s", off )
+ first = off
+ else:
+ raise DNSNameError( "Bad domain name (unknown encoding type %r) at %s"%( t,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( utfstr )
+ 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 []
+
+ def clear( self ):
+ """Clear our cache of entries"""
+ self.cache.clear()
+
+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( '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
+ 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.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.lower() == self.name.lower():
+ 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.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
+
+ elif record.type == _TYPE_TXT:
+ if record.name.lower() == self.name.lower():
+ log.debug( 'Got TXT record for %s', record.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 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
+ self.address = None
+ self.ignore = ignore
+ if ignore:
+ self.ignore = [socket.inet_aton( a ) for a in ignore]
+ def request( self, zeroconf, timeout=_PROBE_TIMEOUT, delay=_PROBE_TIME ):
+ now = currentTimeMillis()
+ next = now + delay
+ last = now + timeout
+ result = 0
+ try:
+ 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.lower(), _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
+
+ 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
+ (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):
+ if getattr( record, 'address', None ):
+ self.address = record.address
+ else:
+ self.address = True
+ else:
+ log.debug(
+ """Ignoring own-response"""
+ )
+
diff --git a/zeroconf/mcastsocket.py b/zeroconf/mcastsocket.py
new file mode 100644
index 00000000..9999373b
--- /dev/null
+++ b/zeroconf/mcastsocket.py
@@ -0,0 +1,132 @@
+"""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).
+
+ 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__ )
+
+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
+
+ 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
+
+ returns socket.socket instance configured as specified
+ """
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ 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)
+ 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...
+ log.debug( 'Limiting multicast to use interface of %s', interface_ip )
+ sock.setsockopt(
+ socket.IPPROTO_IP, socket.IP_MULTICAST_IF,
+ socket.inet_aton( interface_ip) # + socket.inet_aton( '0.0.0.0' )
+ )
+ 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:
+ 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,
+ # 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"""
+ log.info( 'Joining multicast group: %s', group )
+ sock.setsockopt(
+ 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.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
new file mode 100644
index 00000000..75b587f9
--- /dev/null
+++ b/zeroconf/mdns.py
@@ -0,0 +1,713 @@
+""" 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 is the threaded mDNS responder/query-er implementation
+"""
+import string
+import time
+import struct
+import socket
+import errno
+import threading
+import select
+import traceback
+import logging
+log = logging.getLogger(__name__)
+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
+# Minimum time before sending duplicate message...
+_MINIMUM_REPEAT_TIME = 200
+
+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
+ sockets are ready.
+
+ A reader needs a handle_read() method, which is called when the socket
+ it is interested in is ready for reading.
+
+ Writers are not implemented here, because we only send short
+ packets.
+ """
+
+ def __init__(self, zeroconf):
+ threading.Thread.__init__(self)
+ self.zeroconf = zeroconf
+ self.readers = {} # maps socket to reader
+ self.timeout = 5
+ self.condition = threading.Condition()
+ self.start()
+
+ def run(self):
+ while not globals()['_GLOBAL_DONE']:
+ rs = self.getReaders()
+ if len(rs) == 0:
+ # 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/25.)
+ self.condition.release()
+ 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.info( 'Failure on select, ignoring: %s', err )
+ except Exception, 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.info( 'Error handling read: %s', err )
+ log.debug( 'Traceback: %s', traceback.format_exc())
+
+ def getReaders(self):
+ result = []
+ self.condition.acquire()
+ result = self.readers.keys()
+ self.condition.release()
+ return result
+
+ def addReader(self, reader, socket):
+ self.condition.acquire()
+ self.readers[socket] = reader
+ self.condition.notify()
+ self.condition.release()
+
+ def delReader(self, socket):
+ self.condition.acquire()
+ del(self.readers[socket])
+ self.condition.notify()
+ self.condition.release()
+
+ def notify(self):
+ self.condition.acquire()
+ 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
+ to cache information as it arrives.
+
+ 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)
+
+ def handle_read(self):
+ try:
+ data, (addr, port) = self.zeroconf.socket.recvfrom(dns._MAX_MSG_ABSOLUTE)
+ except Exception, 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
+ 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:
+ 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
+ have expired."""
+
+ def __init__(self, zeroconf):
+ threading.Thread.__init__(self)
+ self.zeroconf = zeroconf
+ self.daemon = True
+ self.start()
+
+ 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 = dns.currentTimeMillis()
+ for record in self.zeroconf.cache.entries():
+ if record.isExpired(now):
+ self.zeroconf.updateRecord(now, record)
+ self.zeroconf.cache.remove(record)
+
+
+class ServiceBrowser(threading.Thread):
+ """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_"""
+ threading.Thread.__init__(self)
+ self.zeroconf = zeroconf
+ self.type = type_
+ self.listener = listener
+ self.daemon = True
+ self.services = {}
+ self.nextTime = dns.currentTimeMillis()
+ self.delay = _BROWSER_TIME
+ self.list = []
+
+ self.done = 0
+
+ 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 == dns._TYPE_PTR and record.name == self.type:
+ expired = record.isExpired(now)
+ try:
+ oldrecord = self.services[record.alias.lower()]
+ if not expired:
+ oldrecord.resetTTL(record)
+ else:
+ del(self.services[record.alias.lower()])
+ callback = lambda x: self.listener.removeService(x, self.type, record.alias)
+ self.list.append(callback)
+ return
+ except:
+ if not expired:
+ 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)
+ if expires < self.nextTime:
+ self.nextTime = expires
+
+ def cancel(self):
+ self.done = 1
+ self.zeroconf.notifyAll()
+
+ def run(self):
+ while 1:
+ event = None
+ 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 = dns.currentTimeMillis()
+
+ if self.nextTime <= now:
+ 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)
+ self.zeroconf.send(out)
+ self.nextTime = now + self.delay
+ self.delay = min(20 * 1000, self.delay * 2)
+
+ if len(self.list) > 0:
+ event = self.list.pop(0)
+
+ if event is not None:
+ event(self.zeroconf)
+
+class Zeroconf(object):
+ """Implementation of Zeroconf Multicast DNS Service Discovery
+
+ Supports registration, unregistration, queries and browsing.
+ """
+ def __init__(self, bindaddress=None):
+ """Creates an instance of the Zeroconf class, establishing
+ multicast communications, listening and reaping threads."""
+ globals()['_GLOBAL_DONE'] = 0
+ if bindaddress is None:
+ 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
+ self.socket = mcastsocket.create_socket( (bindaddress, dns._MDNS_PORT) )
+ mcastsocket.join_group( self.socket, dns._MDNS_ADDR )
+
+ self.listeners = []
+ self.suppression_queue = []
+ self.browsers = []
+ self.services = {}
+
+ self.cache = dns.DNSCache()
+
+ self.condition = threading.Condition()
+
+ self.engine = Engine(self)
+ self.listener = Listener(self)
+ self.reaper = Reaper(self)
+
+ def isLoopback(self):
+ return self.intf.startswith("127.0.0.1")
+
+ def isLinklocal(self):
+ return self.intf.startswith("169.254.")
+
+ def wait(self, timeout):
+ """Calling thread waits for a given number of milliseconds or
+ until notified."""
+ self.condition.acquire()
+ self.condition.wait(timeout/1000)
+ self.condition.release()
+
+ def notifyAll(self):
+ """Notifies all waiting threads"""
+ self.condition.acquire()
+ self.condition.notifyAll()
+ self.condition.release()
+
+ 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
+ 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
+
+ TODO: maybe make this part of checkService instead? Same basic
+ operation, it's just looking for unique server name instead of unique
+ 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
+
+ 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.
+
+ 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
+ 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, ))
+ delay = dns._PROBE_TIME
+ for failures,name in enumerate(names(name)):
+ 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.info(
+ "Throttling host-name configuration to prevent network flood"
+ )
+ delay = dns._PROBE_THROTTLED_TIME
+ timeout = 2.5 * delay
+ if not address:
+ return name
+ 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, delay=delay ):
+ return info.address
+ return None
+
+ 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 = 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
+ will then have its updateRecord method called when information
+ arrives for that type_."""
+ self.removeServiceListener(listener)
+ self.browsers.append(ServiceBrowser(self, type_, listener))
+
+ def removeServiceListener(self, listener):
+ """Removes a listener from the set that is currently listening."""
+ for browser in self.browsers:
+ if browser.listener == listener:
+ browser.cancel()
+ del(browser)
+
+ 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 = dns.currentTimeMillis()
+ nextTime = now
+ i = 0
+ while i < 3:
+ if now < nextTime:
+ self.wait(nextTime - now)
+ now = dns.currentTimeMillis()
+ continue
+ 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."""
+ try:
+ del(self.services[info.name.lower()])
+ except:
+ pass
+ now = dns.currentTimeMillis()
+ nextTime = now
+ i = 0
+ while i < 3:
+ if now < nextTime:
+ 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, 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(dns.DNSAddress(info.server, dns._TYPE_A, dns._CLASS_IN, 0, info.address), 0)
+ self.send(out)
+ i += 1
+ nextTime += _UNREGISTER_TIME
+
+ def unregisterAllServices(self):
+ """Unregister all registered services."""
+ if len(self.services) > 0:
+ now = dns.currentTimeMillis()
+ nextTime = now
+ i = 0
+ while i < 3:
+ if now < nextTime:
+ self.wait(nextTime - now)
+ now = dns.currentTimeMillis()
+ continue
+ out = dns.DNSOutgoing(dns._FLAGS_QR_RESPONSE | dns._FLAGS_AA)
+ for info in self.services.values():
+ 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(dns.DNSAddress(info.server, dns._TYPE_A, dns._CLASS_IN, 0, info.address), 0)
+ self.send(out)
+ i += 1
+ nextTime += _UNREGISTER_TIME
+
+ def checkService(self, info):
+ """Checks the network for a unique service name, modifying the
+ ServiceInfo passed in if it is not unique."""
+ now = dns.currentTimeMillis()
+ nextTime = now
+ i = 0
+ while i < 3:
+ for record in self.cache.entriesWithName(info.type):
+ 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)
+ return
+ raise dns.NonUniqueNameException( info.name )
+ if now < nextTime:
+ self.wait(nextTime - now)
+ now = dns.currentTimeMillis()
+ continue
+ out = dns.DNSOutgoing(dns._FLAGS_QR_QUERY | dns._FLAGS_AA)
+ self.debug = out
+ 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
+
+ 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 = dns.currentTimeMillis()
+ self.listeners.append(listener)
+ if question is not None:
+ for record in self.cache.entriesWithName(question.name):
+ if question.answeredBy(record) and not record.isExpired(now):
+ listener.updateRecord(self, now, record)
+ self.notifyAll()
+
+ def removeListener(self, listener):
+ """Removes a listener."""
+ try:
+ self.listeners.remove(listener)
+ self.notifyAll()
+ except:
+ pass
+
+ def updateRecord(self, now, rec):
+ """Used to notify listeners of new information that has updated
+ a record."""
+ for listener in self.listeners:
+ listener.updateRecord(self, now, rec)
+ self.notifyAll()
+
+ def handleResponse(self, msg):
+ """Deal with incoming response packets. All answers
+ are held in the cache, and listeners are notified."""
+ now = dns.currentTimeMillis()
+ for record in msg.answers:
+ expired = record.isExpired(now)
+ if record in self.cache.entries():
+ if expired:
+ self.cache.remove(record)
+ else:
+ entry = self.cache.get(record)
+ if entry is not None:
+ entry.resetTTL(record)
+ record = entry
+ else:
+ self.cache.add(record)
+
+ self.updateRecord(now, record)
+
+ def handleQuery(self, msg, addr, port):
+ """Deal with incoming query packets. Provides a response if
+ possible."""
+ out = None
+
+ # Support unicast client responses
+ #
+ 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 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.debug( 'Sent response: %s', out.answers )
+ else:
+ 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.debug( 'Question: %s', question )
+ for service in self.services.values():
+ if question.type == dns._TYPE_PTR:
+ 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
+ # 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, ):
+ 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.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.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.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.
+
+ 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.debug( '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
+ servicing further queries."""
+ if globals()['_GLOBAL_DONE'] == 0:
+ globals()['_GLOBAL_DONE'] = 1
+ self.notifyAll()
+ self.engine.notify()
+ self.unregisterAllServices()
+ mcastsocket.leave_group( self.socket, dns._MDNS_ADDR )
+ self.socket.close()
+