From 792e993222587161b2637c41e2bc04dcdec020a9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Mar 2009 15:49:43 -0700 Subject: [PATCH] Advertise the calibre Stanza server on Bonjour. Now accessing your calibre EPUB collection in Stanza should be automatic on your home network. --- src/calibre/library/server.py | 17 +- src/calibre/manual/faq.rst | 8 +- src/calibre/utils/Zeroconf.py | 1574 +++++++++++++++++++++++++++++++++ src/calibre/utils/mdns.py | 90 ++ 4 files changed, 1678 insertions(+), 11 deletions(-) create mode 100755 src/calibre/utils/Zeroconf.py create mode 100644 src/calibre/utils/mdns.py diff --git a/src/calibre/library/server.py b/src/calibre/library/server.py index c2f7377102..930a644be8 100644 --- a/src/calibre/library/server.py +++ b/src/calibre/library/server.py @@ -23,6 +23,8 @@ from calibre.resources import jquery, server_resources, build_time from calibre.library import server_config as config from calibre.library.database2 import LibraryDatabase2, FIELD_MAP from calibre.utils.config import config_dir +from calibre.utils.mdns import publish as publish_zeroconf, \ + stop_server as stop_zeroconf build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S') server_resources['jquery.js'] = jquery @@ -171,17 +173,14 @@ class LibraryServer(object): try: cherrypy.engine.start() self.is_running = True - try: - subprocess.Popen(('dns-sd -R "calibre Books" ' - '_stanza._tcp local %d')% - self.opts.port, shell=True) - except: - pass + publish_zeroconf('Books in calibre', '_stanza._tcp', + self.opts.port, {'path':'/stanza'}) cherrypy.engine.block() except Exception, e: self.exception = e finally: self.is_running = False + stop_zeroconf() def exit(self): cherrypy.engine.exit() @@ -338,7 +337,11 @@ class LibraryServer(object): @expose def index(self, **kwargs): 'The / URL' - return self.static('index.html') + stanza = cherrypy.request.headers.get('Stanza-Device-Name', 919) + if stanza == 919: + return self.static('index.html') + return self.stanza() + @expose def get(self, what, id): diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 4e5eefe4c4..0e5d5fffc8 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -123,15 +123,15 @@ turned into a collection on the reader. Note that the PRS-500 does not support c How do I use |app| with my iPhone? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ First install the Stanza reader on your iPhone from http://www.lexcycle.com . Then, - * Set the output format for calibre to EPUB (this can be done in the configuration dialog accessed by the little hammer icon next to the search bar) + * Set the output format for calibre to EPUB (The output format can be set next to the big red heart) * Convert the books you want to read on your iPhone to EPUB format by selecting them and clicking the Convert button. - * Turn on the Content Server in the configurations dialog and leave |app| running. - * In the Stanza reader on your iPhone, add a new catalog. The URL of the catalog is of the form + * Turn on the Content Server in |app|'s preferences and leave |app| running. + * Now you should be able to access your books on your iPhone. If not, try the following: + In the Stanza reader on your iPhone, add a new catalog. The URL of the catalog is of the form ``http://10.34.56.89:8080/stanza``, where you should replace the IP address ``10.34.56.89`` with the IP address of your computer. Stanza will the use the |app| content server to access all the EPUB books in your |app| database. -A more detailed guide is available `here `_ Library Management ------------------ diff --git a/src/calibre/utils/Zeroconf.py b/src/calibre/utils/Zeroconf.py new file mode 100755 index 0000000000..610c353fca --- /dev/null +++ b/src/calibre/utils/Zeroconf.py @@ -0,0 +1,1574 @@ +''' 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.setDaemon(True) # By Kovid + 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: + traceback.print_exc() + 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.setDaemon(True) + 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: + if record.name == self.server: + 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.servicetypes = {} + + 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 + if self.servicetypes.has_key(info.type): + self.servicetypes[info.type]+=1 + else: + self.servicetypes[info.type]=1 + 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()]) + if self.servicetypes[info.type]>1: + self.servicetypes[info.type]-=1 + else: + del self.servicetypes[info.type] + 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: + if question.name == '_services._dns-sd._udp.local.': + for stype in self.servicetypes.keys(): + if out is None: + out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) + out.addAnswer(msg, DNSPointer('_services._dns-sd._udp.local.', _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype)) + 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/src/calibre/utils/mdns.py b/src/calibre/utils/mdns.py new file mode 100644 index 0000000000..022f9c1407 --- /dev/null +++ b/src/calibre/utils/mdns.py @@ -0,0 +1,90 @@ +from __future__ import with_statement +__license__ = 'GPL 3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import socket + +_server = None + +def get_external_ip(): + 'Get IP address of interface used to connect to the outside world' + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('google.com', 0)) + return s.getsockname()[0] + except: + return '127.0.0.1' + +def start_server(): + global _server + if _server is None: + from calibre.utils.Zeroconf import Zeroconf + _server = Zeroconf() + return _server + +def publish(desc, type, port, properties=None, add_hostname=True): + ''' + Publish a service. + + :param desc: Description of service + :param type: Name and type of service. For example _stanza._tcp + :param port: Port the service listens on + :param properties: An optional dictionary whose keys and values will be put + into the TXT record. + ''' + server = start_server() + hostname = socket.gethostname() + if add_hostname: + desc += ' (on %s)'%hostname + local_ip = get_external_ip() + type = type+'.local.' + from calibre.utils.Zeroconf import ServiceInfo + service = ServiceInfo(type, desc+'.'+type, + address=socket.inet_aton(local_ip), + port=port, + properties=properties, + server=hostname+'.local.') + server.registerService(service) + +def stop_server(): + global _server + if _server is not None: + _server.close() + +''' +class Publish(object): + + def __init__(self, desc, name, port, txt=''): + self.desc = desc + self.name = name + self.port = port + self.txt = txt + + def start(self): + if iswindows: + return + if isosx: + args = ['dns-sd', self.desc, self.name, '.', self.port] + else: + args = ['avahi-publish-service', self.desc, self.name, self.port] + if self.txt: + args.append(self.txt) + + self.process = subprocess.Popen(args) + + def stop(self): + if iswindows: + pass + else: + process = getattr(self, 'process', None) + if process is not None: + process.poll() + if process.returncode is not None: + process.terminate() + process.poll() + if process.returncode is not None: + process.kill() + +def publish(desc, name, port, txt): +''' \ No newline at end of file