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