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