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