Zeroconf.py
1888 lines
| 60.2 KiB
| text/x-python
|
PythonLexer
timeless
|
r28296 | from __future__ import absolute_import, print_function | ||
timeless
|
r28295 | |||
Matt Mackall
|
r7071 | """ 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 <a href="http://strangeberry.com">StrangeBerry</a>, | ||||
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 | ||||
Martin Geisler
|
r15782 | License along with this library; if not, see | ||
<http://www.gnu.org/licenses/>. | ||||
Peter Arrenbrecht
|
r7877 | |||
Matt Mackall
|
r7071 | """ | ||
"""0.12 update - allow selection of binding interface | ||||
timeless
|
r28298 | typo fix - Thanks A. M. Kuchlingi | ||
removed all use of word 'Rendezvous' - this is an API change""" | ||||
Matt Mackall
|
r7071 | |||
"""0.11 update - correction to comments for addListener method | ||||
support for new record types seen from OS X | ||||
timeless
|
r28298 | - IPv6 address | ||
- hostinfo | ||||
ignore unknown DNS record types | ||||
fixes to name decoding | ||||
timeless
|
r28299 | works alongside other processes using port 5353 (e.g. Mac OS X) | ||
timeless
|
r28298 | tested against Mac OS X 10.3.2's mDNSResponder | ||
corrections to removal of list entries for service browser""" | ||||
Matt Mackall
|
r7071 | |||
"""0.10 update - Jonathon Paisley contributed these corrections: | ||||
always multicast replies, even when query is unicast | ||||
timeless
|
r28298 | 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 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | modified some unit tests to accommodate these changes""" | ||
Matt Mackall
|
r7071 | |||
"""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 | ||||
timeless
|
r28298 | ServiceBrowser now works | ||
new unit tests""" | ||||
Matt Mackall
|
r7071 | |||
"""0.06 update - small improvements with unit tests | ||||
added defined exception types | ||||
timeless
|
r28298 | 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""" | ||||
Matt Mackall
|
r7071 | |||
Mads Kiilerich
|
r17424 | """0.05 update - ensure case insensitivity on domain names | ||
Matt Mackall
|
r7071 | support for unicast DNS queries""" | ||
"""0.04 update - added some unit tests | ||||
added __ne__ adjuncts where required | ||||
timeless
|
r28298 | ensure names end in '.local.' | ||
timeout on receiving socket for clean shutdown""" | ||||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43347 | __author__ = b"Paul Scott-Murphy" | ||
__email__ = b"paul at scott dash murphy dot com" | ||||
__version__ = b"0.12" | ||||
Matt Mackall
|
r7071 | |||
Jun Wu
|
r34448 | import errno | ||
timeless
|
r28422 | import itertools | ||
timeless
|
r28296 | import select | ||
import socket | ||||
Matt Mackall
|
r7071 | import struct | ||
import threading | ||||
timeless
|
r28296 | import time | ||
Matt Mackall
|
r7071 | import traceback | ||
Pulkit Goyal
|
r42754 | from mercurial import pycompat | ||
Augie Fackler
|
r43347 | __all__ = [b"Zeroconf", b"ServiceInfo", b"ServiceBrowser"] | ||
Matt Mackall
|
r7071 | |||
# hook for threads | ||||
Augie Fackler
|
r43347 | globals()[b'_GLOBAL_DONE'] = 0 | ||
Matt Mackall
|
r7071 | |||
# Some timing constants | ||||
_UNREGISTER_TIME = 125 | ||||
_CHECK_TIME = 175 | ||||
_REGISTER_TIME = 225 | ||||
_LISTENER_TIME = 200 | ||||
_BROWSER_TIME = 500 | ||||
# Some DNS constants | ||||
Peter Arrenbrecht
|
r7877 | |||
Gregory Szorc
|
r41660 | _MDNS_ADDR = r'224.0.0.251' | ||
timeless
|
r28297 | _MDNS_PORT = 5353 | ||
_DNS_PORT = 53 | ||||
Augie Fackler
|
r43346 | _DNS_TTL = 60 * 60 # one hour default TTL | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | _MAX_MSG_TYPICAL = 1460 # unused | ||
Matt Mackall
|
r7071 | _MAX_MSG_ABSOLUTE = 8972 | ||
Augie Fackler
|
r43346 | _FLAGS_QR_MASK = 0x8000 # query response mask | ||
_FLAGS_QR_QUERY = 0x0000 # query | ||||
_FLAGS_QR_RESPONSE = 0x8000 # response | ||||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | _FLAGS_AA = 0x0400 # Authoritative answer | ||
_FLAGS_TC = 0x0200 # Truncated | ||||
_FLAGS_RD = 0x0100 # Recursion desired | ||||
_FLAGS_RA = 0x8000 # Recursion available | ||||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | _FLAGS_Z = 0x0040 # Zero | ||
_FLAGS_AD = 0x0020 # Authentic data | ||||
_FLAGS_CD = 0x0010 # Checking disabled | ||||
Matt Mackall
|
r7071 | |||
_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 | ||||
timeless
|
r27637 | _TYPE_ANY = 255 | ||
Matt Mackall
|
r7071 | |||
# Mapping constants to names | ||||
Augie Fackler
|
r43346 | _CLASSES = { | ||
Augie Fackler
|
r43347 | _CLASS_IN: b"in", | ||
_CLASS_CS: b"cs", | ||||
_CLASS_CH: b"ch", | ||||
_CLASS_HS: b"hs", | ||||
_CLASS_NONE: b"none", | ||||
_CLASS_ANY: b"any", | ||||
Augie Fackler
|
r43346 | } | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | _TYPES = { | ||
Augie Fackler
|
r43347 | _TYPE_A: b"a", | ||
_TYPE_NS: b"ns", | ||||
_TYPE_MD: b"md", | ||||
_TYPE_MF: b"mf", | ||||
_TYPE_CNAME: b"cname", | ||||
_TYPE_SOA: b"soa", | ||||
_TYPE_MB: b"mb", | ||||
_TYPE_MG: b"mg", | ||||
_TYPE_MR: b"mr", | ||||
_TYPE_NULL: b"null", | ||||
_TYPE_WKS: b"wks", | ||||
_TYPE_PTR: b"ptr", | ||||
_TYPE_HINFO: b"hinfo", | ||||
_TYPE_MINFO: b"minfo", | ||||
_TYPE_MX: b"mx", | ||||
_TYPE_TXT: b"txt", | ||||
_TYPE_AAAA: b"quada", | ||||
_TYPE_SRV: b"srv", | ||||
_TYPE_ANY: b"any", | ||||
Augie Fackler
|
r43346 | } | ||
Matt Mackall
|
r7071 | |||
# utility functions | ||||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | def currentTimeMillis(): | ||
timeless
|
r28298 | """Current system time in milliseconds""" | ||
return time.time() * 1000 | ||||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | # Exceptions | ||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class NonLocalNameException(Exception): | ||
timeless
|
r28298 | pass | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class NonUniqueNameException(Exception): | ||
timeless
|
r28298 | pass | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class NamePartTooLongException(Exception): | ||
timeless
|
r28298 | pass | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class AbstractMethodException(Exception): | ||
timeless
|
r28298 | pass | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class BadTypeInNameException(Exception): | ||
timeless
|
r28298 | pass | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Javi Merino
|
r11435 | class BadDomainName(Exception): | ||
timeless
|
r28298 | def __init__(self, pos): | ||
Augie Fackler
|
r43347 | Exception.__init__(self, b"at position %s" % pos) | ||
Javi Merino
|
r11435 | |||
Augie Fackler
|
r43346 | |||
Javi Merino
|
r11435 | class BadDomainNameCircular(BadDomainName): | ||
timeless
|
r28298 | pass | ||
Javi Merino
|
r11435 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | # implementation classes | ||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class DNSEntry(object): | ||
timeless
|
r28298 | """A DNS entry""" | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def __init__(self, name, type, clazz): | ||
Gregory Szorc
|
r41660 | self.key = name.lower() | ||
timeless
|
r28298 | self.name = name | ||
self.type = type | ||||
self.clazz = clazz & _CLASS_MASK | ||||
self.unique = (clazz & _CLASS_UNIQUE) != 0 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __eq__(self, other): | ||
"""Equality test on name, type, and class""" | ||||
if isinstance(other, DNSEntry): | ||||
Augie Fackler
|
r43346 | return ( | ||
self.name == other.name | ||||
and self.type == other.type | ||||
and self.clazz == other.clazz | ||||
) | ||||
timeless
|
r28298 | return 0 | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __ne__(self, other): | ||
"""Non-equality test""" | ||||
return not self.__eq__(other) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def getClazz(self, clazz): | ||
"""Class accessor""" | ||||
try: | ||||
return _CLASSES[clazz] | ||||
except KeyError: | ||||
Augie Fackler
|
r43347 | return b"?(%s)" % clazz | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def getType(self, type): | ||
"""Type accessor""" | ||||
try: | ||||
return _TYPES[type] | ||||
except KeyError: | ||||
Augie Fackler
|
r43347 | return b"?(%s)" % type | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def toString(self, hdr, other): | ||
"""String representation with additional information""" | ||||
Augie Fackler
|
r43347 | result = b"%s[%s,%s" % ( | ||
Augie Fackler
|
r43346 | hdr, | ||
self.getType(self.type), | ||||
self.getClazz(self.clazz), | ||||
) | ||||
timeless
|
r28298 | if self.unique: | ||
Augie Fackler
|
r43347 | result += b"-unique," | ||
timeless
|
r28298 | else: | ||
Augie Fackler
|
r43347 | result += b"," | ||
timeless
|
r28298 | result += self.name | ||
if other is not None: | ||||
Augie Fackler
|
r43347 | result += b",%s]" % other | ||
timeless
|
r28298 | else: | ||
Augie Fackler
|
r43347 | result += b"]" | ||
timeless
|
r28298 | return result | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class DNSQuestion(DNSEntry): | ||
timeless
|
r28298 | """A DNS question entry""" | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def __init__(self, name, type, clazz): | ||
Pulkit Goyal
|
r42754 | if pycompat.ispy3 and isinstance(name, str): | ||
name = name.encode('ascii') | ||||
Augie Fackler
|
r43347 | if not name.endswith(b".local."): | ||
timeless
|
r28298 | raise NonLocalNameException(name) | ||
DNSEntry.__init__(self, name, type, clazz) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def answeredBy(self, rec): | ||
"""Returns true if the question is answered by the record""" | ||||
Augie Fackler
|
r43346 | return ( | ||
self.clazz == rec.clazz | ||||
and (self.type == rec.type or self.type == _TYPE_ANY) | ||||
and self.name == rec.name | ||||
) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
Augie Fackler
|
r43347 | return DNSEntry.toString(self, b"question", None) | ||
Matt Mackall
|
r7071 | |||
class DNSRecord(DNSEntry): | ||||
timeless
|
r28298 | """A DNS record - like a DNS entry, but has a TTL""" | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def __init__(self, name, type, clazz, ttl): | ||
DNSEntry.__init__(self, name, type, clazz) | ||||
self.ttl = ttl | ||||
self.created = currentTimeMillis() | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __eq__(self, other): | ||
"""Tests equality as per DNSRecord""" | ||||
if isinstance(other, DNSRecord): | ||||
return DNSEntry.__eq__(self, other) | ||||
return 0 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def getRemainingTTL(self, now): | ||
"""Returns the remaining TTL in seconds.""" | ||||
return max(0, (self.getExpirationTime(100) - now) / 1000) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def isExpired(self, now): | ||
"""Returns true if this record has expired.""" | ||||
return self.getExpirationTime(100) <= now | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def isStale(self, now): | ||
"""Returns true if this record is at least half way expired.""" | ||||
return self.getExpirationTime(50) <= now | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def write(self, out): | ||
"""Abstract method""" | ||||
raise AbstractMethodException | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def toString(self, other): | ||
"""String representation with additional information""" | ||||
Augie Fackler
|
r43347 | arg = b"%s/%s,%s" % ( | ||
Augie Fackler
|
r43346 | self.ttl, | ||
self.getRemainingTTL(currentTimeMillis()), | ||||
other, | ||||
) | ||||
Augie Fackler
|
r43347 | return DNSEntry.toString(self, b"record", arg) | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class DNSAddress(DNSRecord): | ||
timeless
|
r28298 | """A DNS address record""" | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def __init__(self, name, type, clazz, ttl, address): | ||
DNSRecord.__init__(self, name, type, clazz, ttl) | ||||
self.address = address | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def write(self, out): | ||
"""Used in constructing an outgoing packet""" | ||||
out.writeString(self.address, len(self.address)) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __eq__(self, other): | ||
"""Tests equality on address""" | ||||
if isinstance(other, DNSAddress): | ||||
return self.address == other.address | ||||
return 0 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
try: | ||||
return socket.inet_ntoa(self.address) | ||||
except Exception: | ||||
return self.address | ||||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class DNSHinfo(DNSRecord): | ||
timeless
|
r28298 | """A DNS host information record""" | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __init__(self, name, type, clazz, ttl, cpu, os): | ||
DNSRecord.__init__(self, name, type, clazz, ttl) | ||||
self.cpu = cpu | ||||
self.os = os | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def write(self, out): | ||
"""Used in constructing an outgoing packet""" | ||||
out.writeString(self.cpu, len(self.cpu)) | ||||
out.writeString(self.os, len(self.os)) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
Augie Fackler
|
r43347 | return self.cpu + b" " + self.os | ||
Peter Arrenbrecht
|
r7877 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class DNSPointer(DNSRecord): | ||
timeless
|
r28298 | """A DNS pointer record""" | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def __init__(self, name, type, clazz, ttl, alias): | ||
DNSRecord.__init__(self, name, type, clazz, ttl) | ||||
self.alias = alias | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def write(self, out): | ||
"""Used in constructing an outgoing packet""" | ||||
out.writeName(self.alias) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __eq__(self, other): | ||
"""Tests equality on alias""" | ||||
if isinstance(other, DNSPointer): | ||||
return self.alias == other.alias | ||||
return 0 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
return self.toString(self.alias) | ||||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class DNSText(DNSRecord): | ||
timeless
|
r28298 | """A DNS text record""" | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def __init__(self, name, type, clazz, ttl, text): | ||
DNSRecord.__init__(self, name, type, clazz, ttl) | ||||
self.text = text | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def write(self, out): | ||
"""Used in constructing an outgoing packet""" | ||||
out.writeString(self.text, len(self.text)) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __eq__(self, other): | ||
"""Tests equality on text""" | ||||
if isinstance(other, DNSText): | ||||
return self.text == other.text | ||||
return 0 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
if len(self.text) > 10: | ||||
Augie Fackler
|
r43347 | return self.toString(self.text[:7] + b"...") | ||
timeless
|
r28298 | else: | ||
return self.toString(self.text) | ||||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class DNSService(DNSRecord): | ||
timeless
|
r28298 | """A DNS service record""" | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | 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 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __eq__(self, other): | ||
"""Tests equality on priority, weight, port and server""" | ||||
if isinstance(other, DNSService): | ||||
Augie Fackler
|
r43346 | return ( | ||
self.priority == other.priority | ||||
and self.weight == other.weight | ||||
and self.port == other.port | ||||
and self.server == other.server | ||||
) | ||||
timeless
|
r28298 | return 0 | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
Augie Fackler
|
r43347 | return self.toString(b"%s:%s" % (self.server, self.port)) | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class DNSIncoming(object): | ||
timeless
|
r28298 | """Object representation of an incoming DNS packet""" | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def __init__(self, data): | ||
"""Constructor from string holding bytes of packet""" | ||||
self.offset = 0 | ||||
self.data = data | ||||
self.questions = [] | ||||
self.answers = [] | ||||
timeless
|
r28419 | self.numquestions = 0 | ||
self.numanswers = 0 | ||||
self.numauthorities = 0 | ||||
self.numadditionals = 0 | ||||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | self.readHeader() | ||
self.readQuestions() | ||||
self.readOthers() | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def readHeader(self): | ||
"""Reads header portion of packet""" | ||||
Augie Fackler
|
r43347 | format = b'!HHHHHH' | ||
timeless
|
r28298 | length = struct.calcsize(format) | ||
Augie Fackler
|
r43346 | info = struct.unpack( | ||
format, self.data[self.offset : self.offset + length] | ||||
) | ||||
timeless
|
r28298 | self.offset += length | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | self.id = info[0] | ||
self.flags = info[1] | ||||
timeless
|
r28419 | self.numquestions = info[2] | ||
self.numanswers = info[3] | ||||
self.numauthorities = info[4] | ||||
self.numadditionals = info[5] | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def readQuestions(self): | ||
"""Reads questions section of packet""" | ||||
Augie Fackler
|
r43347 | format = b'!HH' | ||
timeless
|
r28298 | length = struct.calcsize(format) | ||
timeless
|
r28419 | for i in range(0, self.numquestions): | ||
timeless
|
r28298 | name = self.readName() | ||
Augie Fackler
|
r43346 | info = struct.unpack( | ||
format, self.data[self.offset : self.offset + length] | ||||
) | ||||
timeless
|
r28298 | self.offset += length | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | try: | ||
question = DNSQuestion(name, info[0], info[1]) | ||||
self.questions.append(question) | ||||
except NonLocalNameException: | ||||
pass | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def readInt(self): | ||
"""Reads an integer from the packet""" | ||||
Augie Fackler
|
r43347 | format = b'!I' | ||
timeless
|
r28298 | length = struct.calcsize(format) | ||
Augie Fackler
|
r43346 | info = struct.unpack( | ||
format, self.data[self.offset : self.offset + length] | ||||
) | ||||
timeless
|
r28298 | self.offset += length | ||
return info[0] | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def readCharacterString(self): | ||
"""Reads a character string from the packet""" | ||||
length = ord(self.data[self.offset]) | ||||
self.offset += 1 | ||||
return self.readString(length) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def readString(self, len): | ||
"""Reads a string of a given length from the packet""" | ||||
Augie Fackler
|
r43347 | format = b'!%ds' % len | ||
timeless
|
r28298 | length = struct.calcsize(format) | ||
Augie Fackler
|
r43346 | info = struct.unpack( | ||
format, self.data[self.offset : self.offset + length] | ||||
) | ||||
timeless
|
r28298 | self.offset += length | ||
return info[0] | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def readUnsignedShort(self): | ||
"""Reads an unsigned short from the packet""" | ||||
Augie Fackler
|
r43347 | format = b'!H' | ||
timeless
|
r28298 | length = struct.calcsize(format) | ||
Augie Fackler
|
r43346 | info = struct.unpack( | ||
format, self.data[self.offset : self.offset + length] | ||||
) | ||||
timeless
|
r28298 | self.offset += length | ||
return info[0] | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def readOthers(self): | ||
timeless
|
r28299 | """Reads answers, authorities and additionals section of the packet""" | ||
Augie Fackler
|
r43347 | format = b'!HHiH' | ||
timeless
|
r28298 | length = struct.calcsize(format) | ||
Martin von Zweigbergk
|
r28504 | n = self.numanswers + self.numauthorities + self.numadditionals | ||
timeless
|
r28298 | for i in range(0, n): | ||
domain = self.readName() | ||||
Augie Fackler
|
r43346 | info = struct.unpack( | ||
format, self.data[self.offset : self.offset + length] | ||||
) | ||||
timeless
|
r28298 | self.offset += length | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | rec = None | ||
if info[0] == _TYPE_A: | ||||
Augie Fackler
|
r43346 | rec = DNSAddress( | ||
domain, info[0], info[1], info[2], self.readString(4) | ||||
) | ||||
timeless
|
r28298 | elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR: | ||
Augie Fackler
|
r43346 | rec = DNSPointer( | ||
domain, info[0], info[1], info[2], self.readName() | ||||
) | ||||
timeless
|
r28298 | elif info[0] == _TYPE_TXT: | ||
Augie Fackler
|
r43346 | rec = DNSText( | ||
domain, info[0], info[1], info[2], self.readString(info[3]) | ||||
) | ||||
timeless
|
r28298 | elif info[0] == _TYPE_SRV: | ||
Augie Fackler
|
r43346 | rec = DNSService( | ||
domain, | ||||
info[0], | ||||
info[1], | ||||
info[2], | ||||
self.readUnsignedShort(), | ||||
self.readUnsignedShort(), | ||||
self.readUnsignedShort(), | ||||
self.readName(), | ||||
) | ||||
timeless
|
r28298 | elif info[0] == _TYPE_HINFO: | ||
Augie Fackler
|
r43346 | rec = DNSHinfo( | ||
domain, | ||||
info[0], | ||||
info[1], | ||||
info[2], | ||||
self.readCharacterString(), | ||||
self.readCharacterString(), | ||||
) | ||||
timeless
|
r28298 | elif info[0] == _TYPE_AAAA: | ||
Augie Fackler
|
r43346 | rec = DNSAddress( | ||
domain, info[0], info[1], info[2], self.readString(16) | ||||
) | ||||
timeless
|
r28298 | 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. | ||||
# | ||||
Augie Fackler
|
r43346 | # print "UNKNOWN TYPE = " + str(info[0]) | ||
# raise BadTypeInNameException | ||||
timeless
|
r28298 | self.offset += info[3] | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | if rec is not None: | ||
self.answers.append(rec) | ||||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def isQuery(self): | ||
"""Returns true if this is a query""" | ||||
return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def isResponse(self): | ||
"""Returns true if this is a response""" | ||||
return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def readUTF(self, offset, len): | ||
"""Reads a UTF-8 string of a given length from the packet""" | ||||
Augie Fackler
|
r43346 | return self.data[offset : offset + len].decode('utf-8') | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def readName(self): | ||
"""Reads a domain name from the packet""" | ||||
Pulkit Goyal
|
r42753 | result = r'' | ||
timeless
|
r28298 | off = self.offset | ||
next = -1 | ||||
first = off | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | while True: | ||
Augie Fackler
|
r43346 | len = ord(self.data[off : off + 1]) | ||
timeless
|
r28298 | off += 1 | ||
if len == 0: | ||||
break | ||||
t = len & 0xC0 | ||||
if t == 0x00: | ||||
Pulkit Goyal
|
r42753 | result = r''.join((result, self.readUTF(off, len) + r'.')) | ||
timeless
|
r28298 | off += len | ||
elif t == 0xC0: | ||||
if next < 0: | ||||
next = off + 1 | ||||
Augie Fackler
|
r43346 | off = ((len & 0x3F) << 8) | ord(self.data[off : off + 1]) | ||
timeless
|
r28298 | if off >= first: | ||
raise BadDomainNameCircular(off) | ||||
first = off | ||||
else: | ||||
raise BadDomainName(off) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | if next >= 0: | ||
self.offset = next | ||||
else: | ||||
self.offset = off | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | return result | ||
Peter Arrenbrecht
|
r7877 | |||
Matt Mackall
|
r7071 | class DNSOutgoing(object): | ||
timeless
|
r28298 | """Object representation of an outgoing packet""" | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28302 | def __init__(self, flags, multicast=1): | ||
timeless
|
r28298 | self.finished = 0 | ||
self.id = 0 | ||||
self.multicast = multicast | ||||
self.flags = flags | ||||
self.names = {} | ||||
self.data = [] | ||||
self.size = 12 | ||||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | self.questions = [] | ||
self.answers = [] | ||||
self.authorities = [] | ||||
self.additionals = [] | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def addQuestion(self, record): | ||
"""Adds a question""" | ||||
self.questions.append(record) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def addAnswer(self, inp, record): | ||
"""Adds an answer""" | ||||
if not record.suppressedBy(inp): | ||||
self.addAnswerAtTime(record, 0) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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)) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def addAuthoritativeAnswer(self, record): | ||
"""Adds an authoritative answer""" | ||||
self.authorities.append(record) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def addAdditionalAnswer(self, record): | ||
"""Adds an additional answer""" | ||||
self.additionals.append(record) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def writeByte(self, value): | ||
"""Writes a single byte to the packet""" | ||||
Augie Fackler
|
r43347 | format = b'!c' | ||
timeless
|
r28298 | self.data.append(struct.pack(format, chr(value))) | ||
self.size += 1 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def insertShort(self, index, value): | ||
"""Inserts an unsigned short in a certain position in the packet""" | ||||
Augie Fackler
|
r43347 | format = b'!H' | ||
timeless
|
r28298 | self.data.insert(index, struct.pack(format, value)) | ||
self.size += 2 | ||||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def writeShort(self, value): | ||
"""Writes an unsigned short to the packet""" | ||||
Augie Fackler
|
r43347 | format = b'!H' | ||
timeless
|
r28298 | self.data.append(struct.pack(format, value)) | ||
self.size += 2 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def writeInt(self, value): | ||
"""Writes an unsigned integer to the packet""" | ||||
Augie Fackler
|
r43347 | format = b'!I' | ||
timeless
|
r28298 | self.data.append(struct.pack(format, int(value))) | ||
self.size += 4 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def writeString(self, value, length): | ||
"""Writes a string to the packet""" | ||||
Augie Fackler
|
r43347 | format = b'!' + str(length) + b's' | ||
timeless
|
r28298 | self.data.append(struct.pack(format, value)) | ||
self.size += length | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def writeName(self, name): | ||
"""Writes a domain name to the packet""" | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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 | ||||
Augie Fackler
|
r43347 | parts = name.split(b'.') | ||
if parts[-1] == b'': | ||||
timeless
|
r28298 | parts = parts[:-1] | ||
for part in parts: | ||||
self.writeUTF(part) | ||||
self.writeByte(0) | ||||
return | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | # An index was found, so write a pointer to it | ||
# | ||||
self.writeByte((index >> 8) | 0xC0) | ||||
self.writeByte(index) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def writeQuestion(self, question): | ||
"""Writes a question to the packet""" | ||||
self.writeName(question.name) | ||||
self.writeShort(question.type) | ||||
self.writeShort(question.clazz) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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 | ||||
Peter Arrenbrecht
|
r7877 | |||
Augie Fackler
|
r43347 | length = len(b''.join(self.data[index:])) | ||
Augie Fackler
|
r43346 | self.insertShort(index, length) # Here is the short we adjusted for | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def packet(self): | ||
"""Returns a string containing the packet's bytes | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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) | ||||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | 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) | ||||
Augie Fackler
|
r43347 | return b''.join(self.data) | ||
Matt Mackall
|
r7071 | |||
class DNSCache(object): | ||||
timeless
|
r28298 | """A cache of DNS entries""" | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def __init__(self): | ||
self.cache = {} | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def add(self, entry): | ||
"""Adds an entry""" | ||||
try: | ||||
list = self.cache[entry.key] | ||||
except KeyError: | ||||
list = self.cache[entry.key] = [] | ||||
list.append(entry) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def remove(self, entry): | ||
"""Removes an entry""" | ||||
try: | ||||
list = self.cache[entry.key] | ||||
list.remove(entry) | ||||
except KeyError: | ||||
pass | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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 (KeyError, ValueError): | ||||
return None | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def entriesWithName(self, name): | ||
"""Returns a list of entries whose key matches the name.""" | ||||
try: | ||||
return self.cache[name] | ||||
except KeyError: | ||||
return [] | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def entries(self): | ||
"""Returns a list of all entries""" | ||||
try: | ||||
timeless
|
r28422 | return list(itertools.chain.from_iterable(self.cache.values())) | ||
timeless
|
r28298 | except Exception: | ||
return [] | ||||
Matt Mackall
|
r7071 | |||
class Engine(threading.Thread): | ||||
timeless
|
r28298 | """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. | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | A reader needs a handle_read() method, which is called when the socket | ||
it is interested in is ready for reading. | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | Writers are not implemented here, because we only send short | ||
packets. | ||||
""" | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __init__(self, zeroconf): | ||
threading.Thread.__init__(self) | ||||
self.zeroconf = zeroconf | ||||
Augie Fackler
|
r43346 | self.readers = {} # maps socket to reader | ||
timeless
|
r28298 | self.timeout = 5 | ||
self.condition = threading.Condition() | ||||
self.start() | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def run(self): | ||
Augie Fackler
|
r43347 | while not globals()[b'_GLOBAL_DONE']: | ||
timeless
|
r28298 | 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 sock in rr: | ||||
try: | ||||
self.readers[sock].handle_read() | ||||
except Exception: | ||||
Augie Fackler
|
r43347 | if not globals()[b'_GLOBAL_DONE']: | ||
timeless
|
r28298 | traceback.print_exc() | ||
except Exception: | ||||
pass | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def getReaders(self): | ||
self.condition.acquire() | ||||
result = self.readers.keys() | ||||
self.condition.release() | ||||
return result | ||||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def addReader(self, reader, socket): | ||
self.condition.acquire() | ||||
self.readers[socket] = reader | ||||
self.condition.notify() | ||||
self.condition.release() | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def delReader(self, socket): | ||
self.condition.acquire() | ||||
timeless
|
r28301 | del self.readers[socket] | ||
timeless
|
r28298 | self.condition.notify() | ||
self.condition.release() | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def notify(self): | ||
self.condition.acquire() | ||||
self.condition.notify() | ||||
self.condition.release() | ||||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | class Listener(object): | ||
timeless
|
r28298 | """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. | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | It requires registration with an Engine object in order to have | ||
the read() method called when a socket is available for reading.""" | ||||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def __init__(self, zeroconf): | ||
self.zeroconf = zeroconf | ||||
self.zeroconf.engine.addReader(self, self.zeroconf.socket) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def handle_read(self): | ||
Jun Wu
|
r34448 | sock = self.zeroconf.socket | ||
try: | ||||
data, (addr, port) = sock.recvfrom(_MAX_MSG_ABSOLUTE) | ||||
except socket.error as e: | ||||
if e.errno == errno.EBADF: | ||||
# some other thread may close the socket | ||||
return | ||||
else: | ||||
raise | ||||
timeless
|
r28298 | 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) | ||||
Matt Mackall
|
r7071 | |||
class Reaper(threading.Thread): | ||||
timeless
|
r28298 | """A Reaper is used by this module to remove cache entries that | ||
have expired.""" | ||||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def __init__(self, zeroconf): | ||
threading.Thread.__init__(self) | ||||
self.zeroconf = zeroconf | ||||
self.start() | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def run(self): | ||
while True: | ||||
self.zeroconf.wait(10 * 1000) | ||||
Augie Fackler
|
r43347 | if globals()[b'_GLOBAL_DONE']: | ||
timeless
|
r28298 | return | ||
now = currentTimeMillis() | ||||
for record in self.zeroconf.cache.entries(): | ||||
if record.isExpired(now): | ||||
self.zeroconf.updateRecord(now, record) | ||||
self.zeroconf.cache.remove(record) | ||||
Matt Mackall
|
r7071 | |||
class ServiceBrowser(threading.Thread): | ||||
timeless
|
r28298 | """Used to browse for a service of a specific type. | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | The listener object will have its addService() and | ||
removeService() methods called when this browser | ||||
discovers changes in the services availability.""" | ||||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | 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 = {} | ||||
timeless
|
r28419 | self.nexttime = currentTimeMillis() | ||
timeless
|
r28298 | self.delay = _BROWSER_TIME | ||
self.list = [] | ||||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | self.done = 0 | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | self.zeroconf.addListener( | ||
self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN) | ||||
) | ||||
timeless
|
r28298 | self.start() | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def updateRecord(self, zeroconf, now, record): | ||
"""Callback invoked by Zeroconf when new information arrives. | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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: | ||||
timeless
|
r28301 | del self.services[record.alias.lower()] | ||
Augie Fackler
|
r43346 | callback = lambda x: self.listener.removeService( | ||
x, self.type, record.alias | ||||
) | ||||
timeless
|
r28298 | self.list.append(callback) | ||
return | ||||
except Exception: | ||||
if not expired: | ||||
self.services[record.alias.lower()] = record | ||||
Augie Fackler
|
r43346 | callback = lambda x: self.listener.addService( | ||
x, self.type, record.alias | ||||
) | ||||
timeless
|
r28298 | self.list.append(callback) | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | expires = record.getExpirationTime(75) | ||
timeless
|
r28419 | if expires < self.nexttime: | ||
self.nexttime = expires | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def cancel(self): | ||
self.done = 1 | ||||
self.zeroconf.notifyAll() | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def run(self): | ||
while True: | ||||
event = None | ||||
now = currentTimeMillis() | ||||
timeless
|
r28419 | if len(self.list) == 0 and self.nexttime > now: | ||
self.zeroconf.wait(self.nexttime - now) | ||||
Augie Fackler
|
r43347 | if globals()[b'_GLOBAL_DONE'] or self.done: | ||
timeless
|
r28298 | return | ||
now = currentTimeMillis() | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28419 | if self.nexttime <= now: | ||
timeless
|
r28298 | 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) | ||||
timeless
|
r28419 | self.nexttime = now + self.delay | ||
timeless
|
r28298 | self.delay = min(20 * 1000, self.delay * 2) | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | if len(self.list) > 0: | ||
event = self.list.pop(0) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | if event is not None: | ||
event(self.zeroconf) | ||||
Peter Arrenbrecht
|
r7877 | |||
Matt Mackall
|
r7071 | |||
class ServiceInfo(object): | ||||
timeless
|
r28298 | """Service information""" | ||
Peter Arrenbrecht
|
r7877 | |||
Augie Fackler
|
r43346 | def __init__( | ||
self, | ||||
type, | ||||
name, | ||||
address=None, | ||||
port=None, | ||||
weight=0, | ||||
priority=0, | ||||
properties=None, | ||||
server=None, | ||||
): | ||||
timeless
|
r28298 | """Create a service description. | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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 | ||||
timeless
|
r28299 | properties: dictionary of properties (or a string holding the bytes for | ||
the text field) | ||||
timeless
|
r28298 | server: fully qualified name for service host (defaults to name)""" | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def setProperties(self, properties): | ||
"""Sets properties and text of this info from a dictionary""" | ||||
if isinstance(properties, dict): | ||||
self.properties = properties | ||||
list = [] | ||||
Augie Fackler
|
r43347 | result = b'' | ||
timeless
|
r28298 | for key in properties: | ||
value = properties[key] | ||||
if value is None: | ||||
Augie Fackler
|
r43347 | suffix = b'' | ||
timeless
|
r28298 | elif isinstance(value, str): | ||
suffix = value | ||||
elif isinstance(value, int): | ||||
if value: | ||||
Augie Fackler
|
r43347 | suffix = b'true' | ||
timeless
|
r28298 | else: | ||
Augie Fackler
|
r43347 | suffix = b'false' | ||
timeless
|
r28298 | else: | ||
Augie Fackler
|
r43347 | suffix = b'' | ||
list.append(b'='.join((key, suffix))) | ||||
timeless
|
r28298 | for item in list: | ||
Augie Fackler
|
r43347 | result = b''.join( | ||
(result, struct.pack(b'!c', chr(len(item))), item) | ||||
Augie Fackler
|
r43346 | ) | ||
timeless
|
r28298 | self.text = result | ||
else: | ||||
self.text = properties | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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 | ||||
Augie Fackler
|
r43346 | strs.append(text[index : index + length]) | ||
timeless
|
r28298 | index += length | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | for s in strs: | ||
Augie Fackler
|
r43347 | eindex = s.find(b'=') | ||
timeless
|
r28298 | if eindex == -1: | ||
# No equals sign at all | ||||
key = s | ||||
value = 0 | ||||
else: | ||||
key = s[:eindex] | ||||
Augie Fackler
|
r43346 | value = s[eindex + 1 :] | ||
Augie Fackler
|
r43347 | if value == b'true': | ||
timeless
|
r28298 | value = 1 | ||
Augie Fackler
|
r43347 | elif value == b'false' or not value: | ||
timeless
|
r28298 | value = 0 | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | # Only update non-existent properties | ||
timeless
|
r28420 | if key and result.get(key) is None: | ||
timeless
|
r28298 | result[key] = value | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | self.properties = result | ||
except Exception: | ||||
traceback.print_exc() | ||||
self.properties = None | ||||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | def getType(self): | ||
"""Type accessor""" | ||||
return self.type | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def getName(self): | ||
"""Name accessor""" | ||||
Augie Fackler
|
r43347 | if self.type is not None and self.name.endswith(b"." + self.type): | ||
Augie Fackler
|
r43346 | return self.name[: len(self.name) - len(self.type) - 1] | ||
timeless
|
r28298 | return self.name | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def getAddress(self): | ||
"""Address accessor""" | ||||
return self.address | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def getPort(self): | ||
"""Port accessor""" | ||||
return self.port | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def getPriority(self): | ||
"""Priority accessor""" | ||||
return self.priority | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def getWeight(self): | ||
"""Weight accessor""" | ||||
return self.weight | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def getProperties(self): | ||
"""Properties accessor""" | ||||
return self.properties | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def getText(self): | ||
"""Text accessor""" | ||||
return self.text | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def getServer(self): | ||
"""Server accessor""" | ||||
return self.server | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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: | ||||
Augie Fackler
|
r43346 | # if record.name == self.name: | ||
timeless
|
r28298 | 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 | ||||
Augie Fackler
|
r43346 | # self.address = None | ||
self.updateRecord( | ||||
zeroconf, | ||||
now, | ||||
zeroconf.cache.getByDetails( | ||||
self.server, _TYPE_A, _CLASS_IN | ||||
), | ||||
) | ||||
timeless
|
r28298 | elif record.type == _TYPE_TXT: | ||
if record.name == self.name: | ||||
self.setText(record.text) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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 | ||||
try: | ||||
Augie Fackler
|
r43346 | 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 | ||||
): | ||||
timeless
|
r28298 | if last <= now: | ||
return 0 | ||||
if next <= now: | ||||
out = DNSOutgoing(_FLAGS_QR_QUERY) | ||||
Augie Fackler
|
r43346 | out.addQuestion( | ||
DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN) | ||||
) | ||||
timeless
|
r28299 | out.addAnswerAtTime( | ||
Augie Fackler
|
r43346 | zeroconf.cache.getByDetails( | ||
self.name, _TYPE_SRV, _CLASS_IN | ||||
), | ||||
now, | ||||
) | ||||
out.addQuestion( | ||||
DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN) | ||||
) | ||||
timeless
|
r28299 | out.addAnswerAtTime( | ||
Augie Fackler
|
r43346 | zeroconf.cache.getByDetails( | ||
self.name, _TYPE_TXT, _CLASS_IN | ||||
), | ||||
now, | ||||
) | ||||
timeless
|
r28298 | if self.server is not None: | ||
timeless
|
r28299 | out.addQuestion( | ||
Augie Fackler
|
r43346 | DNSQuestion(self.server, _TYPE_A, _CLASS_IN) | ||
) | ||||
timeless
|
r28299 | out.addAnswerAtTime( | ||
Augie Fackler
|
r43346 | zeroconf.cache.getByDetails( | ||
self.server, _TYPE_A, _CLASS_IN | ||||
), | ||||
now, | ||||
) | ||||
timeless
|
r28298 | zeroconf.send(out) | ||
next = now + delay | ||||
delay = delay * 2 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | zeroconf.wait(min(next, last) - now) | ||
now = currentTimeMillis() | ||||
result = 1 | ||||
finally: | ||||
zeroconf.removeListener(self) | ||||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | return result | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __eq__(self, other): | ||
"""Tests equality of service name""" | ||||
if isinstance(other, ServiceInfo): | ||||
return other.name == self.name | ||||
return 0 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __ne__(self, other): | ||
"""Non-equality test""" | ||||
return not self.__eq__(other) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def __repr__(self): | ||
"""String representation""" | ||||
Augie Fackler
|
r43347 | result = b"service[%s,%s:%s," % ( | ||
Augie Fackler
|
r43346 | self.name, | ||
socket.inet_ntoa(self.getAddress()), | ||||
self.port, | ||||
) | ||||
timeless
|
r28298 | if self.text is None: | ||
Augie Fackler
|
r43347 | result += b"None" | ||
timeless
|
r28298 | else: | ||
if len(self.text) < 20: | ||||
result += self.text | ||||
else: | ||||
Augie Fackler
|
r43347 | result += self.text[:17] + b"..." | ||
result += b"]" | ||||
timeless
|
r28298 | return result | ||
Peter Arrenbrecht
|
r7877 | |||
Matt Mackall
|
r7071 | |||
class Zeroconf(object): | ||||
timeless
|
r28298 | """Implementation of Zeroconf Multicast DNS Service Discovery | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | Supports registration, unregistration, queries and browsing. | ||
""" | ||||
Augie Fackler
|
r43346 | |||
timeless
|
r28298 | def __init__(self, bindaddress=None): | ||
"""Creates an instance of the Zeroconf class, establishing | ||||
multicast communications, listening and reaping threads.""" | ||||
Augie Fackler
|
r43347 | globals()[b'_GLOBAL_DONE'] = 0 | ||
timeless
|
r28298 | if bindaddress is None: | ||
self.intf = socket.gethostbyname(socket.gethostname()) | ||||
else: | ||||
self.intf = bindaddress | ||||
Augie Fackler
|
r43347 | self.group = (b'', _MDNS_PORT) | ||
timeless
|
r28298 | 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 Exception: | ||||
# SO_REUSEADDR should be equivalent to SO_REUSEPORT for | ||||
# multicast UDP sockets (p 731, "TCP/IP Illustrated, | ||||
# Volume 2"), but some BSD-derived systems require | ||||
# SO_REUSEPORT to be specified explicitly. 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 | ||||
Augie Fackler
|
r43347 | self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, b"\xff") | ||
self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, b"\x01") | ||||
timeless
|
r28298 | try: | ||
self.socket.bind(self.group) | ||||
except Exception: | ||||
# Some versions of linux raise an exception even though | ||||
# SO_REUSEADDR and SO_REUSEPORT have been set, so ignore it | ||||
pass | ||||
Augie Fackler
|
r43346 | self.socket.setsockopt( | ||
socket.SOL_IP, | ||||
socket.IP_ADD_MEMBERSHIP, | ||||
socket.inet_aton(_MDNS_ADDR) + socket.inet_aton(r'0.0.0.0'), | ||||
) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | self.listeners = [] | ||
self.browsers = [] | ||||
self.services = {} | ||||
self.servicetypes = {} | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | self.cache = DNSCache() | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | self.condition = threading.Condition() | ||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | self.engine = Engine(self) | ||
self.listener = Listener(self) | ||||
self.reaper = Reaper(self) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def isLoopback(self): | ||
Augie Fackler
|
r43347 | return self.intf.startswith(b"127.0.0.1") | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def isLinklocal(self): | ||
Augie Fackler
|
r43347 | return self.intf.startswith(b"169.254.") | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def wait(self, timeout): | ||
"""Calling thread waits for a given number of milliseconds or | ||||
until notified.""" | ||||
self.condition.acquire() | ||||
timeless
|
r28300 | self.condition.wait(timeout / 1000) | ||
timeless
|
r28298 | self.condition.release() | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def notifyAll(self): | ||
"""Notifies all waiting threads""" | ||||
self.condition.acquire() | ||||
self.condition.notifyAll() | ||||
self.condition.release() | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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 | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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)) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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() | ||||
timeless
|
r28301 | del browser | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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 | ||||
timeless
|
r28421 | if info.type in self.servicetypes: | ||
timeless
|
r28300 | self.servicetypes[info.type] += 1 | ||
timeless
|
r28298 | else: | ||
timeless
|
r28300 | self.servicetypes[info.type] = 1 | ||
timeless
|
r28298 | now = currentTimeMillis() | ||
timeless
|
r28419 | nexttime = now | ||
timeless
|
r28298 | i = 0 | ||
while i < 3: | ||||
timeless
|
r28419 | if now < nexttime: | ||
self.wait(nexttime - now) | ||||
timeless
|
r28298 | now = currentTimeMillis() | ||
continue | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||||
Augie Fackler
|
r43346 | out.addAnswerAtTime( | ||
DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0 | ||||
) | ||||
timeless
|
r28299 | out.addAnswerAtTime( | ||
DNSService( | ||||
Augie Fackler
|
r43346 | info.name, | ||
_TYPE_SRV, | ||||
_CLASS_IN, | ||||
ttl, | ||||
info.priority, | ||||
info.weight, | ||||
info.port, | ||||
info.server, | ||||
), | ||||
0, | ||||
) | ||||
timeless
|
r28299 | out.addAnswerAtTime( | ||
Augie Fackler
|
r43346 | DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0 | ||
) | ||||
timeless
|
r28298 | if info.address: | ||
Augie Fackler
|
r43346 | out.addAnswerAtTime( | ||
DNSAddress( | ||||
info.server, _TYPE_A, _CLASS_IN, ttl, info.address | ||||
), | ||||
0, | ||||
) | ||||
timeless
|
r28298 | self.send(out) | ||
i += 1 | ||||
timeless
|
r28419 | nexttime += _REGISTER_TIME | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def unregisterService(self, info): | ||
"""Unregister a service.""" | ||||
try: | ||||
timeless
|
r28301 | del self.services[info.name.lower()] | ||
timeless
|
r28300 | if self.servicetypes[info.type] > 1: | ||
self.servicetypes[info.type] -= 1 | ||||
timeless
|
r28298 | else: | ||
del self.servicetypes[info.type] | ||||
except KeyError: | ||||
pass | ||||
now = currentTimeMillis() | ||||
timeless
|
r28419 | nexttime = now | ||
timeless
|
r28298 | i = 0 | ||
while i < 3: | ||||
timeless
|
r28419 | if now < nexttime: | ||
self.wait(nexttime - now) | ||||
timeless
|
r28298 | now = currentTimeMillis() | ||
continue | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||||
timeless
|
r28299 | out.addAnswerAtTime( | ||
Augie Fackler
|
r43346 | DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0 | ||
) | ||||
timeless
|
r28299 | out.addAnswerAtTime( | ||
Augie Fackler
|
r43346 | 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 | ||||
) | ||||
timeless
|
r28298 | if info.address: | ||
Augie Fackler
|
r43346 | out.addAnswerAtTime( | ||
DNSAddress( | ||||
info.server, _TYPE_A, _CLASS_IN, 0, info.address | ||||
), | ||||
0, | ||||
) | ||||
timeless
|
r28298 | self.send(out) | ||
i += 1 | ||||
timeless
|
r28419 | nexttime += _UNREGISTER_TIME | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def unregisterAllServices(self): | ||
"""Unregister all registered services.""" | ||||
if len(self.services) > 0: | ||||
now = currentTimeMillis() | ||||
timeless
|
r28419 | nexttime = now | ||
timeless
|
r28298 | i = 0 | ||
while i < 3: | ||||
timeless
|
r28419 | if now < nexttime: | ||
self.wait(nexttime - now) | ||||
timeless
|
r28298 | now = currentTimeMillis() | ||
continue | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||||
for info in self.services.values(): | ||||
Augie Fackler
|
r43346 | out.addAnswerAtTime( | ||
DNSPointer( | ||||
info.type, _TYPE_PTR, _CLASS_IN, 0, info.name | ||||
), | ||||
0, | ||||
) | ||||
timeless
|
r28299 | out.addAnswerAtTime( | ||
Augie Fackler
|
r43346 | 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, | ||||
) | ||||
timeless
|
r28298 | if info.address: | ||
Augie Fackler
|
r43346 | out.addAnswerAtTime( | ||
DNSAddress( | ||||
info.server, _TYPE_A, _CLASS_IN, 0, info.address | ||||
), | ||||
0, | ||||
) | ||||
timeless
|
r28298 | self.send(out) | ||
i += 1 | ||||
timeless
|
r28419 | nexttime += _UNREGISTER_TIME | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def checkService(self, info): | ||
"""Checks the network for a unique service name, modifying the | ||||
ServiceInfo passed in if it is not unique.""" | ||||
now = currentTimeMillis() | ||||
timeless
|
r28419 | nexttime = now | ||
timeless
|
r28298 | i = 0 | ||
while i < 3: | ||||
for record in self.cache.entriesWithName(info.type): | ||||
Augie Fackler
|
r43346 | if ( | ||
record.type == _TYPE_PTR | ||||
and not record.isExpired(now) | ||||
and record.alias == info.name | ||||
): | ||||
Augie Fackler
|
r43347 | if info.name.find(b'.') < 0: | ||
info.name = b"%w.[%s:%d].%s" % ( | ||||
Augie Fackler
|
r43346 | info.name, | ||
info.address, | ||||
info.port, | ||||
info.type, | ||||
) | ||||
timeless
|
r28298 | self.checkService(info) | ||
return | ||||
raise NonUniqueNameException | ||||
timeless
|
r28419 | if now < nexttime: | ||
self.wait(nexttime - now) | ||||
timeless
|
r28298 | now = currentTimeMillis() | ||
continue | ||||
out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA) | ||||
self.debug = out | ||||
out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN)) | ||||
Augie Fackler
|
r43346 | out.addAuthoritativeAnswer( | ||
DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name) | ||||
) | ||||
timeless
|
r28298 | self.send(out) | ||
i += 1 | ||||
timeless
|
r28419 | nexttime += _CHECK_TIME | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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() | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def removeListener(self, listener): | ||
"""Removes a listener.""" | ||||
try: | ||||
self.listeners.remove(listener) | ||||
self.notifyAll() | ||||
except Exception: | ||||
pass | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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() | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | 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) | ||||
Peter Arrenbrecht
|
r7877 | |||
timeless
|
r28298 | self.updateRecord(now, record) | ||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def handleQuery(self, msg, addr, port): | ||
"""Deal with incoming query packets. Provides a response if | ||||
possible.""" | ||||
out = None | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | # Support unicast client responses | ||
# | ||||
if port != _MDNS_PORT: | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0) | ||||
for question in msg.questions: | ||||
out.addQuestion(question) | ||||
Peter Arrenbrecht
|
r7874 | |||
timeless
|
r28298 | for question in msg.questions: | ||
if question.type == _TYPE_PTR: | ||||
Augie Fackler
|
r43347 | if question.name == b"_services._dns-sd._udp.local.": | ||
timeless
|
r28298 | for stype in self.servicetypes.keys(): | ||
if out is None: | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||||
Augie Fackler
|
r43346 | out.addAnswer( | ||
msg, | ||||
DNSPointer( | ||||
Augie Fackler
|
r43347 | b"_services._dns-sd._udp.local.", | ||
Augie Fackler
|
r43346 | _TYPE_PTR, | ||
_CLASS_IN, | ||||
_DNS_TTL, | ||||
stype, | ||||
), | ||||
) | ||||
timeless
|
r28298 | for service in self.services.values(): | ||
if question.name == service.type: | ||||
if out is None: | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||||
Augie Fackler
|
r43346 | out.addAnswer( | ||
msg, | ||||
DNSPointer( | ||||
service.type, | ||||
_TYPE_PTR, | ||||
_CLASS_IN, | ||||
_DNS_TTL, | ||||
service.name, | ||||
), | ||||
) | ||||
timeless
|
r28298 | else: | ||
try: | ||||
if out is None: | ||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||||
Peter Arrenbrecht
|
r7874 | |||
timeless
|
r28298 | # 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(): | ||||
Augie Fackler
|
r43346 | out.addAnswer( | ||
msg, | ||||
DNSAddress( | ||||
question.name, | ||||
_TYPE_A, | ||||
_CLASS_IN | _CLASS_UNIQUE, | ||||
_DNS_TTL, | ||||
service.address, | ||||
), | ||||
) | ||||
Peter Arrenbrecht
|
r7874 | |||
timeless
|
r28298 | service = self.services.get(question.name.lower(), None) | ||
Boris Feld
|
r35647 | if not service: | ||
continue | ||||
Peter Arrenbrecht
|
r7874 | |||
Augie Fackler
|
r43346 | 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, | ||||
), | ||||
) | ||||
timeless
|
r28298 | if question.type == _TYPE_SRV: | ||
timeless
|
r28299 | out.addAdditionalAnswer( | ||
Augie Fackler
|
r43346 | DNSAddress( | ||
service.server, | ||||
_TYPE_A, | ||||
_CLASS_IN | _CLASS_UNIQUE, | ||||
_DNS_TTL, | ||||
service.address, | ||||
) | ||||
) | ||||
timeless
|
r28298 | except Exception: | ||
traceback.print_exc() | ||||
Peter Arrenbrecht
|
r7874 | |||
timeless
|
r28298 | if out is not None and out.answers: | ||
out.id = msg.id | ||||
self.send(out, addr, port) | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28302 | def send(self, out, addr=_MDNS_ADDR, port=_MDNS_PORT): | ||
timeless
|
r28298 | """Sends an outgoing packet.""" | ||
# This is a quick test to see if we can parse the packets we generate | ||||
Augie Fackler
|
r43346 | # temp = DNSIncoming(out.packet()) | ||
timeless
|
r28298 | try: | ||
self.socket.sendto(out.packet(), 0, (addr, port)) | ||||
except Exception: | ||||
# Ignore this, it may be a temporary loss of network connection | ||||
pass | ||||
Matt Mackall
|
r7071 | |||
timeless
|
r28298 | def close(self): | ||
"""Ends the background threads, and prevent this instance from | ||||
servicing further queries.""" | ||||
Augie Fackler
|
r43347 | if globals()[b'_GLOBAL_DONE'] == 0: | ||
globals()[b'_GLOBAL_DONE'] = 1 | ||||
timeless
|
r28298 | self.notifyAll() | ||
self.engine.notify() | ||||
self.unregisterAllServices() | ||||
Augie Fackler
|
r43346 | self.socket.setsockopt( | ||
socket.SOL_IP, | ||||
socket.IP_DROP_MEMBERSHIP, | ||||
socket.inet_aton(_MDNS_ADDR) + socket.inet_aton(r'0.0.0.0'), | ||||
) | ||||
timeless
|
r28298 | self.socket.close() | ||
Peter Arrenbrecht
|
r7877 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | # Test a few module features, including service registration, service | ||
# query (for Zoe), and service unregistration. | ||||
Peter Arrenbrecht
|
r7877 | if __name__ == '__main__': | ||
Augie Fackler
|
r43347 | print(b"Multicast DNS Service Discovery for Python, version", __version__) | ||
timeless
|
r28298 | r = Zeroconf() | ||
Augie Fackler
|
r43347 | print(b"1. Testing registration of a service...") | ||
desc = {b'version': b'0.10', b'a': b'test value', b'b': b'another value'} | ||||
Augie Fackler
|
r43346 | info = ServiceInfo( | ||
Augie Fackler
|
r43347 | b"_http._tcp.local.", | ||
b"My Service Name._http._tcp.local.", | ||||
socket.inet_aton(b"127.0.0.1"), | ||||
Augie Fackler
|
r43346 | 1234, | ||
0, | ||||
0, | ||||
desc, | ||||
) | ||||
Augie Fackler
|
r43347 | print(b" Registering service...") | ||
timeless
|
r28298 | r.registerService(info) | ||
Augie Fackler
|
r43347 | print(b" Registration done.") | ||
print(b"2. Testing query of service information...") | ||||
Augie Fackler
|
r43346 | print( | ||
Augie Fackler
|
r43347 | b" Getting ZOE service:", | ||
str(r.getServiceInfo(b"_http._tcp.local.", b"ZOE._http._tcp.local.")), | ||||
Augie Fackler
|
r43346 | ) | ||
Augie Fackler
|
r43347 | print(b" Query done.") | ||
print(b"3. Testing query of own service...") | ||||
Augie Fackler
|
r43346 | print( | ||
Augie Fackler
|
r43347 | b" Getting self:", | ||
Augie Fackler
|
r43346 | str( | ||
r.getServiceInfo( | ||||
Augie Fackler
|
r43347 | b"_http._tcp.local.", b"My Service Name._http._tcp.local." | ||
Augie Fackler
|
r43346 | ) | ||
), | ||||
) | ||||
Augie Fackler
|
r43347 | print(b" Query done.") | ||
print(b"4. Testing unregister of service information...") | ||||
timeless
|
r28298 | r.unregisterService(info) | ||
Augie Fackler
|
r43347 | print(b" Unregister done.") | ||
timeless
|
r28298 | r.close() | ||