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