##// END OF EJS Templates
zeroconf: fix an issue concatenating bytes and str...
Matt Harbison -
r48678:ad2c3707 stable
parent child Browse files
Show More
@@ -1,1892 +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 = ''.join((result, self.readUTF(off, len) + '.'))
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 format = b'!' + str(length) + b's'
773 format = '!' + str(length) + '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 1194 (
1195 1195 result,
1196 1196 struct.pack(b'!c', pycompat.bytechr(len(item))),
1197 1197 item,
1198 1198 )
1199 1199 )
1200 1200 self.text = result
1201 1201 else:
1202 1202 self.text = properties
1203 1203
1204 1204 def setText(self, text):
1205 1205 """Sets properties and text given a text field"""
1206 1206 self.text = text
1207 1207 try:
1208 1208 result = {}
1209 1209 end = len(text)
1210 1210 index = 0
1211 1211 strs = []
1212 1212 while index < end:
1213 1213 length = ord(text[index])
1214 1214 index += 1
1215 1215 strs.append(text[index : index + length])
1216 1216 index += length
1217 1217
1218 1218 for s in strs:
1219 1219 eindex = s.find(b'=')
1220 1220 if eindex == -1:
1221 1221 # No equals sign at all
1222 1222 key = s
1223 1223 value = 0
1224 1224 else:
1225 1225 key = s[:eindex]
1226 1226 value = s[eindex + 1 :]
1227 1227 if value == b'true':
1228 1228 value = 1
1229 1229 elif value == b'false' or not value:
1230 1230 value = 0
1231 1231
1232 1232 # Only update non-existent properties
1233 1233 if key and result.get(key) is None:
1234 1234 result[key] = value
1235 1235
1236 1236 self.properties = result
1237 1237 except Exception:
1238 1238 traceback.print_exc()
1239 1239 self.properties = None
1240 1240
1241 1241 def getType(self):
1242 1242 """Type accessor"""
1243 1243 return self.type
1244 1244
1245 1245 def getName(self):
1246 1246 """Name accessor"""
1247 1247 if self.type is not None and self.name.endswith(b"." + self.type):
1248 1248 return self.name[: len(self.name) - len(self.type) - 1]
1249 1249 return self.name
1250 1250
1251 1251 def getAddress(self):
1252 1252 """Address accessor"""
1253 1253 return self.address
1254 1254
1255 1255 def getPort(self):
1256 1256 """Port accessor"""
1257 1257 return self.port
1258 1258
1259 1259 def getPriority(self):
1260 1260 """Priority accessor"""
1261 1261 return self.priority
1262 1262
1263 1263 def getWeight(self):
1264 1264 """Weight accessor"""
1265 1265 return self.weight
1266 1266
1267 1267 def getProperties(self):
1268 1268 """Properties accessor"""
1269 1269 return self.properties
1270 1270
1271 1271 def getText(self):
1272 1272 """Text accessor"""
1273 1273 return self.text
1274 1274
1275 1275 def getServer(self):
1276 1276 """Server accessor"""
1277 1277 return self.server
1278 1278
1279 1279 def updateRecord(self, zeroconf, now, record):
1280 1280 """Updates service information from a DNS record"""
1281 1281 if record is not None and not record.isExpired(now):
1282 1282 if record.type == _TYPE_A:
1283 1283 # if record.name == self.name:
1284 1284 if record.name == self.server:
1285 1285 self.address = record.address
1286 1286 elif record.type == _TYPE_SRV:
1287 1287 if record.name == self.name:
1288 1288 self.server = record.server
1289 1289 self.port = record.port
1290 1290 self.weight = record.weight
1291 1291 self.priority = record.priority
1292 1292 # self.address = None
1293 1293 self.updateRecord(
1294 1294 zeroconf,
1295 1295 now,
1296 1296 zeroconf.cache.getByDetails(
1297 1297 self.server, _TYPE_A, _CLASS_IN
1298 1298 ),
1299 1299 )
1300 1300 elif record.type == _TYPE_TXT:
1301 1301 if record.name == self.name:
1302 1302 self.setText(record.text)
1303 1303
1304 1304 def request(self, zeroconf, timeout):
1305 1305 """Returns true if the service could be discovered on the
1306 1306 network, and updates this object with details discovered.
1307 1307 """
1308 1308 now = currentTimeMillis()
1309 1309 delay = _LISTENER_TIME
1310 1310 next = now + delay
1311 1311 last = now + timeout
1312 1312 try:
1313 1313 zeroconf.addListener(
1314 1314 self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN)
1315 1315 )
1316 1316 while (
1317 1317 self.server is None or self.address is None or self.text is None
1318 1318 ):
1319 1319 if last <= now:
1320 1320 return 0
1321 1321 if next <= now:
1322 1322 out = DNSOutgoing(_FLAGS_QR_QUERY)
1323 1323 out.addQuestion(
1324 1324 DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN)
1325 1325 )
1326 1326 out.addAnswerAtTime(
1327 1327 zeroconf.cache.getByDetails(
1328 1328 self.name, _TYPE_SRV, _CLASS_IN
1329 1329 ),
1330 1330 now,
1331 1331 )
1332 1332 out.addQuestion(
1333 1333 DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN)
1334 1334 )
1335 1335 out.addAnswerAtTime(
1336 1336 zeroconf.cache.getByDetails(
1337 1337 self.name, _TYPE_TXT, _CLASS_IN
1338 1338 ),
1339 1339 now,
1340 1340 )
1341 1341 if self.server is not None:
1342 1342 out.addQuestion(
1343 1343 DNSQuestion(self.server, _TYPE_A, _CLASS_IN)
1344 1344 )
1345 1345 out.addAnswerAtTime(
1346 1346 zeroconf.cache.getByDetails(
1347 1347 self.server, _TYPE_A, _CLASS_IN
1348 1348 ),
1349 1349 now,
1350 1350 )
1351 1351 zeroconf.send(out)
1352 1352 next = now + delay
1353 1353 delay = delay * 2
1354 1354
1355 1355 zeroconf.wait(min(next, last) - now)
1356 1356 now = currentTimeMillis()
1357 1357 result = 1
1358 1358 finally:
1359 1359 zeroconf.removeListener(self)
1360 1360
1361 1361 return result
1362 1362
1363 1363 def __eq__(self, other):
1364 1364 """Tests equality of service name"""
1365 1365 if isinstance(other, ServiceInfo):
1366 1366 return other.name == self.name
1367 1367 return 0
1368 1368
1369 1369 def __ne__(self, other):
1370 1370 """Non-equality test"""
1371 1371 return not self.__eq__(other)
1372 1372
1373 1373 def __repr__(self):
1374 1374 """String representation"""
1375 1375 result = b"service[%s,%s:%s," % (
1376 1376 self.name,
1377 1377 socket.inet_ntoa(self.getAddress()),
1378 1378 self.port,
1379 1379 )
1380 1380 if self.text is None:
1381 1381 result += b"None"
1382 1382 else:
1383 1383 if len(self.text) < 20:
1384 1384 result += self.text
1385 1385 else:
1386 1386 result += self.text[:17] + b"..."
1387 1387 result += b"]"
1388 1388 return result
1389 1389
1390 1390
1391 1391 class Zeroconf(object):
1392 1392 """Implementation of Zeroconf Multicast DNS Service Discovery
1393 1393
1394 1394 Supports registration, unregistration, queries and browsing.
1395 1395 """
1396 1396
1397 1397 def __init__(self, bindaddress=None):
1398 1398 """Creates an instance of the Zeroconf class, establishing
1399 1399 multicast communications, listening and reaping threads."""
1400 1400 globals()[b'_GLOBAL_DONE'] = 0
1401 1401 if bindaddress is None:
1402 1402 self.intf = socket.gethostbyname(socket.gethostname())
1403 1403 else:
1404 1404 self.intf = bindaddress
1405 1405 self.group = (b'', _MDNS_PORT)
1406 1406 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1407 1407 try:
1408 1408 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1409 1409 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1410 1410 except Exception:
1411 1411 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1412 1412 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1413 1413 # Volume 2"), but some BSD-derived systems require
1414 1414 # SO_REUSEPORT to be specified explicitly. Also, not all
1415 1415 # versions of Python have SO_REUSEPORT available. So
1416 1416 # if you're on a BSD-based system, and haven't upgraded
1417 1417 # to Python 2.3 yet, you may find this library doesn't
1418 1418 # work as expected.
1419 1419 #
1420 1420 pass
1421 1421 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, b"\xff")
1422 1422 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, b"\x01")
1423 1423 try:
1424 1424 self.socket.bind(self.group)
1425 1425 except Exception:
1426 1426 # Some versions of linux raise an exception even though
1427 1427 # SO_REUSEADDR and SO_REUSEPORT have been set, so ignore it
1428 1428 pass
1429 1429 self.socket.setsockopt(
1430 1430 socket.SOL_IP,
1431 1431 socket.IP_ADD_MEMBERSHIP,
1432 1432 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'),
1433 1433 )
1434 1434
1435 1435 self.listeners = []
1436 1436 self.browsers = []
1437 1437 self.services = {}
1438 1438 self.servicetypes = {}
1439 1439
1440 1440 self.cache = DNSCache()
1441 1441
1442 1442 self.condition = threading.Condition()
1443 1443
1444 1444 self.engine = Engine(self)
1445 1445 self.listener = Listener(self)
1446 1446 self.reaper = Reaper(self)
1447 1447
1448 1448 def isLoopback(self):
1449 1449 return self.intf.startswith(b"127.0.0.1")
1450 1450
1451 1451 def isLinklocal(self):
1452 1452 return self.intf.startswith(b"169.254.")
1453 1453
1454 1454 def wait(self, timeout):
1455 1455 """Calling thread waits for a given number of milliseconds or
1456 1456 until notified."""
1457 1457 self.condition.acquire()
1458 1458 self.condition.wait(timeout / 1000)
1459 1459 self.condition.release()
1460 1460
1461 1461 def notifyAll(self):
1462 1462 """Notifies all waiting threads"""
1463 1463 self.condition.acquire()
1464 1464 self.condition.notifyAll()
1465 1465 self.condition.release()
1466 1466
1467 1467 def getServiceInfo(self, type, name, timeout=3000):
1468 1468 """Returns network's service information for a particular
1469 1469 name and type, or None if no service matches by the timeout,
1470 1470 which defaults to 3 seconds."""
1471 1471 info = ServiceInfo(type, name)
1472 1472 if info.request(self, timeout):
1473 1473 return info
1474 1474 return None
1475 1475
1476 1476 def addServiceListener(self, type, listener):
1477 1477 """Adds a listener for a particular service type. This object
1478 1478 will then have its updateRecord method called when information
1479 1479 arrives for that type."""
1480 1480 self.removeServiceListener(listener)
1481 1481 self.browsers.append(ServiceBrowser(self, type, listener))
1482 1482
1483 1483 def removeServiceListener(self, listener):
1484 1484 """Removes a listener from the set that is currently listening."""
1485 1485 for browser in self.browsers:
1486 1486 if browser.listener == listener:
1487 1487 browser.cancel()
1488 1488 del browser
1489 1489
1490 1490 def registerService(self, info, ttl=_DNS_TTL):
1491 1491 """Registers service information to the network with a default TTL
1492 1492 of 60 seconds. Zeroconf will then respond to requests for
1493 1493 information for that service. The name of the service may be
1494 1494 changed if needed to make it unique on the network."""
1495 1495 self.checkService(info)
1496 1496 self.services[info.name.lower()] = info
1497 1497 if info.type in self.servicetypes:
1498 1498 self.servicetypes[info.type] += 1
1499 1499 else:
1500 1500 self.servicetypes[info.type] = 1
1501 1501 now = currentTimeMillis()
1502 1502 nexttime = now
1503 1503 i = 0
1504 1504 while i < 3:
1505 1505 if now < nexttime:
1506 1506 self.wait(nexttime - now)
1507 1507 now = currentTimeMillis()
1508 1508 continue
1509 1509 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1510 1510 out.addAnswerAtTime(
1511 1511 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0
1512 1512 )
1513 1513 out.addAnswerAtTime(
1514 1514 DNSService(
1515 1515 info.name,
1516 1516 _TYPE_SRV,
1517 1517 _CLASS_IN,
1518 1518 ttl,
1519 1519 info.priority,
1520 1520 info.weight,
1521 1521 info.port,
1522 1522 info.server,
1523 1523 ),
1524 1524 0,
1525 1525 )
1526 1526 out.addAnswerAtTime(
1527 1527 DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0
1528 1528 )
1529 1529 if info.address:
1530 1530 out.addAnswerAtTime(
1531 1531 DNSAddress(
1532 1532 info.server, _TYPE_A, _CLASS_IN, ttl, info.address
1533 1533 ),
1534 1534 0,
1535 1535 )
1536 1536 self.send(out)
1537 1537 i += 1
1538 1538 nexttime += _REGISTER_TIME
1539 1539
1540 1540 def unregisterService(self, info):
1541 1541 """Unregister a service."""
1542 1542 try:
1543 1543 del self.services[info.name.lower()]
1544 1544 if self.servicetypes[info.type] > 1:
1545 1545 self.servicetypes[info.type] -= 1
1546 1546 else:
1547 1547 del self.servicetypes[info.type]
1548 1548 except KeyError:
1549 1549 pass
1550 1550 now = currentTimeMillis()
1551 1551 nexttime = now
1552 1552 i = 0
1553 1553 while i < 3:
1554 1554 if now < nexttime:
1555 1555 self.wait(nexttime - now)
1556 1556 now = currentTimeMillis()
1557 1557 continue
1558 1558 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1559 1559 out.addAnswerAtTime(
1560 1560 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0
1561 1561 )
1562 1562 out.addAnswerAtTime(
1563 1563 DNSService(
1564 1564 info.name,
1565 1565 _TYPE_SRV,
1566 1566 _CLASS_IN,
1567 1567 0,
1568 1568 info.priority,
1569 1569 info.weight,
1570 1570 info.port,
1571 1571 info.name,
1572 1572 ),
1573 1573 0,
1574 1574 )
1575 1575 out.addAnswerAtTime(
1576 1576 DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0
1577 1577 )
1578 1578 if info.address:
1579 1579 out.addAnswerAtTime(
1580 1580 DNSAddress(
1581 1581 info.server, _TYPE_A, _CLASS_IN, 0, info.address
1582 1582 ),
1583 1583 0,
1584 1584 )
1585 1585 self.send(out)
1586 1586 i += 1
1587 1587 nexttime += _UNREGISTER_TIME
1588 1588
1589 1589 def unregisterAllServices(self):
1590 1590 """Unregister all registered services."""
1591 1591 if len(self.services) > 0:
1592 1592 now = currentTimeMillis()
1593 1593 nexttime = now
1594 1594 i = 0
1595 1595 while i < 3:
1596 1596 if now < nexttime:
1597 1597 self.wait(nexttime - now)
1598 1598 now = currentTimeMillis()
1599 1599 continue
1600 1600 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1601 1601 for info in self.services.values():
1602 1602 out.addAnswerAtTime(
1603 1603 DNSPointer(
1604 1604 info.type, _TYPE_PTR, _CLASS_IN, 0, info.name
1605 1605 ),
1606 1606 0,
1607 1607 )
1608 1608 out.addAnswerAtTime(
1609 1609 DNSService(
1610 1610 info.name,
1611 1611 _TYPE_SRV,
1612 1612 _CLASS_IN,
1613 1613 0,
1614 1614 info.priority,
1615 1615 info.weight,
1616 1616 info.port,
1617 1617 info.server,
1618 1618 ),
1619 1619 0,
1620 1620 )
1621 1621 out.addAnswerAtTime(
1622 1622 DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text),
1623 1623 0,
1624 1624 )
1625 1625 if info.address:
1626 1626 out.addAnswerAtTime(
1627 1627 DNSAddress(
1628 1628 info.server, _TYPE_A, _CLASS_IN, 0, info.address
1629 1629 ),
1630 1630 0,
1631 1631 )
1632 1632 self.send(out)
1633 1633 i += 1
1634 1634 nexttime += _UNREGISTER_TIME
1635 1635
1636 1636 def checkService(self, info):
1637 1637 """Checks the network for a unique service name, modifying the
1638 1638 ServiceInfo passed in if it is not unique."""
1639 1639 now = currentTimeMillis()
1640 1640 nexttime = now
1641 1641 i = 0
1642 1642 while i < 3:
1643 1643 for record in self.cache.entriesWithName(info.type):
1644 1644 if (
1645 1645 record.type == _TYPE_PTR
1646 1646 and not record.isExpired(now)
1647 1647 and record.alias == info.name
1648 1648 ):
1649 1649 if info.name.find(b'.') < 0:
1650 1650 info.name = b"%s.[%s:%d].%s" % (
1651 1651 info.name,
1652 1652 info.address,
1653 1653 info.port,
1654 1654 info.type,
1655 1655 )
1656 1656 self.checkService(info)
1657 1657 return
1658 1658 raise NonUniqueNameException
1659 1659 if now < nexttime:
1660 1660 self.wait(nexttime - now)
1661 1661 now = currentTimeMillis()
1662 1662 continue
1663 1663 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1664 1664 self.debug = out
1665 1665 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1666 1666 out.addAuthoritativeAnswer(
1667 1667 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name)
1668 1668 )
1669 1669 self.send(out)
1670 1670 i += 1
1671 1671 nexttime += _CHECK_TIME
1672 1672
1673 1673 def addListener(self, listener, question):
1674 1674 """Adds a listener for a given question. The listener will have
1675 1675 its updateRecord method called when information is available to
1676 1676 answer the question."""
1677 1677 now = currentTimeMillis()
1678 1678 self.listeners.append(listener)
1679 1679 if question is not None:
1680 1680 for record in self.cache.entriesWithName(question.name):
1681 1681 if question.answeredBy(record) and not record.isExpired(now):
1682 1682 listener.updateRecord(self, now, record)
1683 1683 self.notifyAll()
1684 1684
1685 1685 def removeListener(self, listener):
1686 1686 """Removes a listener."""
1687 1687 try:
1688 1688 self.listeners.remove(listener)
1689 1689 self.notifyAll()
1690 1690 except Exception:
1691 1691 pass
1692 1692
1693 1693 def updateRecord(self, now, rec):
1694 1694 """Used to notify listeners of new information that has updated
1695 1695 a record."""
1696 1696 for listener in self.listeners:
1697 1697 listener.updateRecord(self, now, rec)
1698 1698 self.notifyAll()
1699 1699
1700 1700 def handleResponse(self, msg):
1701 1701 """Deal with incoming response packets. All answers
1702 1702 are held in the cache, and listeners are notified."""
1703 1703 now = currentTimeMillis()
1704 1704 for record in msg.answers:
1705 1705 expired = record.isExpired(now)
1706 1706 if record in self.cache.entries():
1707 1707 if expired:
1708 1708 self.cache.remove(record)
1709 1709 else:
1710 1710 entry = self.cache.get(record)
1711 1711 if entry is not None:
1712 1712 entry.resetTTL(record)
1713 1713 record = entry
1714 1714 else:
1715 1715 self.cache.add(record)
1716 1716
1717 1717 self.updateRecord(now, record)
1718 1718
1719 1719 def handleQuery(self, msg, addr, port):
1720 1720 """Deal with incoming query packets. Provides a response if
1721 1721 possible."""
1722 1722 out = None
1723 1723
1724 1724 # Support unicast client responses
1725 1725 #
1726 1726 if port != _MDNS_PORT:
1727 1727 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
1728 1728 for question in msg.questions:
1729 1729 out.addQuestion(question)
1730 1730
1731 1731 for question in msg.questions:
1732 1732 if question.type == _TYPE_PTR:
1733 1733 if question.name == b"_services._dns-sd._udp.local.":
1734 1734 for stype in self.servicetypes.keys():
1735 1735 if out is None:
1736 1736 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1737 1737 out.addAnswer(
1738 1738 msg,
1739 1739 DNSPointer(
1740 1740 b"_services._dns-sd._udp.local.",
1741 1741 _TYPE_PTR,
1742 1742 _CLASS_IN,
1743 1743 _DNS_TTL,
1744 1744 stype,
1745 1745 ),
1746 1746 )
1747 1747 for service in self.services.values():
1748 1748 if question.name == service.type:
1749 1749 if out is None:
1750 1750 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1751 1751 out.addAnswer(
1752 1752 msg,
1753 1753 DNSPointer(
1754 1754 service.type,
1755 1755 _TYPE_PTR,
1756 1756 _CLASS_IN,
1757 1757 _DNS_TTL,
1758 1758 service.name,
1759 1759 ),
1760 1760 )
1761 1761 else:
1762 1762 try:
1763 1763 if out is None:
1764 1764 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1765 1765
1766 1766 # Answer A record queries for any service addresses we know
1767 1767 if question.type == _TYPE_A or question.type == _TYPE_ANY:
1768 1768 for service in self.services.values():
1769 1769 if service.server == question.name.lower():
1770 1770 out.addAnswer(
1771 1771 msg,
1772 1772 DNSAddress(
1773 1773 question.name,
1774 1774 _TYPE_A,
1775 1775 _CLASS_IN | _CLASS_UNIQUE,
1776 1776 _DNS_TTL,
1777 1777 service.address,
1778 1778 ),
1779 1779 )
1780 1780
1781 1781 service = self.services.get(question.name.lower(), None)
1782 1782 if not service:
1783 1783 continue
1784 1784
1785 1785 if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
1786 1786 out.addAnswer(
1787 1787 msg,
1788 1788 DNSService(
1789 1789 question.name,
1790 1790 _TYPE_SRV,
1791 1791 _CLASS_IN | _CLASS_UNIQUE,
1792 1792 _DNS_TTL,
1793 1793 service.priority,
1794 1794 service.weight,
1795 1795 service.port,
1796 1796 service.server,
1797 1797 ),
1798 1798 )
1799 1799 if question.type == _TYPE_TXT or question.type == _TYPE_ANY:
1800 1800 out.addAnswer(
1801 1801 msg,
1802 1802 DNSText(
1803 1803 question.name,
1804 1804 _TYPE_TXT,
1805 1805 _CLASS_IN | _CLASS_UNIQUE,
1806 1806 _DNS_TTL,
1807 1807 service.text,
1808 1808 ),
1809 1809 )
1810 1810 if question.type == _TYPE_SRV:
1811 1811 out.addAdditionalAnswer(
1812 1812 DNSAddress(
1813 1813 service.server,
1814 1814 _TYPE_A,
1815 1815 _CLASS_IN | _CLASS_UNIQUE,
1816 1816 _DNS_TTL,
1817 1817 service.address,
1818 1818 )
1819 1819 )
1820 1820 except Exception:
1821 1821 traceback.print_exc()
1822 1822
1823 1823 if out is not None and out.answers:
1824 1824 out.id = msg.id
1825 1825 self.send(out, addr, port)
1826 1826
1827 1827 def send(self, out, addr=_MDNS_ADDR, port=_MDNS_PORT):
1828 1828 """Sends an outgoing packet."""
1829 1829 # This is a quick test to see if we can parse the packets we generate
1830 1830 # temp = DNSIncoming(out.packet())
1831 1831 try:
1832 1832 self.socket.sendto(out.packet(), 0, (addr, port))
1833 1833 except Exception:
1834 1834 # Ignore this, it may be a temporary loss of network connection
1835 1835 pass
1836 1836
1837 1837 def close(self):
1838 1838 """Ends the background threads, and prevent this instance from
1839 1839 servicing further queries."""
1840 1840 if globals()[b'_GLOBAL_DONE'] == 0:
1841 1841 globals()[b'_GLOBAL_DONE'] = 1
1842 1842 self.notifyAll()
1843 1843 self.engine.notify()
1844 1844 self.unregisterAllServices()
1845 1845 self.socket.setsockopt(
1846 1846 socket.SOL_IP,
1847 1847 socket.IP_DROP_MEMBERSHIP,
1848 1848 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'),
1849 1849 )
1850 1850 self.socket.close()
1851 1851
1852 1852
1853 1853 # Test a few module features, including service registration, service
1854 1854 # query (for Zoe), and service unregistration.
1855 1855
1856 1856 if __name__ == '__main__':
1857 1857 print(b"Multicast DNS Service Discovery for Python, version", __version__)
1858 1858 r = Zeroconf()
1859 1859 print(b"1. Testing registration of a service...")
1860 1860 desc = {b'version': b'0.10', b'a': b'test value', b'b': b'another value'}
1861 1861 info = ServiceInfo(
1862 1862 b"_http._tcp.local.",
1863 1863 b"My Service Name._http._tcp.local.",
1864 1864 socket.inet_aton(b"127.0.0.1"),
1865 1865 1234,
1866 1866 0,
1867 1867 0,
1868 1868 desc,
1869 1869 )
1870 1870 print(b" Registering service...")
1871 1871 r.registerService(info)
1872 1872 print(b" Registration done.")
1873 1873 print(b"2. Testing query of service information...")
1874 1874 print(
1875 1875 b" Getting ZOE service:",
1876 1876 str(r.getServiceInfo(b"_http._tcp.local.", b"ZOE._http._tcp.local.")),
1877 1877 )
1878 1878 print(b" Query done.")
1879 1879 print(b"3. Testing query of own service...")
1880 1880 print(
1881 1881 b" Getting self:",
1882 1882 str(
1883 1883 r.getServiceInfo(
1884 1884 b"_http._tcp.local.", b"My Service Name._http._tcp.local."
1885 1885 )
1886 1886 ),
1887 1887 )
1888 1888 print(b" Query done.")
1889 1889 print(b"4. Testing unregister of service information...")
1890 1890 r.unregisterService(info)
1891 1891 print(b" Unregister done.")
1892 1892 r.close()
General Comments 0
You need to be logged in to leave comments. Login now