##// END OF EJS Templates
zeroconf: fix traceback under py3...
Kim Alvefur -
r44094:0f82b29f stable
parent child Browse files
Show More
@@ -1,1888 +1,1892 b''
1 1 from __future__ import absolute_import, print_function
2 2
3 3 """ Multicast DNS Service Discovery for Python, v0.12
4 4 Copyright (C) 2003, Paul Scott-Murphy
5 5
6 6 This module provides a framework for the use of DNS Service Discovery
7 7 using IP multicast. It has been tested against the JRendezvous
8 8 implementation from <a href="http://strangeberry.com">StrangeBerry</a>,
9 9 and against the mDNSResponder from Mac OS X 10.3.8.
10 10
11 11 This library is free software; you can redistribute it and/or
12 12 modify it under the terms of the GNU Lesser General Public
13 13 License as published by the Free Software Foundation; either
14 14 version 2.1 of the License, or (at your option) any later version.
15 15
16 16 This library is distributed in the hope that it will be useful,
17 17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 19 Lesser General Public License for more details.
20 20
21 21 You should have received a copy of the GNU Lesser General Public
22 22 License along with this library; if not, see
23 23 <http://www.gnu.org/licenses/>.
24 24
25 25 """
26 26
27 27 """0.12 update - allow selection of binding interface
28 28 typo fix - Thanks A. M. Kuchlingi
29 29 removed all use of word 'Rendezvous' - this is an API change"""
30 30
31 31 """0.11 update - correction to comments for addListener method
32 32 support for new record types seen from OS X
33 33 - IPv6 address
34 34 - hostinfo
35 35 ignore unknown DNS record types
36 36 fixes to name decoding
37 37 works alongside other processes using port 5353 (e.g. Mac OS X)
38 38 tested against Mac OS X 10.3.2's mDNSResponder
39 39 corrections to removal of list entries for service browser"""
40 40
41 41 """0.10 update - Jonathon Paisley contributed these corrections:
42 42 always multicast replies, even when query is unicast
43 43 correct a pointer encoding problem
44 44 can now write records in any order
45 45 traceback shown on failure
46 46 better TXT record parsing
47 47 server is now separate from name
48 48 can cancel a service browser
49 49
50 50 modified some unit tests to accommodate these changes"""
51 51
52 52 """0.09 update - remove all records on service unregistration
53 53 fix DOS security problem with readName"""
54 54
55 55 """0.08 update - changed licensing to LGPL"""
56 56
57 57 """0.07 update - faster shutdown on engine
58 58 pointer encoding of outgoing names
59 59 ServiceBrowser now works
60 60 new unit tests"""
61 61
62 62 """0.06 update - small improvements with unit tests
63 63 added defined exception types
64 64 new style objects
65 65 fixed hostname/interface problem
66 66 fixed socket timeout problem
67 67 fixed addServiceListener() typo bug
68 68 using select() for socket reads
69 69 tested on Debian unstable with Python 2.2.2"""
70 70
71 71 """0.05 update - ensure case insensitivity on domain names
72 72 support for unicast DNS queries"""
73 73
74 74 """0.04 update - added some unit tests
75 75 added __ne__ adjuncts where required
76 76 ensure names end in '.local.'
77 77 timeout on receiving socket for clean shutdown"""
78 78
79 79 __author__ = b"Paul Scott-Murphy"
80 80 __email__ = b"paul at scott dash murphy dot com"
81 81 __version__ = b"0.12"
82 82
83 83 import errno
84 84 import itertools
85 85 import select
86 86 import socket
87 87 import struct
88 88 import threading
89 89 import time
90 90 import traceback
91 91
92 92 from mercurial import pycompat
93 93
94 94 __all__ = [b"Zeroconf", b"ServiceInfo", b"ServiceBrowser"]
95 95
96 96 # hook for threads
97 97
98 98 globals()[b'_GLOBAL_DONE'] = 0
99 99
100 100 # Some timing constants
101 101
102 102 _UNREGISTER_TIME = 125
103 103 _CHECK_TIME = 175
104 104 _REGISTER_TIME = 225
105 105 _LISTENER_TIME = 200
106 106 _BROWSER_TIME = 500
107 107
108 108 # Some DNS constants
109 109
110 110 _MDNS_ADDR = r'224.0.0.251'
111 111 _MDNS_PORT = 5353
112 112 _DNS_PORT = 53
113 113 _DNS_TTL = 60 * 60 # one hour default TTL
114 114
115 115 _MAX_MSG_TYPICAL = 1460 # unused
116 116 _MAX_MSG_ABSOLUTE = 8972
117 117
118 118 _FLAGS_QR_MASK = 0x8000 # query response mask
119 119 _FLAGS_QR_QUERY = 0x0000 # query
120 120 _FLAGS_QR_RESPONSE = 0x8000 # response
121 121
122 122 _FLAGS_AA = 0x0400 # Authoritative answer
123 123 _FLAGS_TC = 0x0200 # Truncated
124 124 _FLAGS_RD = 0x0100 # Recursion desired
125 125 _FLAGS_RA = 0x8000 # Recursion available
126 126
127 127 _FLAGS_Z = 0x0040 # Zero
128 128 _FLAGS_AD = 0x0020 # Authentic data
129 129 _FLAGS_CD = 0x0010 # Checking disabled
130 130
131 131 _CLASS_IN = 1
132 132 _CLASS_CS = 2
133 133 _CLASS_CH = 3
134 134 _CLASS_HS = 4
135 135 _CLASS_NONE = 254
136 136 _CLASS_ANY = 255
137 137 _CLASS_MASK = 0x7FFF
138 138 _CLASS_UNIQUE = 0x8000
139 139
140 140 _TYPE_A = 1
141 141 _TYPE_NS = 2
142 142 _TYPE_MD = 3
143 143 _TYPE_MF = 4
144 144 _TYPE_CNAME = 5
145 145 _TYPE_SOA = 6
146 146 _TYPE_MB = 7
147 147 _TYPE_MG = 8
148 148 _TYPE_MR = 9
149 149 _TYPE_NULL = 10
150 150 _TYPE_WKS = 11
151 151 _TYPE_PTR = 12
152 152 _TYPE_HINFO = 13
153 153 _TYPE_MINFO = 14
154 154 _TYPE_MX = 15
155 155 _TYPE_TXT = 16
156 156 _TYPE_AAAA = 28
157 157 _TYPE_SRV = 33
158 158 _TYPE_ANY = 255
159 159
160 160 # Mapping constants to names
161 161
162 162 _CLASSES = {
163 163 _CLASS_IN: b"in",
164 164 _CLASS_CS: b"cs",
165 165 _CLASS_CH: b"ch",
166 166 _CLASS_HS: b"hs",
167 167 _CLASS_NONE: b"none",
168 168 _CLASS_ANY: b"any",
169 169 }
170 170
171 171 _TYPES = {
172 172 _TYPE_A: b"a",
173 173 _TYPE_NS: b"ns",
174 174 _TYPE_MD: b"md",
175 175 _TYPE_MF: b"mf",
176 176 _TYPE_CNAME: b"cname",
177 177 _TYPE_SOA: b"soa",
178 178 _TYPE_MB: b"mb",
179 179 _TYPE_MG: b"mg",
180 180 _TYPE_MR: b"mr",
181 181 _TYPE_NULL: b"null",
182 182 _TYPE_WKS: b"wks",
183 183 _TYPE_PTR: b"ptr",
184 184 _TYPE_HINFO: b"hinfo",
185 185 _TYPE_MINFO: b"minfo",
186 186 _TYPE_MX: b"mx",
187 187 _TYPE_TXT: b"txt",
188 188 _TYPE_AAAA: b"quada",
189 189 _TYPE_SRV: b"srv",
190 190 _TYPE_ANY: b"any",
191 191 }
192 192
193 193 # utility functions
194 194
195 195
196 196 def currentTimeMillis():
197 197 """Current system time in milliseconds"""
198 198 return time.time() * 1000
199 199
200 200
201 201 # Exceptions
202 202
203 203
204 204 class NonLocalNameException(Exception):
205 205 pass
206 206
207 207
208 208 class NonUniqueNameException(Exception):
209 209 pass
210 210
211 211
212 212 class NamePartTooLongException(Exception):
213 213 pass
214 214
215 215
216 216 class AbstractMethodException(Exception):
217 217 pass
218 218
219 219
220 220 class BadTypeInNameException(Exception):
221 221 pass
222 222
223 223
224 224 class BadDomainName(Exception):
225 225 def __init__(self, pos):
226 226 Exception.__init__(self, b"at position %s" % pos)
227 227
228 228
229 229 class BadDomainNameCircular(BadDomainName):
230 230 pass
231 231
232 232
233 233 # implementation classes
234 234
235 235
236 236 class DNSEntry(object):
237 237 """A DNS entry"""
238 238
239 239 def __init__(self, name, type, clazz):
240 240 self.key = name.lower()
241 241 self.name = name
242 242 self.type = type
243 243 self.clazz = clazz & _CLASS_MASK
244 244 self.unique = (clazz & _CLASS_UNIQUE) != 0
245 245
246 246 def __eq__(self, other):
247 247 """Equality test on name, type, and class"""
248 248 if isinstance(other, DNSEntry):
249 249 return (
250 250 self.name == other.name
251 251 and self.type == other.type
252 252 and self.clazz == other.clazz
253 253 )
254 254 return 0
255 255
256 256 def __ne__(self, other):
257 257 """Non-equality test"""
258 258 return not self.__eq__(other)
259 259
260 260 def getClazz(self, clazz):
261 261 """Class accessor"""
262 262 try:
263 263 return _CLASSES[clazz]
264 264 except KeyError:
265 265 return b"?(%s)" % clazz
266 266
267 267 def getType(self, type):
268 268 """Type accessor"""
269 269 try:
270 270 return _TYPES[type]
271 271 except KeyError:
272 272 return b"?(%s)" % type
273 273
274 274 def toString(self, hdr, other):
275 275 """String representation with additional information"""
276 276 result = b"%s[%s,%s" % (
277 277 hdr,
278 278 self.getType(self.type),
279 279 self.getClazz(self.clazz),
280 280 )
281 281 if self.unique:
282 282 result += b"-unique,"
283 283 else:
284 284 result += b","
285 285 result += self.name
286 286 if other is not None:
287 287 result += b",%s]" % other
288 288 else:
289 289 result += b"]"
290 290 return result
291 291
292 292
293 293 class DNSQuestion(DNSEntry):
294 294 """A DNS question entry"""
295 295
296 296 def __init__(self, name, type, clazz):
297 297 if pycompat.ispy3 and isinstance(name, str):
298 298 name = name.encode('ascii')
299 299 if not name.endswith(b".local."):
300 300 raise NonLocalNameException(name)
301 301 DNSEntry.__init__(self, name, type, clazz)
302 302
303 303 def answeredBy(self, rec):
304 304 """Returns true if the question is answered by the record"""
305 305 return (
306 306 self.clazz == rec.clazz
307 307 and (self.type == rec.type or self.type == _TYPE_ANY)
308 308 and self.name == rec.name
309 309 )
310 310
311 311 def __repr__(self):
312 312 """String representation"""
313 313 return DNSEntry.toString(self, b"question", None)
314 314
315 315
316 316 class DNSRecord(DNSEntry):
317 317 """A DNS record - like a DNS entry, but has a TTL"""
318 318
319 319 def __init__(self, name, type, clazz, ttl):
320 320 DNSEntry.__init__(self, name, type, clazz)
321 321 self.ttl = ttl
322 322 self.created = currentTimeMillis()
323 323
324 324 def __eq__(self, other):
325 325 """Tests equality as per DNSRecord"""
326 326 if isinstance(other, DNSRecord):
327 327 return DNSEntry.__eq__(self, other)
328 328 return 0
329 329
330 330 def suppressedBy(self, msg):
331 331 """Returns true if any answer in a message can suffice for the
332 332 information held in this record."""
333 333 for record in msg.answers:
334 334 if self.suppressedByAnswer(record):
335 335 return 1
336 336 return 0
337 337
338 338 def suppressedByAnswer(self, other):
339 339 """Returns true if another record has same name, type and class,
340 340 and if its TTL is at least half of this record's."""
341 341 if self == other and other.ttl > (self.ttl / 2):
342 342 return 1
343 343 return 0
344 344
345 345 def getExpirationTime(self, percent):
346 346 """Returns the time at which this record will have expired
347 347 by a certain percentage."""
348 348 return self.created + (percent * self.ttl * 10)
349 349
350 350 def getRemainingTTL(self, now):
351 351 """Returns the remaining TTL in seconds."""
352 352 return max(0, (self.getExpirationTime(100) - now) / 1000)
353 353
354 354 def isExpired(self, now):
355 355 """Returns true if this record has expired."""
356 356 return self.getExpirationTime(100) <= now
357 357
358 358 def isStale(self, now):
359 359 """Returns true if this record is at least half way expired."""
360 360 return self.getExpirationTime(50) <= now
361 361
362 362 def resetTTL(self, other):
363 363 """Sets this record's TTL and created time to that of
364 364 another record."""
365 365 self.created = other.created
366 366 self.ttl = other.ttl
367 367
368 368 def write(self, out):
369 369 """Abstract method"""
370 370 raise AbstractMethodException
371 371
372 372 def toString(self, other):
373 373 """String representation with additional information"""
374 374 arg = b"%s/%s,%s" % (
375 375 self.ttl,
376 376 self.getRemainingTTL(currentTimeMillis()),
377 377 other,
378 378 )
379 379 return DNSEntry.toString(self, b"record", arg)
380 380
381 381
382 382 class DNSAddress(DNSRecord):
383 383 """A DNS address record"""
384 384
385 385 def __init__(self, name, type, clazz, ttl, address):
386 386 DNSRecord.__init__(self, name, type, clazz, ttl)
387 387 self.address = address
388 388
389 389 def write(self, out):
390 390 """Used in constructing an outgoing packet"""
391 391 out.writeString(self.address, len(self.address))
392 392
393 393 def __eq__(self, other):
394 394 """Tests equality on address"""
395 395 if isinstance(other, DNSAddress):
396 396 return self.address == other.address
397 397 return 0
398 398
399 399 def __repr__(self):
400 400 """String representation"""
401 401 try:
402 402 return socket.inet_ntoa(self.address)
403 403 except Exception:
404 404 return self.address
405 405
406 406
407 407 class DNSHinfo(DNSRecord):
408 408 """A DNS host information record"""
409 409
410 410 def __init__(self, name, type, clazz, ttl, cpu, os):
411 411 DNSRecord.__init__(self, name, type, clazz, ttl)
412 412 self.cpu = cpu
413 413 self.os = os
414 414
415 415 def write(self, out):
416 416 """Used in constructing an outgoing packet"""
417 417 out.writeString(self.cpu, len(self.cpu))
418 418 out.writeString(self.os, len(self.os))
419 419
420 420 def __eq__(self, other):
421 421 """Tests equality on cpu and os"""
422 422 if isinstance(other, DNSHinfo):
423 423 return self.cpu == other.cpu and self.os == other.os
424 424 return 0
425 425
426 426 def __repr__(self):
427 427 """String representation"""
428 428 return self.cpu + b" " + self.os
429 429
430 430
431 431 class DNSPointer(DNSRecord):
432 432 """A DNS pointer record"""
433 433
434 434 def __init__(self, name, type, clazz, ttl, alias):
435 435 DNSRecord.__init__(self, name, type, clazz, ttl)
436 436 self.alias = alias
437 437
438 438 def write(self, out):
439 439 """Used in constructing an outgoing packet"""
440 440 out.writeName(self.alias)
441 441
442 442 def __eq__(self, other):
443 443 """Tests equality on alias"""
444 444 if isinstance(other, DNSPointer):
445 445 return self.alias == other.alias
446 446 return 0
447 447
448 448 def __repr__(self):
449 449 """String representation"""
450 450 return self.toString(self.alias)
451 451
452 452
453 453 class DNSText(DNSRecord):
454 454 """A DNS text record"""
455 455
456 456 def __init__(self, name, type, clazz, ttl, text):
457 457 DNSRecord.__init__(self, name, type, clazz, ttl)
458 458 self.text = text
459 459
460 460 def write(self, out):
461 461 """Used in constructing an outgoing packet"""
462 462 out.writeString(self.text, len(self.text))
463 463
464 464 def __eq__(self, other):
465 465 """Tests equality on text"""
466 466 if isinstance(other, DNSText):
467 467 return self.text == other.text
468 468 return 0
469 469
470 470 def __repr__(self):
471 471 """String representation"""
472 472 if len(self.text) > 10:
473 473 return self.toString(self.text[:7] + b"...")
474 474 else:
475 475 return self.toString(self.text)
476 476
477 477
478 478 class DNSService(DNSRecord):
479 479 """A DNS service record"""
480 480
481 481 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
482 482 DNSRecord.__init__(self, name, type, clazz, ttl)
483 483 self.priority = priority
484 484 self.weight = weight
485 485 self.port = port
486 486 self.server = server
487 487
488 488 def write(self, out):
489 489 """Used in constructing an outgoing packet"""
490 490 out.writeShort(self.priority)
491 491 out.writeShort(self.weight)
492 492 out.writeShort(self.port)
493 493 out.writeName(self.server)
494 494
495 495 def __eq__(self, other):
496 496 """Tests equality on priority, weight, port and server"""
497 497 if isinstance(other, DNSService):
498 498 return (
499 499 self.priority == other.priority
500 500 and self.weight == other.weight
501 501 and self.port == other.port
502 502 and self.server == other.server
503 503 )
504 504 return 0
505 505
506 506 def __repr__(self):
507 507 """String representation"""
508 508 return self.toString(b"%s:%s" % (self.server, self.port))
509 509
510 510
511 511 class DNSIncoming(object):
512 512 """Object representation of an incoming DNS packet"""
513 513
514 514 def __init__(self, data):
515 515 """Constructor from string holding bytes of packet"""
516 516 self.offset = 0
517 517 self.data = data
518 518 self.questions = []
519 519 self.answers = []
520 520 self.numquestions = 0
521 521 self.numanswers = 0
522 522 self.numauthorities = 0
523 523 self.numadditionals = 0
524 524
525 525 self.readHeader()
526 526 self.readQuestions()
527 527 self.readOthers()
528 528
529 529 def readHeader(self):
530 530 """Reads header portion of packet"""
531 531 format = b'!HHHHHH'
532 532 length = struct.calcsize(format)
533 533 info = struct.unpack(
534 534 format, self.data[self.offset : self.offset + length]
535 535 )
536 536 self.offset += length
537 537
538 538 self.id = info[0]
539 539 self.flags = info[1]
540 540 self.numquestions = info[2]
541 541 self.numanswers = info[3]
542 542 self.numauthorities = info[4]
543 543 self.numadditionals = info[5]
544 544
545 545 def readQuestions(self):
546 546 """Reads questions section of packet"""
547 547 format = b'!HH'
548 548 length = struct.calcsize(format)
549 549 for i in range(0, self.numquestions):
550 550 name = self.readName()
551 551 info = struct.unpack(
552 552 format, self.data[self.offset : self.offset + length]
553 553 )
554 554 self.offset += length
555 555
556 556 try:
557 557 question = DNSQuestion(name, info[0], info[1])
558 558 self.questions.append(question)
559 559 except NonLocalNameException:
560 560 pass
561 561
562 562 def readInt(self):
563 563 """Reads an integer from the packet"""
564 564 format = b'!I'
565 565 length = struct.calcsize(format)
566 566 info = struct.unpack(
567 567 format, self.data[self.offset : self.offset + length]
568 568 )
569 569 self.offset += length
570 570 return info[0]
571 571
572 572 def readCharacterString(self):
573 573 """Reads a character string from the packet"""
574 574 length = ord(self.data[self.offset])
575 575 self.offset += 1
576 576 return self.readString(length)
577 577
578 578 def readString(self, len):
579 579 """Reads a string of a given length from the packet"""
580 580 format = b'!%ds' % len
581 581 length = struct.calcsize(format)
582 582 info = struct.unpack(
583 583 format, self.data[self.offset : self.offset + length]
584 584 )
585 585 self.offset += length
586 586 return info[0]
587 587
588 588 def readUnsignedShort(self):
589 589 """Reads an unsigned short from the packet"""
590 590 format = b'!H'
591 591 length = struct.calcsize(format)
592 592 info = struct.unpack(
593 593 format, self.data[self.offset : self.offset + length]
594 594 )
595 595 self.offset += length
596 596 return info[0]
597 597
598 598 def readOthers(self):
599 599 """Reads answers, authorities and additionals section of the packet"""
600 600 format = b'!HHiH'
601 601 length = struct.calcsize(format)
602 602 n = self.numanswers + self.numauthorities + self.numadditionals
603 603 for i in range(0, n):
604 604 domain = self.readName()
605 605 info = struct.unpack(
606 606 format, self.data[self.offset : self.offset + length]
607 607 )
608 608 self.offset += length
609 609
610 610 rec = None
611 611 if info[0] == _TYPE_A:
612 612 rec = DNSAddress(
613 613 domain, info[0], info[1], info[2], self.readString(4)
614 614 )
615 615 elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR:
616 616 rec = DNSPointer(
617 617 domain, info[0], info[1], info[2], self.readName()
618 618 )
619 619 elif info[0] == _TYPE_TXT:
620 620 rec = DNSText(
621 621 domain, info[0], info[1], info[2], self.readString(info[3])
622 622 )
623 623 elif info[0] == _TYPE_SRV:
624 624 rec = DNSService(
625 625 domain,
626 626 info[0],
627 627 info[1],
628 628 info[2],
629 629 self.readUnsignedShort(),
630 630 self.readUnsignedShort(),
631 631 self.readUnsignedShort(),
632 632 self.readName(),
633 633 )
634 634 elif info[0] == _TYPE_HINFO:
635 635 rec = DNSHinfo(
636 636 domain,
637 637 info[0],
638 638 info[1],
639 639 info[2],
640 640 self.readCharacterString(),
641 641 self.readCharacterString(),
642 642 )
643 643 elif info[0] == _TYPE_AAAA:
644 644 rec = DNSAddress(
645 645 domain, info[0], info[1], info[2], self.readString(16)
646 646 )
647 647 else:
648 648 # Try to ignore types we don't know about
649 649 # this may mean the rest of the name is
650 650 # unable to be parsed, and may show errors
651 651 # so this is left for debugging. New types
652 652 # encountered need to be parsed properly.
653 653 #
654 654 # print "UNKNOWN TYPE = " + str(info[0])
655 655 # raise BadTypeInNameException
656 656 self.offset += info[3]
657 657
658 658 if rec is not None:
659 659 self.answers.append(rec)
660 660
661 661 def isQuery(self):
662 662 """Returns true if this is a query"""
663 663 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
664 664
665 665 def isResponse(self):
666 666 """Returns true if this is a response"""
667 667 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
668 668
669 669 def readUTF(self, offset, len):
670 670 """Reads a UTF-8 string of a given length from the packet"""
671 671 return self.data[offset : offset + len].decode('utf-8')
672 672
673 673 def readName(self):
674 674 """Reads a domain name from the packet"""
675 675 result = r''
676 676 off = self.offset
677 677 next = -1
678 678 first = off
679 679
680 680 while True:
681 681 len = ord(self.data[off : off + 1])
682 682 off += 1
683 683 if len == 0:
684 684 break
685 685 t = len & 0xC0
686 686 if t == 0x00:
687 687 result = r''.join((result, self.readUTF(off, len) + r'.'))
688 688 off += len
689 689 elif t == 0xC0:
690 690 if next < 0:
691 691 next = off + 1
692 692 off = ((len & 0x3F) << 8) | ord(self.data[off : off + 1])
693 693 if off >= first:
694 694 raise BadDomainNameCircular(off)
695 695 first = off
696 696 else:
697 697 raise BadDomainName(off)
698 698
699 699 if next >= 0:
700 700 self.offset = next
701 701 else:
702 702 self.offset = off
703 703
704 704 return result
705 705
706 706
707 707 class DNSOutgoing(object):
708 708 """Object representation of an outgoing packet"""
709 709
710 710 def __init__(self, flags, multicast=1):
711 711 self.finished = 0
712 712 self.id = 0
713 713 self.multicast = multicast
714 714 self.flags = flags
715 715 self.names = {}
716 716 self.data = []
717 717 self.size = 12
718 718
719 719 self.questions = []
720 720 self.answers = []
721 721 self.authorities = []
722 722 self.additionals = []
723 723
724 724 def addQuestion(self, record):
725 725 """Adds a question"""
726 726 self.questions.append(record)
727 727
728 728 def addAnswer(self, inp, record):
729 729 """Adds an answer"""
730 730 if not record.suppressedBy(inp):
731 731 self.addAnswerAtTime(record, 0)
732 732
733 733 def addAnswerAtTime(self, record, now):
734 734 """Adds an answer if if does not expire by a certain time"""
735 735 if record is not None:
736 736 if now == 0 or not record.isExpired(now):
737 737 self.answers.append((record, now))
738 738
739 739 def addAuthoritativeAnswer(self, record):
740 740 """Adds an authoritative answer"""
741 741 self.authorities.append(record)
742 742
743 743 def addAdditionalAnswer(self, record):
744 744 """Adds an additional answer"""
745 745 self.additionals.append(record)
746 746
747 747 def writeByte(self, value):
748 748 """Writes a single byte to the packet"""
749 749 format = b'!c'
750 750 self.data.append(struct.pack(format, chr(value)))
751 751 self.size += 1
752 752
753 753 def insertShort(self, index, value):
754 754 """Inserts an unsigned short in a certain position in the packet"""
755 755 format = b'!H'
756 756 self.data.insert(index, struct.pack(format, value))
757 757 self.size += 2
758 758
759 759 def writeShort(self, value):
760 760 """Writes an unsigned short to the packet"""
761 761 format = b'!H'
762 762 self.data.append(struct.pack(format, value))
763 763 self.size += 2
764 764
765 765 def writeInt(self, value):
766 766 """Writes an unsigned integer to the packet"""
767 767 format = b'!I'
768 768 self.data.append(struct.pack(format, int(value)))
769 769 self.size += 4
770 770
771 771 def writeString(self, value, length):
772 772 """Writes a string to the packet"""
773 773 format = b'!' + str(length) + b's'
774 774 self.data.append(struct.pack(format, value))
775 775 self.size += length
776 776
777 777 def writeUTF(self, s):
778 778 """Writes a UTF-8 string of a given length to the packet"""
779 779 utfstr = s.encode('utf-8')
780 780 length = len(utfstr)
781 781 if length > 64:
782 782 raise NamePartTooLongException
783 783 self.writeByte(length)
784 784 self.writeString(utfstr, length)
785 785
786 786 def writeName(self, name):
787 787 """Writes a domain name to the packet"""
788 788
789 789 try:
790 790 # Find existing instance of this name in packet
791 791 #
792 792 index = self.names[name]
793 793 except KeyError:
794 794 # No record of this name already, so write it
795 795 # out as normal, recording the location of the name
796 796 # for future pointers to it.
797 797 #
798 798 self.names[name] = self.size
799 799 parts = name.split(b'.')
800 800 if parts[-1] == b'':
801 801 parts = parts[:-1]
802 802 for part in parts:
803 803 self.writeUTF(part)
804 804 self.writeByte(0)
805 805 return
806 806
807 807 # An index was found, so write a pointer to it
808 808 #
809 809 self.writeByte((index >> 8) | 0xC0)
810 810 self.writeByte(index)
811 811
812 812 def writeQuestion(self, question):
813 813 """Writes a question to the packet"""
814 814 self.writeName(question.name)
815 815 self.writeShort(question.type)
816 816 self.writeShort(question.clazz)
817 817
818 818 def writeRecord(self, record, now):
819 819 """Writes a record (answer, authoritative answer, additional) to
820 820 the packet"""
821 821 self.writeName(record.name)
822 822 self.writeShort(record.type)
823 823 if record.unique and self.multicast:
824 824 self.writeShort(record.clazz | _CLASS_UNIQUE)
825 825 else:
826 826 self.writeShort(record.clazz)
827 827 if now == 0:
828 828 self.writeInt(record.ttl)
829 829 else:
830 830 self.writeInt(record.getRemainingTTL(now))
831 831 index = len(self.data)
832 832 # Adjust size for the short we will write before this record
833 833 #
834 834 self.size += 2
835 835 record.write(self)
836 836 self.size -= 2
837 837
838 838 length = len(b''.join(self.data[index:]))
839 839 self.insertShort(index, length) # Here is the short we adjusted for
840 840
841 841 def packet(self):
842 842 """Returns a string containing the packet's bytes
843 843
844 844 No further parts should be added to the packet once this
845 845 is done."""
846 846 if not self.finished:
847 847 self.finished = 1
848 848 for question in self.questions:
849 849 self.writeQuestion(question)
850 850 for answer, time_ in self.answers:
851 851 self.writeRecord(answer, time_)
852 852 for authority in self.authorities:
853 853 self.writeRecord(authority, 0)
854 854 for additional in self.additionals:
855 855 self.writeRecord(additional, 0)
856 856
857 857 self.insertShort(0, len(self.additionals))
858 858 self.insertShort(0, len(self.authorities))
859 859 self.insertShort(0, len(self.answers))
860 860 self.insertShort(0, len(self.questions))
861 861 self.insertShort(0, self.flags)
862 862 if self.multicast:
863 863 self.insertShort(0, 0)
864 864 else:
865 865 self.insertShort(0, self.id)
866 866 return b''.join(self.data)
867 867
868 868
869 869 class DNSCache(object):
870 870 """A cache of DNS entries"""
871 871
872 872 def __init__(self):
873 873 self.cache = {}
874 874
875 875 def add(self, entry):
876 876 """Adds an entry"""
877 877 try:
878 878 list = self.cache[entry.key]
879 879 except KeyError:
880 880 list = self.cache[entry.key] = []
881 881 list.append(entry)
882 882
883 883 def remove(self, entry):
884 884 """Removes an entry"""
885 885 try:
886 886 list = self.cache[entry.key]
887 887 list.remove(entry)
888 888 except KeyError:
889 889 pass
890 890
891 891 def get(self, entry):
892 892 """Gets an entry by key. Will return None if there is no
893 893 matching entry."""
894 894 try:
895 895 list = self.cache[entry.key]
896 896 return list[list.index(entry)]
897 897 except (KeyError, ValueError):
898 898 return None
899 899
900 900 def getByDetails(self, name, type, clazz):
901 901 """Gets an entry by details. Will return None if there is
902 902 no matching entry."""
903 903 entry = DNSEntry(name, type, clazz)
904 904 return self.get(entry)
905 905
906 906 def entriesWithName(self, name):
907 907 """Returns a list of entries whose key matches the name."""
908 908 try:
909 909 return self.cache[name]
910 910 except KeyError:
911 911 return []
912 912
913 913 def entries(self):
914 914 """Returns a list of all entries"""
915 915 try:
916 916 return list(itertools.chain.from_iterable(self.cache.values()))
917 917 except Exception:
918 918 return []
919 919
920 920
921 921 class Engine(threading.Thread):
922 922 """An engine wraps read access to sockets, allowing objects that
923 923 need to receive data from sockets to be called back when the
924 924 sockets are ready.
925 925
926 926 A reader needs a handle_read() method, which is called when the socket
927 927 it is interested in is ready for reading.
928 928
929 929 Writers are not implemented here, because we only send short
930 930 packets.
931 931 """
932 932
933 933 def __init__(self, zeroconf):
934 934 threading.Thread.__init__(self)
935 935 self.zeroconf = zeroconf
936 936 self.readers = {} # maps socket to reader
937 937 self.timeout = 5
938 938 self.condition = threading.Condition()
939 939 self.start()
940 940
941 941 def run(self):
942 942 while not globals()[b'_GLOBAL_DONE']:
943 943 rs = self.getReaders()
944 944 if len(rs) == 0:
945 945 # No sockets to manage, but we wait for the timeout
946 946 # or addition of a socket
947 947 #
948 948 self.condition.acquire()
949 949 self.condition.wait(self.timeout)
950 950 self.condition.release()
951 951 else:
952 952 try:
953 953 rr, wr, er = select.select(rs, [], [], self.timeout)
954 954 for sock in rr:
955 955 try:
956 956 self.readers[sock].handle_read()
957 957 except Exception:
958 958 if not globals()[b'_GLOBAL_DONE']:
959 959 traceback.print_exc()
960 960 except Exception:
961 961 pass
962 962
963 963 def getReaders(self):
964 964 self.condition.acquire()
965 965 result = self.readers.keys()
966 966 self.condition.release()
967 967 return result
968 968
969 969 def addReader(self, reader, socket):
970 970 self.condition.acquire()
971 971 self.readers[socket] = reader
972 972 self.condition.notify()
973 973 self.condition.release()
974 974
975 975 def delReader(self, socket):
976 976 self.condition.acquire()
977 977 del self.readers[socket]
978 978 self.condition.notify()
979 979 self.condition.release()
980 980
981 981 def notify(self):
982 982 self.condition.acquire()
983 983 self.condition.notify()
984 984 self.condition.release()
985 985
986 986
987 987 class Listener(object):
988 988 """A Listener is used by this module to listen on the multicast
989 989 group to which DNS messages are sent, allowing the implementation
990 990 to cache information as it arrives.
991 991
992 992 It requires registration with an Engine object in order to have
993 993 the read() method called when a socket is available for reading."""
994 994
995 995 def __init__(self, zeroconf):
996 996 self.zeroconf = zeroconf
997 997 self.zeroconf.engine.addReader(self, self.zeroconf.socket)
998 998
999 999 def handle_read(self):
1000 1000 sock = self.zeroconf.socket
1001 1001 try:
1002 1002 data, (addr, port) = sock.recvfrom(_MAX_MSG_ABSOLUTE)
1003 1003 except socket.error as e:
1004 1004 if e.errno == errno.EBADF:
1005 1005 # some other thread may close the socket
1006 1006 return
1007 1007 else:
1008 1008 raise
1009 1009 self.data = data
1010 1010 msg = DNSIncoming(data)
1011 1011 if msg.isQuery():
1012 1012 # Always multicast responses
1013 1013 #
1014 1014 if port == _MDNS_PORT:
1015 1015 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
1016 1016 # If it's not a multicast query, reply via unicast
1017 1017 # and multicast
1018 1018 #
1019 1019 elif port == _DNS_PORT:
1020 1020 self.zeroconf.handleQuery(msg, addr, port)
1021 1021 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
1022 1022 else:
1023 1023 self.zeroconf.handleResponse(msg)
1024 1024
1025 1025
1026 1026 class Reaper(threading.Thread):
1027 1027 """A Reaper is used by this module to remove cache entries that
1028 1028 have expired."""
1029 1029
1030 1030 def __init__(self, zeroconf):
1031 1031 threading.Thread.__init__(self)
1032 1032 self.zeroconf = zeroconf
1033 1033 self.start()
1034 1034
1035 1035 def run(self):
1036 1036 while True:
1037 1037 self.zeroconf.wait(10 * 1000)
1038 1038 if globals()[b'_GLOBAL_DONE']:
1039 1039 return
1040 1040 now = currentTimeMillis()
1041 1041 for record in self.zeroconf.cache.entries():
1042 1042 if record.isExpired(now):
1043 1043 self.zeroconf.updateRecord(now, record)
1044 1044 self.zeroconf.cache.remove(record)
1045 1045
1046 1046
1047 1047 class ServiceBrowser(threading.Thread):
1048 1048 """Used to browse for a service of a specific type.
1049 1049
1050 1050 The listener object will have its addService() and
1051 1051 removeService() methods called when this browser
1052 1052 discovers changes in the services availability."""
1053 1053
1054 1054 def __init__(self, zeroconf, type, listener):
1055 1055 """Creates a browser for a specific type"""
1056 1056 threading.Thread.__init__(self)
1057 1057 self.zeroconf = zeroconf
1058 1058 self.type = type
1059 1059 self.listener = listener
1060 1060 self.services = {}
1061 1061 self.nexttime = currentTimeMillis()
1062 1062 self.delay = _BROWSER_TIME
1063 1063 self.list = []
1064 1064
1065 1065 self.done = 0
1066 1066
1067 1067 self.zeroconf.addListener(
1068 1068 self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN)
1069 1069 )
1070 1070 self.start()
1071 1071
1072 1072 def updateRecord(self, zeroconf, now, record):
1073 1073 """Callback invoked by Zeroconf when new information arrives.
1074 1074
1075 1075 Updates information required by browser in the Zeroconf cache."""
1076 1076 if record.type == _TYPE_PTR and record.name == self.type:
1077 1077 expired = record.isExpired(now)
1078 1078 try:
1079 1079 oldrecord = self.services[record.alias.lower()]
1080 1080 if not expired:
1081 1081 oldrecord.resetTTL(record)
1082 1082 else:
1083 1083 del self.services[record.alias.lower()]
1084 1084 callback = lambda x: self.listener.removeService(
1085 1085 x, self.type, record.alias
1086 1086 )
1087 1087 self.list.append(callback)
1088 1088 return
1089 1089 except Exception:
1090 1090 if not expired:
1091 1091 self.services[record.alias.lower()] = record
1092 1092 callback = lambda x: self.listener.addService(
1093 1093 x, self.type, record.alias
1094 1094 )
1095 1095 self.list.append(callback)
1096 1096
1097 1097 expires = record.getExpirationTime(75)
1098 1098 if expires < self.nexttime:
1099 1099 self.nexttime = expires
1100 1100
1101 1101 def cancel(self):
1102 1102 self.done = 1
1103 1103 self.zeroconf.notifyAll()
1104 1104
1105 1105 def run(self):
1106 1106 while True:
1107 1107 event = None
1108 1108 now = currentTimeMillis()
1109 1109 if len(self.list) == 0 and self.nexttime > now:
1110 1110 self.zeroconf.wait(self.nexttime - now)
1111 1111 if globals()[b'_GLOBAL_DONE'] or self.done:
1112 1112 return
1113 1113 now = currentTimeMillis()
1114 1114
1115 1115 if self.nexttime <= now:
1116 1116 out = DNSOutgoing(_FLAGS_QR_QUERY)
1117 1117 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
1118 1118 for record in self.services.values():
1119 1119 if not record.isExpired(now):
1120 1120 out.addAnswerAtTime(record, now)
1121 1121 self.zeroconf.send(out)
1122 1122 self.nexttime = now + self.delay
1123 1123 self.delay = min(20 * 1000, self.delay * 2)
1124 1124
1125 1125 if len(self.list) > 0:
1126 1126 event = self.list.pop(0)
1127 1127
1128 1128 if event is not None:
1129 1129 event(self.zeroconf)
1130 1130
1131 1131
1132 1132 class ServiceInfo(object):
1133 1133 """Service information"""
1134 1134
1135 1135 def __init__(
1136 1136 self,
1137 1137 type,
1138 1138 name,
1139 1139 address=None,
1140 1140 port=None,
1141 1141 weight=0,
1142 1142 priority=0,
1143 1143 properties=None,
1144 1144 server=None,
1145 1145 ):
1146 1146 """Create a service description.
1147 1147
1148 1148 type: fully qualified service type name
1149 1149 name: fully qualified service name
1150 1150 address: IP address as unsigned short, network byte order
1151 1151 port: port that the service runs on
1152 1152 weight: weight of the service
1153 1153 priority: priority of the service
1154 1154 properties: dictionary of properties (or a string holding the bytes for
1155 1155 the text field)
1156 1156 server: fully qualified name for service host (defaults to name)"""
1157 1157
1158 1158 if not name.endswith(type):
1159 1159 raise BadTypeInNameException
1160 1160 self.type = type
1161 1161 self.name = name
1162 1162 self.address = address
1163 1163 self.port = port
1164 1164 self.weight = weight
1165 1165 self.priority = priority
1166 1166 if server:
1167 1167 self.server = server
1168 1168 else:
1169 1169 self.server = name
1170 1170 self.setProperties(properties)
1171 1171
1172 1172 def setProperties(self, properties):
1173 1173 """Sets properties and text of this info from a dictionary"""
1174 1174 if isinstance(properties, dict):
1175 1175 self.properties = properties
1176 1176 list = []
1177 1177 result = b''
1178 1178 for key in properties:
1179 1179 value = properties[key]
1180 1180 if value is None:
1181 1181 suffix = b''
1182 1182 elif isinstance(value, str):
1183 1183 suffix = value
1184 1184 elif isinstance(value, int):
1185 1185 if value:
1186 1186 suffix = b'true'
1187 1187 else:
1188 1188 suffix = b'false'
1189 1189 else:
1190 1190 suffix = b''
1191 1191 list.append(b'='.join((key, suffix)))
1192 1192 for item in list:
1193 1193 result = b''.join(
1194 (result, struct.pack(b'!c', chr(len(item))), item)
1194 (
1195 result,
1196 struct.pack(b'!c', pycompat.bytechr(len(item))),
1197 item,
1198 )
1195 1199 )
1196 1200 self.text = result
1197 1201 else:
1198 1202 self.text = properties
1199 1203
1200 1204 def setText(self, text):
1201 1205 """Sets properties and text given a text field"""
1202 1206 self.text = text
1203 1207 try:
1204 1208 result = {}
1205 1209 end = len(text)
1206 1210 index = 0
1207 1211 strs = []
1208 1212 while index < end:
1209 1213 length = ord(text[index])
1210 1214 index += 1
1211 1215 strs.append(text[index : index + length])
1212 1216 index += length
1213 1217
1214 1218 for s in strs:
1215 1219 eindex = s.find(b'=')
1216 1220 if eindex == -1:
1217 1221 # No equals sign at all
1218 1222 key = s
1219 1223 value = 0
1220 1224 else:
1221 1225 key = s[:eindex]
1222 1226 value = s[eindex + 1 :]
1223 1227 if value == b'true':
1224 1228 value = 1
1225 1229 elif value == b'false' or not value:
1226 1230 value = 0
1227 1231
1228 1232 # Only update non-existent properties
1229 1233 if key and result.get(key) is None:
1230 1234 result[key] = value
1231 1235
1232 1236 self.properties = result
1233 1237 except Exception:
1234 1238 traceback.print_exc()
1235 1239 self.properties = None
1236 1240
1237 1241 def getType(self):
1238 1242 """Type accessor"""
1239 1243 return self.type
1240 1244
1241 1245 def getName(self):
1242 1246 """Name accessor"""
1243 1247 if self.type is not None and self.name.endswith(b"." + self.type):
1244 1248 return self.name[: len(self.name) - len(self.type) - 1]
1245 1249 return self.name
1246 1250
1247 1251 def getAddress(self):
1248 1252 """Address accessor"""
1249 1253 return self.address
1250 1254
1251 1255 def getPort(self):
1252 1256 """Port accessor"""
1253 1257 return self.port
1254 1258
1255 1259 def getPriority(self):
1256 1260 """Priority accessor"""
1257 1261 return self.priority
1258 1262
1259 1263 def getWeight(self):
1260 1264 """Weight accessor"""
1261 1265 return self.weight
1262 1266
1263 1267 def getProperties(self):
1264 1268 """Properties accessor"""
1265 1269 return self.properties
1266 1270
1267 1271 def getText(self):
1268 1272 """Text accessor"""
1269 1273 return self.text
1270 1274
1271 1275 def getServer(self):
1272 1276 """Server accessor"""
1273 1277 return self.server
1274 1278
1275 1279 def updateRecord(self, zeroconf, now, record):
1276 1280 """Updates service information from a DNS record"""
1277 1281 if record is not None and not record.isExpired(now):
1278 1282 if record.type == _TYPE_A:
1279 1283 # if record.name == self.name:
1280 1284 if record.name == self.server:
1281 1285 self.address = record.address
1282 1286 elif record.type == _TYPE_SRV:
1283 1287 if record.name == self.name:
1284 1288 self.server = record.server
1285 1289 self.port = record.port
1286 1290 self.weight = record.weight
1287 1291 self.priority = record.priority
1288 1292 # self.address = None
1289 1293 self.updateRecord(
1290 1294 zeroconf,
1291 1295 now,
1292 1296 zeroconf.cache.getByDetails(
1293 1297 self.server, _TYPE_A, _CLASS_IN
1294 1298 ),
1295 1299 )
1296 1300 elif record.type == _TYPE_TXT:
1297 1301 if record.name == self.name:
1298 1302 self.setText(record.text)
1299 1303
1300 1304 def request(self, zeroconf, timeout):
1301 1305 """Returns true if the service could be discovered on the
1302 1306 network, and updates this object with details discovered.
1303 1307 """
1304 1308 now = currentTimeMillis()
1305 1309 delay = _LISTENER_TIME
1306 1310 next = now + delay
1307 1311 last = now + timeout
1308 1312 try:
1309 1313 zeroconf.addListener(
1310 1314 self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN)
1311 1315 )
1312 1316 while (
1313 1317 self.server is None or self.address is None or self.text is None
1314 1318 ):
1315 1319 if last <= now:
1316 1320 return 0
1317 1321 if next <= now:
1318 1322 out = DNSOutgoing(_FLAGS_QR_QUERY)
1319 1323 out.addQuestion(
1320 1324 DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN)
1321 1325 )
1322 1326 out.addAnswerAtTime(
1323 1327 zeroconf.cache.getByDetails(
1324 1328 self.name, _TYPE_SRV, _CLASS_IN
1325 1329 ),
1326 1330 now,
1327 1331 )
1328 1332 out.addQuestion(
1329 1333 DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN)
1330 1334 )
1331 1335 out.addAnswerAtTime(
1332 1336 zeroconf.cache.getByDetails(
1333 1337 self.name, _TYPE_TXT, _CLASS_IN
1334 1338 ),
1335 1339 now,
1336 1340 )
1337 1341 if self.server is not None:
1338 1342 out.addQuestion(
1339 1343 DNSQuestion(self.server, _TYPE_A, _CLASS_IN)
1340 1344 )
1341 1345 out.addAnswerAtTime(
1342 1346 zeroconf.cache.getByDetails(
1343 1347 self.server, _TYPE_A, _CLASS_IN
1344 1348 ),
1345 1349 now,
1346 1350 )
1347 1351 zeroconf.send(out)
1348 1352 next = now + delay
1349 1353 delay = delay * 2
1350 1354
1351 1355 zeroconf.wait(min(next, last) - now)
1352 1356 now = currentTimeMillis()
1353 1357 result = 1
1354 1358 finally:
1355 1359 zeroconf.removeListener(self)
1356 1360
1357 1361 return result
1358 1362
1359 1363 def __eq__(self, other):
1360 1364 """Tests equality of service name"""
1361 1365 if isinstance(other, ServiceInfo):
1362 1366 return other.name == self.name
1363 1367 return 0
1364 1368
1365 1369 def __ne__(self, other):
1366 1370 """Non-equality test"""
1367 1371 return not self.__eq__(other)
1368 1372
1369 1373 def __repr__(self):
1370 1374 """String representation"""
1371 1375 result = b"service[%s,%s:%s," % (
1372 1376 self.name,
1373 1377 socket.inet_ntoa(self.getAddress()),
1374 1378 self.port,
1375 1379 )
1376 1380 if self.text is None:
1377 1381 result += b"None"
1378 1382 else:
1379 1383 if len(self.text) < 20:
1380 1384 result += self.text
1381 1385 else:
1382 1386 result += self.text[:17] + b"..."
1383 1387 result += b"]"
1384 1388 return result
1385 1389
1386 1390
1387 1391 class Zeroconf(object):
1388 1392 """Implementation of Zeroconf Multicast DNS Service Discovery
1389 1393
1390 1394 Supports registration, unregistration, queries and browsing.
1391 1395 """
1392 1396
1393 1397 def __init__(self, bindaddress=None):
1394 1398 """Creates an instance of the Zeroconf class, establishing
1395 1399 multicast communications, listening and reaping threads."""
1396 1400 globals()[b'_GLOBAL_DONE'] = 0
1397 1401 if bindaddress is None:
1398 1402 self.intf = socket.gethostbyname(socket.gethostname())
1399 1403 else:
1400 1404 self.intf = bindaddress
1401 1405 self.group = (b'', _MDNS_PORT)
1402 1406 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1403 1407 try:
1404 1408 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1405 1409 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1406 1410 except Exception:
1407 1411 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1408 1412 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1409 1413 # Volume 2"), but some BSD-derived systems require
1410 1414 # SO_REUSEPORT to be specified explicitly. Also, not all
1411 1415 # versions of Python have SO_REUSEPORT available. So
1412 1416 # if you're on a BSD-based system, and haven't upgraded
1413 1417 # to Python 2.3 yet, you may find this library doesn't
1414 1418 # work as expected.
1415 1419 #
1416 1420 pass
1417 1421 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, b"\xff")
1418 1422 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, b"\x01")
1419 1423 try:
1420 1424 self.socket.bind(self.group)
1421 1425 except Exception:
1422 1426 # Some versions of linux raise an exception even though
1423 1427 # SO_REUSEADDR and SO_REUSEPORT have been set, so ignore it
1424 1428 pass
1425 1429 self.socket.setsockopt(
1426 1430 socket.SOL_IP,
1427 1431 socket.IP_ADD_MEMBERSHIP,
1428 1432 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton(r'0.0.0.0'),
1429 1433 )
1430 1434
1431 1435 self.listeners = []
1432 1436 self.browsers = []
1433 1437 self.services = {}
1434 1438 self.servicetypes = {}
1435 1439
1436 1440 self.cache = DNSCache()
1437 1441
1438 1442 self.condition = threading.Condition()
1439 1443
1440 1444 self.engine = Engine(self)
1441 1445 self.listener = Listener(self)
1442 1446 self.reaper = Reaper(self)
1443 1447
1444 1448 def isLoopback(self):
1445 1449 return self.intf.startswith(b"127.0.0.1")
1446 1450
1447 1451 def isLinklocal(self):
1448 1452 return self.intf.startswith(b"169.254.")
1449 1453
1450 1454 def wait(self, timeout):
1451 1455 """Calling thread waits for a given number of milliseconds or
1452 1456 until notified."""
1453 1457 self.condition.acquire()
1454 1458 self.condition.wait(timeout / 1000)
1455 1459 self.condition.release()
1456 1460
1457 1461 def notifyAll(self):
1458 1462 """Notifies all waiting threads"""
1459 1463 self.condition.acquire()
1460 1464 self.condition.notifyAll()
1461 1465 self.condition.release()
1462 1466
1463 1467 def getServiceInfo(self, type, name, timeout=3000):
1464 1468 """Returns network's service information for a particular
1465 1469 name and type, or None if no service matches by the timeout,
1466 1470 which defaults to 3 seconds."""
1467 1471 info = ServiceInfo(type, name)
1468 1472 if info.request(self, timeout):
1469 1473 return info
1470 1474 return None
1471 1475
1472 1476 def addServiceListener(self, type, listener):
1473 1477 """Adds a listener for a particular service type. This object
1474 1478 will then have its updateRecord method called when information
1475 1479 arrives for that type."""
1476 1480 self.removeServiceListener(listener)
1477 1481 self.browsers.append(ServiceBrowser(self, type, listener))
1478 1482
1479 1483 def removeServiceListener(self, listener):
1480 1484 """Removes a listener from the set that is currently listening."""
1481 1485 for browser in self.browsers:
1482 1486 if browser.listener == listener:
1483 1487 browser.cancel()
1484 1488 del browser
1485 1489
1486 1490 def registerService(self, info, ttl=_DNS_TTL):
1487 1491 """Registers service information to the network with a default TTL
1488 1492 of 60 seconds. Zeroconf will then respond to requests for
1489 1493 information for that service. The name of the service may be
1490 1494 changed if needed to make it unique on the network."""
1491 1495 self.checkService(info)
1492 1496 self.services[info.name.lower()] = info
1493 1497 if info.type in self.servicetypes:
1494 1498 self.servicetypes[info.type] += 1
1495 1499 else:
1496 1500 self.servicetypes[info.type] = 1
1497 1501 now = currentTimeMillis()
1498 1502 nexttime = now
1499 1503 i = 0
1500 1504 while i < 3:
1501 1505 if now < nexttime:
1502 1506 self.wait(nexttime - now)
1503 1507 now = currentTimeMillis()
1504 1508 continue
1505 1509 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1506 1510 out.addAnswerAtTime(
1507 1511 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0
1508 1512 )
1509 1513 out.addAnswerAtTime(
1510 1514 DNSService(
1511 1515 info.name,
1512 1516 _TYPE_SRV,
1513 1517 _CLASS_IN,
1514 1518 ttl,
1515 1519 info.priority,
1516 1520 info.weight,
1517 1521 info.port,
1518 1522 info.server,
1519 1523 ),
1520 1524 0,
1521 1525 )
1522 1526 out.addAnswerAtTime(
1523 1527 DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0
1524 1528 )
1525 1529 if info.address:
1526 1530 out.addAnswerAtTime(
1527 1531 DNSAddress(
1528 1532 info.server, _TYPE_A, _CLASS_IN, ttl, info.address
1529 1533 ),
1530 1534 0,
1531 1535 )
1532 1536 self.send(out)
1533 1537 i += 1
1534 1538 nexttime += _REGISTER_TIME
1535 1539
1536 1540 def unregisterService(self, info):
1537 1541 """Unregister a service."""
1538 1542 try:
1539 1543 del self.services[info.name.lower()]
1540 1544 if self.servicetypes[info.type] > 1:
1541 1545 self.servicetypes[info.type] -= 1
1542 1546 else:
1543 1547 del self.servicetypes[info.type]
1544 1548 except KeyError:
1545 1549 pass
1546 1550 now = currentTimeMillis()
1547 1551 nexttime = now
1548 1552 i = 0
1549 1553 while i < 3:
1550 1554 if now < nexttime:
1551 1555 self.wait(nexttime - now)
1552 1556 now = currentTimeMillis()
1553 1557 continue
1554 1558 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1555 1559 out.addAnswerAtTime(
1556 1560 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0
1557 1561 )
1558 1562 out.addAnswerAtTime(
1559 1563 DNSService(
1560 1564 info.name,
1561 1565 _TYPE_SRV,
1562 1566 _CLASS_IN,
1563 1567 0,
1564 1568 info.priority,
1565 1569 info.weight,
1566 1570 info.port,
1567 1571 info.name,
1568 1572 ),
1569 1573 0,
1570 1574 )
1571 1575 out.addAnswerAtTime(
1572 1576 DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0
1573 1577 )
1574 1578 if info.address:
1575 1579 out.addAnswerAtTime(
1576 1580 DNSAddress(
1577 1581 info.server, _TYPE_A, _CLASS_IN, 0, info.address
1578 1582 ),
1579 1583 0,
1580 1584 )
1581 1585 self.send(out)
1582 1586 i += 1
1583 1587 nexttime += _UNREGISTER_TIME
1584 1588
1585 1589 def unregisterAllServices(self):
1586 1590 """Unregister all registered services."""
1587 1591 if len(self.services) > 0:
1588 1592 now = currentTimeMillis()
1589 1593 nexttime = now
1590 1594 i = 0
1591 1595 while i < 3:
1592 1596 if now < nexttime:
1593 1597 self.wait(nexttime - now)
1594 1598 now = currentTimeMillis()
1595 1599 continue
1596 1600 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1597 1601 for info in self.services.values():
1598 1602 out.addAnswerAtTime(
1599 1603 DNSPointer(
1600 1604 info.type, _TYPE_PTR, _CLASS_IN, 0, info.name
1601 1605 ),
1602 1606 0,
1603 1607 )
1604 1608 out.addAnswerAtTime(
1605 1609 DNSService(
1606 1610 info.name,
1607 1611 _TYPE_SRV,
1608 1612 _CLASS_IN,
1609 1613 0,
1610 1614 info.priority,
1611 1615 info.weight,
1612 1616 info.port,
1613 1617 info.server,
1614 1618 ),
1615 1619 0,
1616 1620 )
1617 1621 out.addAnswerAtTime(
1618 1622 DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text),
1619 1623 0,
1620 1624 )
1621 1625 if info.address:
1622 1626 out.addAnswerAtTime(
1623 1627 DNSAddress(
1624 1628 info.server, _TYPE_A, _CLASS_IN, 0, info.address
1625 1629 ),
1626 1630 0,
1627 1631 )
1628 1632 self.send(out)
1629 1633 i += 1
1630 1634 nexttime += _UNREGISTER_TIME
1631 1635
1632 1636 def checkService(self, info):
1633 1637 """Checks the network for a unique service name, modifying the
1634 1638 ServiceInfo passed in if it is not unique."""
1635 1639 now = currentTimeMillis()
1636 1640 nexttime = now
1637 1641 i = 0
1638 1642 while i < 3:
1639 1643 for record in self.cache.entriesWithName(info.type):
1640 1644 if (
1641 1645 record.type == _TYPE_PTR
1642 1646 and not record.isExpired(now)
1643 1647 and record.alias == info.name
1644 1648 ):
1645 1649 if info.name.find(b'.') < 0:
1646 1650 info.name = b"%w.[%s:%d].%s" % (
1647 1651 info.name,
1648 1652 info.address,
1649 1653 info.port,
1650 1654 info.type,
1651 1655 )
1652 1656 self.checkService(info)
1653 1657 return
1654 1658 raise NonUniqueNameException
1655 1659 if now < nexttime:
1656 1660 self.wait(nexttime - now)
1657 1661 now = currentTimeMillis()
1658 1662 continue
1659 1663 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1660 1664 self.debug = out
1661 1665 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1662 1666 out.addAuthoritativeAnswer(
1663 1667 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name)
1664 1668 )
1665 1669 self.send(out)
1666 1670 i += 1
1667 1671 nexttime += _CHECK_TIME
1668 1672
1669 1673 def addListener(self, listener, question):
1670 1674 """Adds a listener for a given question. The listener will have
1671 1675 its updateRecord method called when information is available to
1672 1676 answer the question."""
1673 1677 now = currentTimeMillis()
1674 1678 self.listeners.append(listener)
1675 1679 if question is not None:
1676 1680 for record in self.cache.entriesWithName(question.name):
1677 1681 if question.answeredBy(record) and not record.isExpired(now):
1678 1682 listener.updateRecord(self, now, record)
1679 1683 self.notifyAll()
1680 1684
1681 1685 def removeListener(self, listener):
1682 1686 """Removes a listener."""
1683 1687 try:
1684 1688 self.listeners.remove(listener)
1685 1689 self.notifyAll()
1686 1690 except Exception:
1687 1691 pass
1688 1692
1689 1693 def updateRecord(self, now, rec):
1690 1694 """Used to notify listeners of new information that has updated
1691 1695 a record."""
1692 1696 for listener in self.listeners:
1693 1697 listener.updateRecord(self, now, rec)
1694 1698 self.notifyAll()
1695 1699
1696 1700 def handleResponse(self, msg):
1697 1701 """Deal with incoming response packets. All answers
1698 1702 are held in the cache, and listeners are notified."""
1699 1703 now = currentTimeMillis()
1700 1704 for record in msg.answers:
1701 1705 expired = record.isExpired(now)
1702 1706 if record in self.cache.entries():
1703 1707 if expired:
1704 1708 self.cache.remove(record)
1705 1709 else:
1706 1710 entry = self.cache.get(record)
1707 1711 if entry is not None:
1708 1712 entry.resetTTL(record)
1709 1713 record = entry
1710 1714 else:
1711 1715 self.cache.add(record)
1712 1716
1713 1717 self.updateRecord(now, record)
1714 1718
1715 1719 def handleQuery(self, msg, addr, port):
1716 1720 """Deal with incoming query packets. Provides a response if
1717 1721 possible."""
1718 1722 out = None
1719 1723
1720 1724 # Support unicast client responses
1721 1725 #
1722 1726 if port != _MDNS_PORT:
1723 1727 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
1724 1728 for question in msg.questions:
1725 1729 out.addQuestion(question)
1726 1730
1727 1731 for question in msg.questions:
1728 1732 if question.type == _TYPE_PTR:
1729 1733 if question.name == b"_services._dns-sd._udp.local.":
1730 1734 for stype in self.servicetypes.keys():
1731 1735 if out is None:
1732 1736 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1733 1737 out.addAnswer(
1734 1738 msg,
1735 1739 DNSPointer(
1736 1740 b"_services._dns-sd._udp.local.",
1737 1741 _TYPE_PTR,
1738 1742 _CLASS_IN,
1739 1743 _DNS_TTL,
1740 1744 stype,
1741 1745 ),
1742 1746 )
1743 1747 for service in self.services.values():
1744 1748 if question.name == service.type:
1745 1749 if out is None:
1746 1750 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1747 1751 out.addAnswer(
1748 1752 msg,
1749 1753 DNSPointer(
1750 1754 service.type,
1751 1755 _TYPE_PTR,
1752 1756 _CLASS_IN,
1753 1757 _DNS_TTL,
1754 1758 service.name,
1755 1759 ),
1756 1760 )
1757 1761 else:
1758 1762 try:
1759 1763 if out is None:
1760 1764 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1761 1765
1762 1766 # Answer A record queries for any service addresses we know
1763 1767 if question.type == _TYPE_A or question.type == _TYPE_ANY:
1764 1768 for service in self.services.values():
1765 1769 if service.server == question.name.lower():
1766 1770 out.addAnswer(
1767 1771 msg,
1768 1772 DNSAddress(
1769 1773 question.name,
1770 1774 _TYPE_A,
1771 1775 _CLASS_IN | _CLASS_UNIQUE,
1772 1776 _DNS_TTL,
1773 1777 service.address,
1774 1778 ),
1775 1779 )
1776 1780
1777 1781 service = self.services.get(question.name.lower(), None)
1778 1782 if not service:
1779 1783 continue
1780 1784
1781 1785 if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
1782 1786 out.addAnswer(
1783 1787 msg,
1784 1788 DNSService(
1785 1789 question.name,
1786 1790 _TYPE_SRV,
1787 1791 _CLASS_IN | _CLASS_UNIQUE,
1788 1792 _DNS_TTL,
1789 1793 service.priority,
1790 1794 service.weight,
1791 1795 service.port,
1792 1796 service.server,
1793 1797 ),
1794 1798 )
1795 1799 if question.type == _TYPE_TXT or question.type == _TYPE_ANY:
1796 1800 out.addAnswer(
1797 1801 msg,
1798 1802 DNSText(
1799 1803 question.name,
1800 1804 _TYPE_TXT,
1801 1805 _CLASS_IN | _CLASS_UNIQUE,
1802 1806 _DNS_TTL,
1803 1807 service.text,
1804 1808 ),
1805 1809 )
1806 1810 if question.type == _TYPE_SRV:
1807 1811 out.addAdditionalAnswer(
1808 1812 DNSAddress(
1809 1813 service.server,
1810 1814 _TYPE_A,
1811 1815 _CLASS_IN | _CLASS_UNIQUE,
1812 1816 _DNS_TTL,
1813 1817 service.address,
1814 1818 )
1815 1819 )
1816 1820 except Exception:
1817 1821 traceback.print_exc()
1818 1822
1819 1823 if out is not None and out.answers:
1820 1824 out.id = msg.id
1821 1825 self.send(out, addr, port)
1822 1826
1823 1827 def send(self, out, addr=_MDNS_ADDR, port=_MDNS_PORT):
1824 1828 """Sends an outgoing packet."""
1825 1829 # This is a quick test to see if we can parse the packets we generate
1826 1830 # temp = DNSIncoming(out.packet())
1827 1831 try:
1828 1832 self.socket.sendto(out.packet(), 0, (addr, port))
1829 1833 except Exception:
1830 1834 # Ignore this, it may be a temporary loss of network connection
1831 1835 pass
1832 1836
1833 1837 def close(self):
1834 1838 """Ends the background threads, and prevent this instance from
1835 1839 servicing further queries."""
1836 1840 if globals()[b'_GLOBAL_DONE'] == 0:
1837 1841 globals()[b'_GLOBAL_DONE'] = 1
1838 1842 self.notifyAll()
1839 1843 self.engine.notify()
1840 1844 self.unregisterAllServices()
1841 1845 self.socket.setsockopt(
1842 1846 socket.SOL_IP,
1843 1847 socket.IP_DROP_MEMBERSHIP,
1844 1848 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton(r'0.0.0.0'),
1845 1849 )
1846 1850 self.socket.close()
1847 1851
1848 1852
1849 1853 # Test a few module features, including service registration, service
1850 1854 # query (for Zoe), and service unregistration.
1851 1855
1852 1856 if __name__ == '__main__':
1853 1857 print(b"Multicast DNS Service Discovery for Python, version", __version__)
1854 1858 r = Zeroconf()
1855 1859 print(b"1. Testing registration of a service...")
1856 1860 desc = {b'version': b'0.10', b'a': b'test value', b'b': b'another value'}
1857 1861 info = ServiceInfo(
1858 1862 b"_http._tcp.local.",
1859 1863 b"My Service Name._http._tcp.local.",
1860 1864 socket.inet_aton(b"127.0.0.1"),
1861 1865 1234,
1862 1866 0,
1863 1867 0,
1864 1868 desc,
1865 1869 )
1866 1870 print(b" Registering service...")
1867 1871 r.registerService(info)
1868 1872 print(b" Registration done.")
1869 1873 print(b"2. Testing query of service information...")
1870 1874 print(
1871 1875 b" Getting ZOE service:",
1872 1876 str(r.getServiceInfo(b"_http._tcp.local.", b"ZOE._http._tcp.local.")),
1873 1877 )
1874 1878 print(b" Query done.")
1875 1879 print(b"3. Testing query of own service...")
1876 1880 print(
1877 1881 b" Getting self:",
1878 1882 str(
1879 1883 r.getServiceInfo(
1880 1884 b"_http._tcp.local.", b"My Service Name._http._tcp.local."
1881 1885 )
1882 1886 ),
1883 1887 )
1884 1888 print(b" Query done.")
1885 1889 print(b"4. Testing unregister of service information...")
1886 1890 r.unregisterService(info)
1887 1891 print(b" Unregister done.")
1888 1892 r.close()
General Comments 0
You need to be logged in to leave comments. Login now