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