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