##// END OF EJS Templates
zeroconf: use absolute_import
timeless -
r28296:a73394e7 default
parent child Browse files
Show More
@@ -1,1582 +1,1582
1 from __future__ import print_function
1 from __future__ import absolute_import, print_function
2
2
3 """ Multicast DNS Service Discovery for Python, v0.12
3 """ Multicast DNS Service Discovery for Python, v0.12
4 Copyright (C) 2003, Paul Scott-Murphy
4 Copyright (C) 2003, Paul Scott-Murphy
5
5
6 This module provides a framework for the use of DNS Service Discovery
6 This module provides a framework for the use of DNS Service Discovery
7 using IP multicast. It has been tested against the JRendezvous
7 using IP multicast. It has been tested against the JRendezvous
8 implementation from <a href="http://strangeberry.com">StrangeBerry</a>,
8 implementation from <a href="http://strangeberry.com">StrangeBerry</a>,
9 and against the mDNSResponder from Mac OS X 10.3.8.
9 and against the mDNSResponder from Mac OS X 10.3.8.
10
10
11 This library is free software; you can redistribute it and/or
11 This library is free software; you can redistribute it and/or
12 modify it under the terms of the GNU Lesser General Public
12 modify it under the terms of the GNU Lesser General Public
13 License as published by the Free Software Foundation; either
13 License as published by the Free Software Foundation; either
14 version 2.1 of the License, or (at your option) any later version.
14 version 2.1 of the License, or (at your option) any later version.
15
15
16 This library is distributed in the hope that it will be useful,
16 This library is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 Lesser General Public License for more details.
19 Lesser General Public License for more details.
20
20
21 You should have received a copy of the GNU Lesser General Public
21 You should have received a copy of the GNU Lesser General Public
22 License along with this library; if not, see
22 License along with this library; if not, see
23 <http://www.gnu.org/licenses/>.
23 <http://www.gnu.org/licenses/>.
24
24
25 """
25 """
26
26
27 """0.12 update - allow selection of binding interface
27 """0.12 update - allow selection of binding interface
28 typo fix - Thanks A. M. Kuchlingi
28 typo fix - Thanks A. M. Kuchlingi
29 removed all use of word 'Rendezvous' - this is an API change"""
29 removed all use of word 'Rendezvous' - this is an API change"""
30
30
31 """0.11 update - correction to comments for addListener method
31 """0.11 update - correction to comments for addListener method
32 support for new record types seen from OS X
32 support for new record types seen from OS X
33 - IPv6 address
33 - IPv6 address
34 - hostinfo
34 - hostinfo
35 ignore unknown DNS record types
35 ignore unknown DNS record types
36 fixes to name decoding
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. on Mac OS X)
38 tested against Mac OS X 10.3.2's mDNSResponder
38 tested against Mac OS X 10.3.2's mDNSResponder
39 corrections to removal of list entries for service browser"""
39 corrections to removal of list entries for service browser"""
40
40
41 """0.10 update - Jonathon Paisley contributed these corrections:
41 """0.10 update - Jonathon Paisley contributed these corrections:
42 always multicast replies, even when query is unicast
42 always multicast replies, even when query is unicast
43 correct a pointer encoding problem
43 correct a pointer encoding problem
44 can now write records in any order
44 can now write records in any order
45 traceback shown on failure
45 traceback shown on failure
46 better TXT record parsing
46 better TXT record parsing
47 server is now separate from name
47 server is now separate from name
48 can cancel a service browser
48 can cancel a service browser
49
49
50 modified some unit tests to accommodate these changes"""
50 modified some unit tests to accommodate these changes"""
51
51
52 """0.09 update - remove all records on service unregistration
52 """0.09 update - remove all records on service unregistration
53 fix DOS security problem with readName"""
53 fix DOS security problem with readName"""
54
54
55 """0.08 update - changed licensing to LGPL"""
55 """0.08 update - changed licensing to LGPL"""
56
56
57 """0.07 update - faster shutdown on engine
57 """0.07 update - faster shutdown on engine
58 pointer encoding of outgoing names
58 pointer encoding of outgoing names
59 ServiceBrowser now works
59 ServiceBrowser now works
60 new unit tests"""
60 new unit tests"""
61
61
62 """0.06 update - small improvements with unit tests
62 """0.06 update - small improvements with unit tests
63 added defined exception types
63 added defined exception types
64 new style objects
64 new style objects
65 fixed hostname/interface problem
65 fixed hostname/interface problem
66 fixed socket timeout problem
66 fixed socket timeout problem
67 fixed addServiceListener() typo bug
67 fixed addServiceListener() typo bug
68 using select() for socket reads
68 using select() for socket reads
69 tested on Debian unstable with Python 2.2.2"""
69 tested on Debian unstable with Python 2.2.2"""
70
70
71 """0.05 update - ensure case insensitivity on domain names
71 """0.05 update - ensure case insensitivity on domain names
72 support for unicast DNS queries"""
72 support for unicast DNS queries"""
73
73
74 """0.04 update - added some unit tests
74 """0.04 update - added some unit tests
75 added __ne__ adjuncts where required
75 added __ne__ adjuncts where required
76 ensure names end in '.local.'
76 ensure names end in '.local.'
77 timeout on receiving socket for clean shutdown"""
77 timeout on receiving socket for clean shutdown"""
78
78
79 __author__ = "Paul Scott-Murphy"
79 __author__ = "Paul Scott-Murphy"
80 __email__ = "paul at scott dash murphy dot com"
80 __email__ = "paul at scott dash murphy dot com"
81 __version__ = "0.12"
81 __version__ = "0.12"
82
82
83 import select
84 import socket
83 import string
85 import string
84 import time
85 import struct
86 import struct
86 import socket
87 import threading
87 import threading
88 import select
88 import time
89 import traceback
89 import traceback
90
90
91 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
91 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
92
92
93 # hook for threads
93 # hook for threads
94
94
95 globals()['_GLOBAL_DONE'] = 0
95 globals()['_GLOBAL_DONE'] = 0
96
96
97 # Some timing constants
97 # Some timing constants
98
98
99 _UNREGISTER_TIME = 125
99 _UNREGISTER_TIME = 125
100 _CHECK_TIME = 175
100 _CHECK_TIME = 175
101 _REGISTER_TIME = 225
101 _REGISTER_TIME = 225
102 _LISTENER_TIME = 200
102 _LISTENER_TIME = 200
103 _BROWSER_TIME = 500
103 _BROWSER_TIME = 500
104
104
105 # Some DNS constants
105 # Some DNS constants
106
106
107 _MDNS_ADDR = '224.0.0.251'
107 _MDNS_ADDR = '224.0.0.251'
108 _MDNS_PORT = 5353;
108 _MDNS_PORT = 5353;
109 _DNS_PORT = 53;
109 _DNS_PORT = 53;
110 _DNS_TTL = 60 * 60; # one hour default TTL
110 _DNS_TTL = 60 * 60; # one hour default TTL
111
111
112 _MAX_MSG_TYPICAL = 1460 # unused
112 _MAX_MSG_TYPICAL = 1460 # unused
113 _MAX_MSG_ABSOLUTE = 8972
113 _MAX_MSG_ABSOLUTE = 8972
114
114
115 _FLAGS_QR_MASK = 0x8000 # query response mask
115 _FLAGS_QR_MASK = 0x8000 # query response mask
116 _FLAGS_QR_QUERY = 0x0000 # query
116 _FLAGS_QR_QUERY = 0x0000 # query
117 _FLAGS_QR_RESPONSE = 0x8000 # response
117 _FLAGS_QR_RESPONSE = 0x8000 # response
118
118
119 _FLAGS_AA = 0x0400 # Authoritative answer
119 _FLAGS_AA = 0x0400 # Authoritative answer
120 _FLAGS_TC = 0x0200 # Truncated
120 _FLAGS_TC = 0x0200 # Truncated
121 _FLAGS_RD = 0x0100 # Recursion desired
121 _FLAGS_RD = 0x0100 # Recursion desired
122 _FLAGS_RA = 0x8000 # Recursion available
122 _FLAGS_RA = 0x8000 # Recursion available
123
123
124 _FLAGS_Z = 0x0040 # Zero
124 _FLAGS_Z = 0x0040 # Zero
125 _FLAGS_AD = 0x0020 # Authentic data
125 _FLAGS_AD = 0x0020 # Authentic data
126 _FLAGS_CD = 0x0010 # Checking disabled
126 _FLAGS_CD = 0x0010 # Checking disabled
127
127
128 _CLASS_IN = 1
128 _CLASS_IN = 1
129 _CLASS_CS = 2
129 _CLASS_CS = 2
130 _CLASS_CH = 3
130 _CLASS_CH = 3
131 _CLASS_HS = 4
131 _CLASS_HS = 4
132 _CLASS_NONE = 254
132 _CLASS_NONE = 254
133 _CLASS_ANY = 255
133 _CLASS_ANY = 255
134 _CLASS_MASK = 0x7FFF
134 _CLASS_MASK = 0x7FFF
135 _CLASS_UNIQUE = 0x8000
135 _CLASS_UNIQUE = 0x8000
136
136
137 _TYPE_A = 1
137 _TYPE_A = 1
138 _TYPE_NS = 2
138 _TYPE_NS = 2
139 _TYPE_MD = 3
139 _TYPE_MD = 3
140 _TYPE_MF = 4
140 _TYPE_MF = 4
141 _TYPE_CNAME = 5
141 _TYPE_CNAME = 5
142 _TYPE_SOA = 6
142 _TYPE_SOA = 6
143 _TYPE_MB = 7
143 _TYPE_MB = 7
144 _TYPE_MG = 8
144 _TYPE_MG = 8
145 _TYPE_MR = 9
145 _TYPE_MR = 9
146 _TYPE_NULL = 10
146 _TYPE_NULL = 10
147 _TYPE_WKS = 11
147 _TYPE_WKS = 11
148 _TYPE_PTR = 12
148 _TYPE_PTR = 12
149 _TYPE_HINFO = 13
149 _TYPE_HINFO = 13
150 _TYPE_MINFO = 14
150 _TYPE_MINFO = 14
151 _TYPE_MX = 15
151 _TYPE_MX = 15
152 _TYPE_TXT = 16
152 _TYPE_TXT = 16
153 _TYPE_AAAA = 28
153 _TYPE_AAAA = 28
154 _TYPE_SRV = 33
154 _TYPE_SRV = 33
155 _TYPE_ANY = 255
155 _TYPE_ANY = 255
156
156
157 # Mapping constants to names
157 # Mapping constants to names
158
158
159 _CLASSES = { _CLASS_IN : "in",
159 _CLASSES = { _CLASS_IN : "in",
160 _CLASS_CS : "cs",
160 _CLASS_CS : "cs",
161 _CLASS_CH : "ch",
161 _CLASS_CH : "ch",
162 _CLASS_HS : "hs",
162 _CLASS_HS : "hs",
163 _CLASS_NONE : "none",
163 _CLASS_NONE : "none",
164 _CLASS_ANY : "any" }
164 _CLASS_ANY : "any" }
165
165
166 _TYPES = { _TYPE_A : "a",
166 _TYPES = { _TYPE_A : "a",
167 _TYPE_NS : "ns",
167 _TYPE_NS : "ns",
168 _TYPE_MD : "md",
168 _TYPE_MD : "md",
169 _TYPE_MF : "mf",
169 _TYPE_MF : "mf",
170 _TYPE_CNAME : "cname",
170 _TYPE_CNAME : "cname",
171 _TYPE_SOA : "soa",
171 _TYPE_SOA : "soa",
172 _TYPE_MB : "mb",
172 _TYPE_MB : "mb",
173 _TYPE_MG : "mg",
173 _TYPE_MG : "mg",
174 _TYPE_MR : "mr",
174 _TYPE_MR : "mr",
175 _TYPE_NULL : "null",
175 _TYPE_NULL : "null",
176 _TYPE_WKS : "wks",
176 _TYPE_WKS : "wks",
177 _TYPE_PTR : "ptr",
177 _TYPE_PTR : "ptr",
178 _TYPE_HINFO : "hinfo",
178 _TYPE_HINFO : "hinfo",
179 _TYPE_MINFO : "minfo",
179 _TYPE_MINFO : "minfo",
180 _TYPE_MX : "mx",
180 _TYPE_MX : "mx",
181 _TYPE_TXT : "txt",
181 _TYPE_TXT : "txt",
182 _TYPE_AAAA : "quada",
182 _TYPE_AAAA : "quada",
183 _TYPE_SRV : "srv",
183 _TYPE_SRV : "srv",
184 _TYPE_ANY : "any" }
184 _TYPE_ANY : "any" }
185
185
186 # utility functions
186 # utility functions
187
187
188 def currentTimeMillis():
188 def currentTimeMillis():
189 """Current system time in milliseconds"""
189 """Current system time in milliseconds"""
190 return time.time() * 1000
190 return time.time() * 1000
191
191
192 # Exceptions
192 # Exceptions
193
193
194 class NonLocalNameException(Exception):
194 class NonLocalNameException(Exception):
195 pass
195 pass
196
196
197 class NonUniqueNameException(Exception):
197 class NonUniqueNameException(Exception):
198 pass
198 pass
199
199
200 class NamePartTooLongException(Exception):
200 class NamePartTooLongException(Exception):
201 pass
201 pass
202
202
203 class AbstractMethodException(Exception):
203 class AbstractMethodException(Exception):
204 pass
204 pass
205
205
206 class BadTypeInNameException(Exception):
206 class BadTypeInNameException(Exception):
207 pass
207 pass
208
208
209 class BadDomainName(Exception):
209 class BadDomainName(Exception):
210 def __init__(self, pos):
210 def __init__(self, pos):
211 Exception.__init__(self, "at position %s" % pos)
211 Exception.__init__(self, "at position %s" % pos)
212
212
213 class BadDomainNameCircular(BadDomainName):
213 class BadDomainNameCircular(BadDomainName):
214 pass
214 pass
215
215
216 # implementation classes
216 # implementation classes
217
217
218 class DNSEntry(object):
218 class DNSEntry(object):
219 """A DNS entry"""
219 """A DNS entry"""
220
220
221 def __init__(self, name, type, clazz):
221 def __init__(self, name, type, clazz):
222 self.key = string.lower(name)
222 self.key = string.lower(name)
223 self.name = name
223 self.name = name
224 self.type = type
224 self.type = type
225 self.clazz = clazz & _CLASS_MASK
225 self.clazz = clazz & _CLASS_MASK
226 self.unique = (clazz & _CLASS_UNIQUE) != 0
226 self.unique = (clazz & _CLASS_UNIQUE) != 0
227
227
228 def __eq__(self, other):
228 def __eq__(self, other):
229 """Equality test on name, type, and class"""
229 """Equality test on name, type, and class"""
230 if isinstance(other, DNSEntry):
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 self.clazz == other.clazz
232 return 0
232 return 0
233
233
234 def __ne__(self, other):
234 def __ne__(self, other):
235 """Non-equality test"""
235 """Non-equality test"""
236 return not self.__eq__(other)
236 return not self.__eq__(other)
237
237
238 def getClazz(self, clazz):
238 def getClazz(self, clazz):
239 """Class accessor"""
239 """Class accessor"""
240 try:
240 try:
241 return _CLASSES[clazz]
241 return _CLASSES[clazz]
242 except KeyError:
242 except KeyError:
243 return "?(%s)" % (clazz)
243 return "?(%s)" % (clazz)
244
244
245 def getType(self, type):
245 def getType(self, type):
246 """Type accessor"""
246 """Type accessor"""
247 try:
247 try:
248 return _TYPES[type]
248 return _TYPES[type]
249 except KeyError:
249 except KeyError:
250 return "?(%s)" % (type)
250 return "?(%s)" % (type)
251
251
252 def toString(self, hdr, other):
252 def toString(self, hdr, other):
253 """String representation with additional information"""
253 """String representation with additional information"""
254 result = "%s[%s,%s" % (hdr, self.getType(self.type), self.getClazz(self.clazz))
254 result = "%s[%s,%s" % (hdr, self.getType(self.type), self.getClazz(self.clazz))
255 if self.unique:
255 if self.unique:
256 result += "-unique,"
256 result += "-unique,"
257 else:
257 else:
258 result += ","
258 result += ","
259 result += self.name
259 result += self.name
260 if other is not None:
260 if other is not None:
261 result += ",%s]" % (other)
261 result += ",%s]" % (other)
262 else:
262 else:
263 result += "]"
263 result += "]"
264 return result
264 return result
265
265
266 class DNSQuestion(DNSEntry):
266 class DNSQuestion(DNSEntry):
267 """A DNS question entry"""
267 """A DNS question entry"""
268
268
269 def __init__(self, name, type, clazz):
269 def __init__(self, name, type, clazz):
270 if not name.endswith(".local."):
270 if not name.endswith(".local."):
271 raise NonLocalNameException(name)
271 raise NonLocalNameException(name)
272 DNSEntry.__init__(self, name, type, clazz)
272 DNSEntry.__init__(self, name, type, clazz)
273
273
274 def answeredBy(self, rec):
274 def answeredBy(self, rec):
275 """Returns true if the question is answered by the record"""
275 """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
276 return self.clazz == rec.clazz and (self.type == rec.type or self.type == _TYPE_ANY) and self.name == rec.name
277
277
278 def __repr__(self):
278 def __repr__(self):
279 """String representation"""
279 """String representation"""
280 return DNSEntry.toString(self, "question", None)
280 return DNSEntry.toString(self, "question", None)
281
281
282
282
283 class DNSRecord(DNSEntry):
283 class DNSRecord(DNSEntry):
284 """A DNS record - like a DNS entry, but has a TTL"""
284 """A DNS record - like a DNS entry, but has a TTL"""
285
285
286 def __init__(self, name, type, clazz, ttl):
286 def __init__(self, name, type, clazz, ttl):
287 DNSEntry.__init__(self, name, type, clazz)
287 DNSEntry.__init__(self, name, type, clazz)
288 self.ttl = ttl
288 self.ttl = ttl
289 self.created = currentTimeMillis()
289 self.created = currentTimeMillis()
290
290
291 def __eq__(self, other):
291 def __eq__(self, other):
292 """Tests equality as per DNSRecord"""
292 """Tests equality as per DNSRecord"""
293 if isinstance(other, DNSRecord):
293 if isinstance(other, DNSRecord):
294 return DNSEntry.__eq__(self, other)
294 return DNSEntry.__eq__(self, other)
295 return 0
295 return 0
296
296
297 def suppressedBy(self, msg):
297 def suppressedBy(self, msg):
298 """Returns true if any answer in a message can suffice for the
298 """Returns true if any answer in a message can suffice for the
299 information held in this record."""
299 information held in this record."""
300 for record in msg.answers:
300 for record in msg.answers:
301 if self.suppressedByAnswer(record):
301 if self.suppressedByAnswer(record):
302 return 1
302 return 1
303 return 0
303 return 0
304
304
305 def suppressedByAnswer(self, other):
305 def suppressedByAnswer(self, other):
306 """Returns true if another record has same name, type and class,
306 """Returns true if another record has same name, type and class,
307 and if its TTL is at least half of this record's."""
307 and if its TTL is at least half of this record's."""
308 if self == other and other.ttl > (self.ttl / 2):
308 if self == other and other.ttl > (self.ttl / 2):
309 return 1
309 return 1
310 return 0
310 return 0
311
311
312 def getExpirationTime(self, percent):
312 def getExpirationTime(self, percent):
313 """Returns the time at which this record will have expired
313 """Returns the time at which this record will have expired
314 by a certain percentage."""
314 by a certain percentage."""
315 return self.created + (percent * self.ttl * 10)
315 return self.created + (percent * self.ttl * 10)
316
316
317 def getRemainingTTL(self, now):
317 def getRemainingTTL(self, now):
318 """Returns the remaining TTL in seconds."""
318 """Returns the remaining TTL in seconds."""
319 return max(0, (self.getExpirationTime(100) - now) / 1000)
319 return max(0, (self.getExpirationTime(100) - now) / 1000)
320
320
321 def isExpired(self, now):
321 def isExpired(self, now):
322 """Returns true if this record has expired."""
322 """Returns true if this record has expired."""
323 return self.getExpirationTime(100) <= now
323 return self.getExpirationTime(100) <= now
324
324
325 def isStale(self, now):
325 def isStale(self, now):
326 """Returns true if this record is at least half way expired."""
326 """Returns true if this record is at least half way expired."""
327 return self.getExpirationTime(50) <= now
327 return self.getExpirationTime(50) <= now
328
328
329 def resetTTL(self, other):
329 def resetTTL(self, other):
330 """Sets this record's TTL and created time to that of
330 """Sets this record's TTL and created time to that of
331 another record."""
331 another record."""
332 self.created = other.created
332 self.created = other.created
333 self.ttl = other.ttl
333 self.ttl = other.ttl
334
334
335 def write(self, out):
335 def write(self, out):
336 """Abstract method"""
336 """Abstract method"""
337 raise AbstractMethodException
337 raise AbstractMethodException
338
338
339 def toString(self, other):
339 def toString(self, other):
340 """String representation with additional information"""
340 """String representation with additional information"""
341 arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other)
341 arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other)
342 return DNSEntry.toString(self, "record", arg)
342 return DNSEntry.toString(self, "record", arg)
343
343
344 class DNSAddress(DNSRecord):
344 class DNSAddress(DNSRecord):
345 """A DNS address record"""
345 """A DNS address record"""
346
346
347 def __init__(self, name, type, clazz, ttl, address):
347 def __init__(self, name, type, clazz, ttl, address):
348 DNSRecord.__init__(self, name, type, clazz, ttl)
348 DNSRecord.__init__(self, name, type, clazz, ttl)
349 self.address = address
349 self.address = address
350
350
351 def write(self, out):
351 def write(self, out):
352 """Used in constructing an outgoing packet"""
352 """Used in constructing an outgoing packet"""
353 out.writeString(self.address, len(self.address))
353 out.writeString(self.address, len(self.address))
354
354
355 def __eq__(self, other):
355 def __eq__(self, other):
356 """Tests equality on address"""
356 """Tests equality on address"""
357 if isinstance(other, DNSAddress):
357 if isinstance(other, DNSAddress):
358 return self.address == other.address
358 return self.address == other.address
359 return 0
359 return 0
360
360
361 def __repr__(self):
361 def __repr__(self):
362 """String representation"""
362 """String representation"""
363 try:
363 try:
364 return socket.inet_ntoa(self.address)
364 return socket.inet_ntoa(self.address)
365 except Exception:
365 except Exception:
366 return self.address
366 return self.address
367
367
368 class DNSHinfo(DNSRecord):
368 class DNSHinfo(DNSRecord):
369 """A DNS host information record"""
369 """A DNS host information record"""
370
370
371 def __init__(self, name, type, clazz, ttl, cpu, os):
371 def __init__(self, name, type, clazz, ttl, cpu, os):
372 DNSRecord.__init__(self, name, type, clazz, ttl)
372 DNSRecord.__init__(self, name, type, clazz, ttl)
373 self.cpu = cpu
373 self.cpu = cpu
374 self.os = os
374 self.os = os
375
375
376 def write(self, out):
376 def write(self, out):
377 """Used in constructing an outgoing packet"""
377 """Used in constructing an outgoing packet"""
378 out.writeString(self.cpu, len(self.cpu))
378 out.writeString(self.cpu, len(self.cpu))
379 out.writeString(self.os, len(self.os))
379 out.writeString(self.os, len(self.os))
380
380
381 def __eq__(self, other):
381 def __eq__(self, other):
382 """Tests equality on cpu and os"""
382 """Tests equality on cpu and os"""
383 if isinstance(other, DNSHinfo):
383 if isinstance(other, DNSHinfo):
384 return self.cpu == other.cpu and self.os == other.os
384 return self.cpu == other.cpu and self.os == other.os
385 return 0
385 return 0
386
386
387 def __repr__(self):
387 def __repr__(self):
388 """String representation"""
388 """String representation"""
389 return self.cpu + " " + self.os
389 return self.cpu + " " + self.os
390
390
391 class DNSPointer(DNSRecord):
391 class DNSPointer(DNSRecord):
392 """A DNS pointer record"""
392 """A DNS pointer record"""
393
393
394 def __init__(self, name, type, clazz, ttl, alias):
394 def __init__(self, name, type, clazz, ttl, alias):
395 DNSRecord.__init__(self, name, type, clazz, ttl)
395 DNSRecord.__init__(self, name, type, clazz, ttl)
396 self.alias = alias
396 self.alias = alias
397
397
398 def write(self, out):
398 def write(self, out):
399 """Used in constructing an outgoing packet"""
399 """Used in constructing an outgoing packet"""
400 out.writeName(self.alias)
400 out.writeName(self.alias)
401
401
402 def __eq__(self, other):
402 def __eq__(self, other):
403 """Tests equality on alias"""
403 """Tests equality on alias"""
404 if isinstance(other, DNSPointer):
404 if isinstance(other, DNSPointer):
405 return self.alias == other.alias
405 return self.alias == other.alias
406 return 0
406 return 0
407
407
408 def __repr__(self):
408 def __repr__(self):
409 """String representation"""
409 """String representation"""
410 return self.toString(self.alias)
410 return self.toString(self.alias)
411
411
412 class DNSText(DNSRecord):
412 class DNSText(DNSRecord):
413 """A DNS text record"""
413 """A DNS text record"""
414
414
415 def __init__(self, name, type, clazz, ttl, text):
415 def __init__(self, name, type, clazz, ttl, text):
416 DNSRecord.__init__(self, name, type, clazz, ttl)
416 DNSRecord.__init__(self, name, type, clazz, ttl)
417 self.text = text
417 self.text = text
418
418
419 def write(self, out):
419 def write(self, out):
420 """Used in constructing an outgoing packet"""
420 """Used in constructing an outgoing packet"""
421 out.writeString(self.text, len(self.text))
421 out.writeString(self.text, len(self.text))
422
422
423 def __eq__(self, other):
423 def __eq__(self, other):
424 """Tests equality on text"""
424 """Tests equality on text"""
425 if isinstance(other, DNSText):
425 if isinstance(other, DNSText):
426 return self.text == other.text
426 return self.text == other.text
427 return 0
427 return 0
428
428
429 def __repr__(self):
429 def __repr__(self):
430 """String representation"""
430 """String representation"""
431 if len(self.text) > 10:
431 if len(self.text) > 10:
432 return self.toString(self.text[:7] + "...")
432 return self.toString(self.text[:7] + "...")
433 else:
433 else:
434 return self.toString(self.text)
434 return self.toString(self.text)
435
435
436 class DNSService(DNSRecord):
436 class DNSService(DNSRecord):
437 """A DNS service record"""
437 """A DNS service record"""
438
438
439 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
439 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
440 DNSRecord.__init__(self, name, type, clazz, ttl)
440 DNSRecord.__init__(self, name, type, clazz, ttl)
441 self.priority = priority
441 self.priority = priority
442 self.weight = weight
442 self.weight = weight
443 self.port = port
443 self.port = port
444 self.server = server
444 self.server = server
445
445
446 def write(self, out):
446 def write(self, out):
447 """Used in constructing an outgoing packet"""
447 """Used in constructing an outgoing packet"""
448 out.writeShort(self.priority)
448 out.writeShort(self.priority)
449 out.writeShort(self.weight)
449 out.writeShort(self.weight)
450 out.writeShort(self.port)
450 out.writeShort(self.port)
451 out.writeName(self.server)
451 out.writeName(self.server)
452
452
453 def __eq__(self, other):
453 def __eq__(self, other):
454 """Tests equality on priority, weight, port and server"""
454 """Tests equality on priority, weight, port and server"""
455 if isinstance(other, DNSService):
455 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
456 return self.priority == other.priority and self.weight == other.weight and self.port == other.port and self.server == other.server
457 return 0
457 return 0
458
458
459 def __repr__(self):
459 def __repr__(self):
460 """String representation"""
460 """String representation"""
461 return self.toString("%s:%s" % (self.server, self.port))
461 return self.toString("%s:%s" % (self.server, self.port))
462
462
463 class DNSIncoming(object):
463 class DNSIncoming(object):
464 """Object representation of an incoming DNS packet"""
464 """Object representation of an incoming DNS packet"""
465
465
466 def __init__(self, data):
466 def __init__(self, data):
467 """Constructor from string holding bytes of packet"""
467 """Constructor from string holding bytes of packet"""
468 self.offset = 0
468 self.offset = 0
469 self.data = data
469 self.data = data
470 self.questions = []
470 self.questions = []
471 self.answers = []
471 self.answers = []
472 self.numQuestions = 0
472 self.numQuestions = 0
473 self.numAnswers = 0
473 self.numAnswers = 0
474 self.numAuthorities = 0
474 self.numAuthorities = 0
475 self.numAdditionals = 0
475 self.numAdditionals = 0
476
476
477 self.readHeader()
477 self.readHeader()
478 self.readQuestions()
478 self.readQuestions()
479 self.readOthers()
479 self.readOthers()
480
480
481 def readHeader(self):
481 def readHeader(self):
482 """Reads header portion of packet"""
482 """Reads header portion of packet"""
483 format = '!HHHHHH'
483 format = '!HHHHHH'
484 length = struct.calcsize(format)
484 length = struct.calcsize(format)
485 info = struct.unpack(format, self.data[self.offset:self.offset+length])
485 info = struct.unpack(format, self.data[self.offset:self.offset+length])
486 self.offset += length
486 self.offset += length
487
487
488 self.id = info[0]
488 self.id = info[0]
489 self.flags = info[1]
489 self.flags = info[1]
490 self.numQuestions = info[2]
490 self.numQuestions = info[2]
491 self.numAnswers = info[3]
491 self.numAnswers = info[3]
492 self.numAuthorities = info[4]
492 self.numAuthorities = info[4]
493 self.numAdditionals = info[5]
493 self.numAdditionals = info[5]
494
494
495 def readQuestions(self):
495 def readQuestions(self):
496 """Reads questions section of packet"""
496 """Reads questions section of packet"""
497 format = '!HH'
497 format = '!HH'
498 length = struct.calcsize(format)
498 length = struct.calcsize(format)
499 for i in range(0, self.numQuestions):
499 for i in range(0, self.numQuestions):
500 name = self.readName()
500 name = self.readName()
501 info = struct.unpack(format, self.data[self.offset:self.offset+length])
501 info = struct.unpack(format, self.data[self.offset:self.offset+length])
502 self.offset += length
502 self.offset += length
503
503
504 try:
504 try:
505 question = DNSQuestion(name, info[0], info[1])
505 question = DNSQuestion(name, info[0], info[1])
506 self.questions.append(question)
506 self.questions.append(question)
507 except NonLocalNameException:
507 except NonLocalNameException:
508 pass
508 pass
509
509
510 def readInt(self):
510 def readInt(self):
511 """Reads an integer from the packet"""
511 """Reads an integer from the packet"""
512 format = '!I'
512 format = '!I'
513 length = struct.calcsize(format)
513 length = struct.calcsize(format)
514 info = struct.unpack(format, self.data[self.offset:self.offset+length])
514 info = struct.unpack(format, self.data[self.offset:self.offset+length])
515 self.offset += length
515 self.offset += length
516 return info[0]
516 return info[0]
517
517
518 def readCharacterString(self):
518 def readCharacterString(self):
519 """Reads a character string from the packet"""
519 """Reads a character string from the packet"""
520 length = ord(self.data[self.offset])
520 length = ord(self.data[self.offset])
521 self.offset += 1
521 self.offset += 1
522 return self.readString(length)
522 return self.readString(length)
523
523
524 def readString(self, len):
524 def readString(self, len):
525 """Reads a string of a given length from the packet"""
525 """Reads a string of a given length from the packet"""
526 format = '!' + str(len) + 's'
526 format = '!' + str(len) + 's'
527 length = struct.calcsize(format)
527 length = struct.calcsize(format)
528 info = struct.unpack(format, self.data[self.offset:self.offset+length])
528 info = struct.unpack(format, self.data[self.offset:self.offset+length])
529 self.offset += length
529 self.offset += length
530 return info[0]
530 return info[0]
531
531
532 def readUnsignedShort(self):
532 def readUnsignedShort(self):
533 """Reads an unsigned short from the packet"""
533 """Reads an unsigned short from the packet"""
534 format = '!H'
534 format = '!H'
535 length = struct.calcsize(format)
535 length = struct.calcsize(format)
536 info = struct.unpack(format, self.data[self.offset:self.offset+length])
536 info = struct.unpack(format, self.data[self.offset:self.offset+length])
537 self.offset += length
537 self.offset += length
538 return info[0]
538 return info[0]
539
539
540 def readOthers(self):
540 def readOthers(self):
541 """Reads the answers, authorities and additionals section of the packet"""
541 """Reads the answers, authorities and additionals section of the packet"""
542 format = '!HHiH'
542 format = '!HHiH'
543 length = struct.calcsize(format)
543 length = struct.calcsize(format)
544 n = self.numAnswers + self.numAuthorities + self.numAdditionals
544 n = self.numAnswers + self.numAuthorities + self.numAdditionals
545 for i in range(0, n):
545 for i in range(0, n):
546 domain = self.readName()
546 domain = self.readName()
547 info = struct.unpack(format, self.data[self.offset:self.offset+length])
547 info = struct.unpack(format, self.data[self.offset:self.offset+length])
548 self.offset += length
548 self.offset += length
549
549
550 rec = None
550 rec = None
551 if info[0] == _TYPE_A:
551 if info[0] == _TYPE_A:
552 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4))
552 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4))
553 elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR:
553 elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR:
554 rec = DNSPointer(domain, info[0], info[1], info[2], self.readName())
554 rec = DNSPointer(domain, info[0], info[1], info[2], self.readName())
555 elif info[0] == _TYPE_TXT:
555 elif info[0] == _TYPE_TXT:
556 rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3]))
556 rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3]))
557 elif info[0] == _TYPE_SRV:
557 elif info[0] == _TYPE_SRV:
558 rec = DNSService(domain, info[0], info[1], info[2], self.readUnsignedShort(), self.readUnsignedShort(), self.readUnsignedShort(), self.readName())
558 rec = DNSService(domain, info[0], info[1], info[2], self.readUnsignedShort(), self.readUnsignedShort(), self.readUnsignedShort(), self.readName())
559 elif info[0] == _TYPE_HINFO:
559 elif info[0] == _TYPE_HINFO:
560 rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString())
560 rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString())
561 elif info[0] == _TYPE_AAAA:
561 elif info[0] == _TYPE_AAAA:
562 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16))
562 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16))
563 else:
563 else:
564 # Try to ignore types we don't know about
564 # Try to ignore types we don't know about
565 # this may mean the rest of the name is
565 # this may mean the rest of the name is
566 # unable to be parsed, and may show errors
566 # unable to be parsed, and may show errors
567 # so this is left for debugging. New types
567 # so this is left for debugging. New types
568 # encountered need to be parsed properly.
568 # encountered need to be parsed properly.
569 #
569 #
570 #print "UNKNOWN TYPE = " + str(info[0])
570 #print "UNKNOWN TYPE = " + str(info[0])
571 #raise BadTypeInNameException
571 #raise BadTypeInNameException
572 self.offset += info[3]
572 self.offset += info[3]
573
573
574 if rec is not None:
574 if rec is not None:
575 self.answers.append(rec)
575 self.answers.append(rec)
576
576
577 def isQuery(self):
577 def isQuery(self):
578 """Returns true if this is a query"""
578 """Returns true if this is a query"""
579 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
579 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
580
580
581 def isResponse(self):
581 def isResponse(self):
582 """Returns true if this is a response"""
582 """Returns true if this is a response"""
583 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
583 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
584
584
585 def readUTF(self, offset, len):
585 def readUTF(self, offset, len):
586 """Reads a UTF-8 string of a given length from the packet"""
586 """Reads a UTF-8 string of a given length from the packet"""
587 return self.data[offset:offset+len].decode('utf-8')
587 return self.data[offset:offset+len].decode('utf-8')
588
588
589 def readName(self):
589 def readName(self):
590 """Reads a domain name from the packet"""
590 """Reads a domain name from the packet"""
591 result = ''
591 result = ''
592 off = self.offset
592 off = self.offset
593 next = -1
593 next = -1
594 first = off
594 first = off
595
595
596 while True:
596 while True:
597 len = ord(self.data[off])
597 len = ord(self.data[off])
598 off += 1
598 off += 1
599 if len == 0:
599 if len == 0:
600 break
600 break
601 t = len & 0xC0
601 t = len & 0xC0
602 if t == 0x00:
602 if t == 0x00:
603 result = ''.join((result, self.readUTF(off, len) + '.'))
603 result = ''.join((result, self.readUTF(off, len) + '.'))
604 off += len
604 off += len
605 elif t == 0xC0:
605 elif t == 0xC0:
606 if next < 0:
606 if next < 0:
607 next = off + 1
607 next = off + 1
608 off = ((len & 0x3F) << 8) | ord(self.data[off])
608 off = ((len & 0x3F) << 8) | ord(self.data[off])
609 if off >= first:
609 if off >= first:
610 raise BadDomainNameCircular(off)
610 raise BadDomainNameCircular(off)
611 first = off
611 first = off
612 else:
612 else:
613 raise BadDomainName(off)
613 raise BadDomainName(off)
614
614
615 if next >= 0:
615 if next >= 0:
616 self.offset = next
616 self.offset = next
617 else:
617 else:
618 self.offset = off
618 self.offset = off
619
619
620 return result
620 return result
621
621
622
622
623 class DNSOutgoing(object):
623 class DNSOutgoing(object):
624 """Object representation of an outgoing packet"""
624 """Object representation of an outgoing packet"""
625
625
626 def __init__(self, flags, multicast = 1):
626 def __init__(self, flags, multicast = 1):
627 self.finished = 0
627 self.finished = 0
628 self.id = 0
628 self.id = 0
629 self.multicast = multicast
629 self.multicast = multicast
630 self.flags = flags
630 self.flags = flags
631 self.names = {}
631 self.names = {}
632 self.data = []
632 self.data = []
633 self.size = 12
633 self.size = 12
634
634
635 self.questions = []
635 self.questions = []
636 self.answers = []
636 self.answers = []
637 self.authorities = []
637 self.authorities = []
638 self.additionals = []
638 self.additionals = []
639
639
640 def addQuestion(self, record):
640 def addQuestion(self, record):
641 """Adds a question"""
641 """Adds a question"""
642 self.questions.append(record)
642 self.questions.append(record)
643
643
644 def addAnswer(self, inp, record):
644 def addAnswer(self, inp, record):
645 """Adds an answer"""
645 """Adds an answer"""
646 if not record.suppressedBy(inp):
646 if not record.suppressedBy(inp):
647 self.addAnswerAtTime(record, 0)
647 self.addAnswerAtTime(record, 0)
648
648
649 def addAnswerAtTime(self, record, now):
649 def addAnswerAtTime(self, record, now):
650 """Adds an answer if if does not expire by a certain time"""
650 """Adds an answer if if does not expire by a certain time"""
651 if record is not None:
651 if record is not None:
652 if now == 0 or not record.isExpired(now):
652 if now == 0 or not record.isExpired(now):
653 self.answers.append((record, now))
653 self.answers.append((record, now))
654
654
655 def addAuthoritativeAnswer(self, record):
655 def addAuthoritativeAnswer(self, record):
656 """Adds an authoritative answer"""
656 """Adds an authoritative answer"""
657 self.authorities.append(record)
657 self.authorities.append(record)
658
658
659 def addAdditionalAnswer(self, record):
659 def addAdditionalAnswer(self, record):
660 """Adds an additional answer"""
660 """Adds an additional answer"""
661 self.additionals.append(record)
661 self.additionals.append(record)
662
662
663 def writeByte(self, value):
663 def writeByte(self, value):
664 """Writes a single byte to the packet"""
664 """Writes a single byte to the packet"""
665 format = '!c'
665 format = '!c'
666 self.data.append(struct.pack(format, chr(value)))
666 self.data.append(struct.pack(format, chr(value)))
667 self.size += 1
667 self.size += 1
668
668
669 def insertShort(self, index, value):
669 def insertShort(self, index, value):
670 """Inserts an unsigned short in a certain position in the packet"""
670 """Inserts an unsigned short in a certain position in the packet"""
671 format = '!H'
671 format = '!H'
672 self.data.insert(index, struct.pack(format, value))
672 self.data.insert(index, struct.pack(format, value))
673 self.size += 2
673 self.size += 2
674
674
675 def writeShort(self, value):
675 def writeShort(self, value):
676 """Writes an unsigned short to the packet"""
676 """Writes an unsigned short to the packet"""
677 format = '!H'
677 format = '!H'
678 self.data.append(struct.pack(format, value))
678 self.data.append(struct.pack(format, value))
679 self.size += 2
679 self.size += 2
680
680
681 def writeInt(self, value):
681 def writeInt(self, value):
682 """Writes an unsigned integer to the packet"""
682 """Writes an unsigned integer to the packet"""
683 format = '!I'
683 format = '!I'
684 self.data.append(struct.pack(format, int(value)))
684 self.data.append(struct.pack(format, int(value)))
685 self.size += 4
685 self.size += 4
686
686
687 def writeString(self, value, length):
687 def writeString(self, value, length):
688 """Writes a string to the packet"""
688 """Writes a string to the packet"""
689 format = '!' + str(length) + 's'
689 format = '!' + str(length) + 's'
690 self.data.append(struct.pack(format, value))
690 self.data.append(struct.pack(format, value))
691 self.size += length
691 self.size += length
692
692
693 def writeUTF(self, s):
693 def writeUTF(self, s):
694 """Writes a UTF-8 string of a given length to the packet"""
694 """Writes a UTF-8 string of a given length to the packet"""
695 utfstr = s.encode('utf-8')
695 utfstr = s.encode('utf-8')
696 length = len(utfstr)
696 length = len(utfstr)
697 if length > 64:
697 if length > 64:
698 raise NamePartTooLongException
698 raise NamePartTooLongException
699 self.writeByte(length)
699 self.writeByte(length)
700 self.writeString(utfstr, length)
700 self.writeString(utfstr, length)
701
701
702 def writeName(self, name):
702 def writeName(self, name):
703 """Writes a domain name to the packet"""
703 """Writes a domain name to the packet"""
704
704
705 try:
705 try:
706 # Find existing instance of this name in packet
706 # Find existing instance of this name in packet
707 #
707 #
708 index = self.names[name]
708 index = self.names[name]
709 except KeyError:
709 except KeyError:
710 # No record of this name already, so write it
710 # No record of this name already, so write it
711 # out as normal, recording the location of the name
711 # out as normal, recording the location of the name
712 # for future pointers to it.
712 # for future pointers to it.
713 #
713 #
714 self.names[name] = self.size
714 self.names[name] = self.size
715 parts = name.split('.')
715 parts = name.split('.')
716 if parts[-1] == '':
716 if parts[-1] == '':
717 parts = parts[:-1]
717 parts = parts[:-1]
718 for part in parts:
718 for part in parts:
719 self.writeUTF(part)
719 self.writeUTF(part)
720 self.writeByte(0)
720 self.writeByte(0)
721 return
721 return
722
722
723 # An index was found, so write a pointer to it
723 # An index was found, so write a pointer to it
724 #
724 #
725 self.writeByte((index >> 8) | 0xC0)
725 self.writeByte((index >> 8) | 0xC0)
726 self.writeByte(index)
726 self.writeByte(index)
727
727
728 def writeQuestion(self, question):
728 def writeQuestion(self, question):
729 """Writes a question to the packet"""
729 """Writes a question to the packet"""
730 self.writeName(question.name)
730 self.writeName(question.name)
731 self.writeShort(question.type)
731 self.writeShort(question.type)
732 self.writeShort(question.clazz)
732 self.writeShort(question.clazz)
733
733
734 def writeRecord(self, record, now):
734 def writeRecord(self, record, now):
735 """Writes a record (answer, authoritative answer, additional) to
735 """Writes a record (answer, authoritative answer, additional) to
736 the packet"""
736 the packet"""
737 self.writeName(record.name)
737 self.writeName(record.name)
738 self.writeShort(record.type)
738 self.writeShort(record.type)
739 if record.unique and self.multicast:
739 if record.unique and self.multicast:
740 self.writeShort(record.clazz | _CLASS_UNIQUE)
740 self.writeShort(record.clazz | _CLASS_UNIQUE)
741 else:
741 else:
742 self.writeShort(record.clazz)
742 self.writeShort(record.clazz)
743 if now == 0:
743 if now == 0:
744 self.writeInt(record.ttl)
744 self.writeInt(record.ttl)
745 else:
745 else:
746 self.writeInt(record.getRemainingTTL(now))
746 self.writeInt(record.getRemainingTTL(now))
747 index = len(self.data)
747 index = len(self.data)
748 # Adjust size for the short we will write before this record
748 # Adjust size for the short we will write before this record
749 #
749 #
750 self.size += 2
750 self.size += 2
751 record.write(self)
751 record.write(self)
752 self.size -= 2
752 self.size -= 2
753
753
754 length = len(''.join(self.data[index:]))
754 length = len(''.join(self.data[index:]))
755 self.insertShort(index, length) # Here is the short we adjusted for
755 self.insertShort(index, length) # Here is the short we adjusted for
756
756
757 def packet(self):
757 def packet(self):
758 """Returns a string containing the packet's bytes
758 """Returns a string containing the packet's bytes
759
759
760 No further parts should be added to the packet once this
760 No further parts should be added to the packet once this
761 is done."""
761 is done."""
762 if not self.finished:
762 if not self.finished:
763 self.finished = 1
763 self.finished = 1
764 for question in self.questions:
764 for question in self.questions:
765 self.writeQuestion(question)
765 self.writeQuestion(question)
766 for answer, time in self.answers:
766 for answer, time_ in self.answers:
767 self.writeRecord(answer, time)
767 self.writeRecord(answer, time_)
768 for authority in self.authorities:
768 for authority in self.authorities:
769 self.writeRecord(authority, 0)
769 self.writeRecord(authority, 0)
770 for additional in self.additionals:
770 for additional in self.additionals:
771 self.writeRecord(additional, 0)
771 self.writeRecord(additional, 0)
772
772
773 self.insertShort(0, len(self.additionals))
773 self.insertShort(0, len(self.additionals))
774 self.insertShort(0, len(self.authorities))
774 self.insertShort(0, len(self.authorities))
775 self.insertShort(0, len(self.answers))
775 self.insertShort(0, len(self.answers))
776 self.insertShort(0, len(self.questions))
776 self.insertShort(0, len(self.questions))
777 self.insertShort(0, self.flags)
777 self.insertShort(0, self.flags)
778 if self.multicast:
778 if self.multicast:
779 self.insertShort(0, 0)
779 self.insertShort(0, 0)
780 else:
780 else:
781 self.insertShort(0, self.id)
781 self.insertShort(0, self.id)
782 return ''.join(self.data)
782 return ''.join(self.data)
783
783
784
784
785 class DNSCache(object):
785 class DNSCache(object):
786 """A cache of DNS entries"""
786 """A cache of DNS entries"""
787
787
788 def __init__(self):
788 def __init__(self):
789 self.cache = {}
789 self.cache = {}
790
790
791 def add(self, entry):
791 def add(self, entry):
792 """Adds an entry"""
792 """Adds an entry"""
793 try:
793 try:
794 list = self.cache[entry.key]
794 list = self.cache[entry.key]
795 except KeyError:
795 except KeyError:
796 list = self.cache[entry.key] = []
796 list = self.cache[entry.key] = []
797 list.append(entry)
797 list.append(entry)
798
798
799 def remove(self, entry):
799 def remove(self, entry):
800 """Removes an entry"""
800 """Removes an entry"""
801 try:
801 try:
802 list = self.cache[entry.key]
802 list = self.cache[entry.key]
803 list.remove(entry)
803 list.remove(entry)
804 except KeyError:
804 except KeyError:
805 pass
805 pass
806
806
807 def get(self, entry):
807 def get(self, entry):
808 """Gets an entry by key. Will return None if there is no
808 """Gets an entry by key. Will return None if there is no
809 matching entry."""
809 matching entry."""
810 try:
810 try:
811 list = self.cache[entry.key]
811 list = self.cache[entry.key]
812 return list[list.index(entry)]
812 return list[list.index(entry)]
813 except (KeyError, ValueError):
813 except (KeyError, ValueError):
814 return None
814 return None
815
815
816 def getByDetails(self, name, type, clazz):
816 def getByDetails(self, name, type, clazz):
817 """Gets an entry by details. Will return None if there is
817 """Gets an entry by details. Will return None if there is
818 no matching entry."""
818 no matching entry."""
819 entry = DNSEntry(name, type, clazz)
819 entry = DNSEntry(name, type, clazz)
820 return self.get(entry)
820 return self.get(entry)
821
821
822 def entriesWithName(self, name):
822 def entriesWithName(self, name):
823 """Returns a list of entries whose key matches the name."""
823 """Returns a list of entries whose key matches the name."""
824 try:
824 try:
825 return self.cache[name]
825 return self.cache[name]
826 except KeyError:
826 except KeyError:
827 return []
827 return []
828
828
829 def entries(self):
829 def entries(self):
830 """Returns a list of all entries"""
830 """Returns a list of all entries"""
831 def add(x, y): return x+y
831 def add(x, y): return x+y
832 try:
832 try:
833 return reduce(add, self.cache.values())
833 return reduce(add, self.cache.values())
834 except Exception:
834 except Exception:
835 return []
835 return []
836
836
837
837
838 class Engine(threading.Thread):
838 class Engine(threading.Thread):
839 """An engine wraps read access to sockets, allowing objects that
839 """An engine wraps read access to sockets, allowing objects that
840 need to receive data from sockets to be called back when the
840 need to receive data from sockets to be called back when the
841 sockets are ready.
841 sockets are ready.
842
842
843 A reader needs a handle_read() method, which is called when the socket
843 A reader needs a handle_read() method, which is called when the socket
844 it is interested in is ready for reading.
844 it is interested in is ready for reading.
845
845
846 Writers are not implemented here, because we only send short
846 Writers are not implemented here, because we only send short
847 packets.
847 packets.
848 """
848 """
849
849
850 def __init__(self, zeroconf):
850 def __init__(self, zeroconf):
851 threading.Thread.__init__(self)
851 threading.Thread.__init__(self)
852 self.zeroconf = zeroconf
852 self.zeroconf = zeroconf
853 self.readers = {} # maps socket to reader
853 self.readers = {} # maps socket to reader
854 self.timeout = 5
854 self.timeout = 5
855 self.condition = threading.Condition()
855 self.condition = threading.Condition()
856 self.start()
856 self.start()
857
857
858 def run(self):
858 def run(self):
859 while not globals()['_GLOBAL_DONE']:
859 while not globals()['_GLOBAL_DONE']:
860 rs = self.getReaders()
860 rs = self.getReaders()
861 if len(rs) == 0:
861 if len(rs) == 0:
862 # No sockets to manage, but we wait for the timeout
862 # No sockets to manage, but we wait for the timeout
863 # or addition of a socket
863 # or addition of a socket
864 #
864 #
865 self.condition.acquire()
865 self.condition.acquire()
866 self.condition.wait(self.timeout)
866 self.condition.wait(self.timeout)
867 self.condition.release()
867 self.condition.release()
868 else:
868 else:
869 try:
869 try:
870 rr, wr, er = select.select(rs, [], [], self.timeout)
870 rr, wr, er = select.select(rs, [], [], self.timeout)
871 for socket in rr:
871 for sock in rr:
872 try:
872 try:
873 self.readers[socket].handle_read()
873 self.readers[sock].handle_read()
874 except Exception:
874 except Exception:
875 if not globals()['_GLOBAL_DONE']:
875 if not globals()['_GLOBAL_DONE']:
876 traceback.print_exc()
876 traceback.print_exc()
877 except Exception:
877 except Exception:
878 pass
878 pass
879
879
880 def getReaders(self):
880 def getReaders(self):
881 self.condition.acquire()
881 self.condition.acquire()
882 result = self.readers.keys()
882 result = self.readers.keys()
883 self.condition.release()
883 self.condition.release()
884 return result
884 return result
885
885
886 def addReader(self, reader, socket):
886 def addReader(self, reader, socket):
887 self.condition.acquire()
887 self.condition.acquire()
888 self.readers[socket] = reader
888 self.readers[socket] = reader
889 self.condition.notify()
889 self.condition.notify()
890 self.condition.release()
890 self.condition.release()
891
891
892 def delReader(self, socket):
892 def delReader(self, socket):
893 self.condition.acquire()
893 self.condition.acquire()
894 del(self.readers[socket])
894 del(self.readers[socket])
895 self.condition.notify()
895 self.condition.notify()
896 self.condition.release()
896 self.condition.release()
897
897
898 def notify(self):
898 def notify(self):
899 self.condition.acquire()
899 self.condition.acquire()
900 self.condition.notify()
900 self.condition.notify()
901 self.condition.release()
901 self.condition.release()
902
902
903 class Listener(object):
903 class Listener(object):
904 """A Listener is used by this module to listen on the multicast
904 """A Listener is used by this module to listen on the multicast
905 group to which DNS messages are sent, allowing the implementation
905 group to which DNS messages are sent, allowing the implementation
906 to cache information as it arrives.
906 to cache information as it arrives.
907
907
908 It requires registration with an Engine object in order to have
908 It requires registration with an Engine object in order to have
909 the read() method called when a socket is available for reading."""
909 the read() method called when a socket is available for reading."""
910
910
911 def __init__(self, zeroconf):
911 def __init__(self, zeroconf):
912 self.zeroconf = zeroconf
912 self.zeroconf = zeroconf
913 self.zeroconf.engine.addReader(self, self.zeroconf.socket)
913 self.zeroconf.engine.addReader(self, self.zeroconf.socket)
914
914
915 def handle_read(self):
915 def handle_read(self):
916 data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE)
916 data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE)
917 self.data = data
917 self.data = data
918 msg = DNSIncoming(data)
918 msg = DNSIncoming(data)
919 if msg.isQuery():
919 if msg.isQuery():
920 # Always multicast responses
920 # Always multicast responses
921 #
921 #
922 if port == _MDNS_PORT:
922 if port == _MDNS_PORT:
923 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
923 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
924 # If it's not a multicast query, reply via unicast
924 # If it's not a multicast query, reply via unicast
925 # and multicast
925 # and multicast
926 #
926 #
927 elif port == _DNS_PORT:
927 elif port == _DNS_PORT:
928 self.zeroconf.handleQuery(msg, addr, port)
928 self.zeroconf.handleQuery(msg, addr, port)
929 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
929 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
930 else:
930 else:
931 self.zeroconf.handleResponse(msg)
931 self.zeroconf.handleResponse(msg)
932
932
933
933
934 class Reaper(threading.Thread):
934 class Reaper(threading.Thread):
935 """A Reaper is used by this module to remove cache entries that
935 """A Reaper is used by this module to remove cache entries that
936 have expired."""
936 have expired."""
937
937
938 def __init__(self, zeroconf):
938 def __init__(self, zeroconf):
939 threading.Thread.__init__(self)
939 threading.Thread.__init__(self)
940 self.zeroconf = zeroconf
940 self.zeroconf = zeroconf
941 self.start()
941 self.start()
942
942
943 def run(self):
943 def run(self):
944 while True:
944 while True:
945 self.zeroconf.wait(10 * 1000)
945 self.zeroconf.wait(10 * 1000)
946 if globals()['_GLOBAL_DONE']:
946 if globals()['_GLOBAL_DONE']:
947 return
947 return
948 now = currentTimeMillis()
948 now = currentTimeMillis()
949 for record in self.zeroconf.cache.entries():
949 for record in self.zeroconf.cache.entries():
950 if record.isExpired(now):
950 if record.isExpired(now):
951 self.zeroconf.updateRecord(now, record)
951 self.zeroconf.updateRecord(now, record)
952 self.zeroconf.cache.remove(record)
952 self.zeroconf.cache.remove(record)
953
953
954
954
955 class ServiceBrowser(threading.Thread):
955 class ServiceBrowser(threading.Thread):
956 """Used to browse for a service of a specific type.
956 """Used to browse for a service of a specific type.
957
957
958 The listener object will have its addService() and
958 The listener object will have its addService() and
959 removeService() methods called when this browser
959 removeService() methods called when this browser
960 discovers changes in the services availability."""
960 discovers changes in the services availability."""
961
961
962 def __init__(self, zeroconf, type, listener):
962 def __init__(self, zeroconf, type, listener):
963 """Creates a browser for a specific type"""
963 """Creates a browser for a specific type"""
964 threading.Thread.__init__(self)
964 threading.Thread.__init__(self)
965 self.zeroconf = zeroconf
965 self.zeroconf = zeroconf
966 self.type = type
966 self.type = type
967 self.listener = listener
967 self.listener = listener
968 self.services = {}
968 self.services = {}
969 self.nextTime = currentTimeMillis()
969 self.nextTime = currentTimeMillis()
970 self.delay = _BROWSER_TIME
970 self.delay = _BROWSER_TIME
971 self.list = []
971 self.list = []
972
972
973 self.done = 0
973 self.done = 0
974
974
975 self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
975 self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
976 self.start()
976 self.start()
977
977
978 def updateRecord(self, zeroconf, now, record):
978 def updateRecord(self, zeroconf, now, record):
979 """Callback invoked by Zeroconf when new information arrives.
979 """Callback invoked by Zeroconf when new information arrives.
980
980
981 Updates information required by browser in the Zeroconf cache."""
981 Updates information required by browser in the Zeroconf cache."""
982 if record.type == _TYPE_PTR and record.name == self.type:
982 if record.type == _TYPE_PTR and record.name == self.type:
983 expired = record.isExpired(now)
983 expired = record.isExpired(now)
984 try:
984 try:
985 oldrecord = self.services[record.alias.lower()]
985 oldrecord = self.services[record.alias.lower()]
986 if not expired:
986 if not expired:
987 oldrecord.resetTTL(record)
987 oldrecord.resetTTL(record)
988 else:
988 else:
989 del(self.services[record.alias.lower()])
989 del(self.services[record.alias.lower()])
990 callback = lambda x: self.listener.removeService(x, self.type, record.alias)
990 callback = lambda x: self.listener.removeService(x, self.type, record.alias)
991 self.list.append(callback)
991 self.list.append(callback)
992 return
992 return
993 except Exception:
993 except Exception:
994 if not expired:
994 if not expired:
995 self.services[record.alias.lower()] = record
995 self.services[record.alias.lower()] = record
996 callback = lambda x: self.listener.addService(x, self.type, record.alias)
996 callback = lambda x: self.listener.addService(x, self.type, record.alias)
997 self.list.append(callback)
997 self.list.append(callback)
998
998
999 expires = record.getExpirationTime(75)
999 expires = record.getExpirationTime(75)
1000 if expires < self.nextTime:
1000 if expires < self.nextTime:
1001 self.nextTime = expires
1001 self.nextTime = expires
1002
1002
1003 def cancel(self):
1003 def cancel(self):
1004 self.done = 1
1004 self.done = 1
1005 self.zeroconf.notifyAll()
1005 self.zeroconf.notifyAll()
1006
1006
1007 def run(self):
1007 def run(self):
1008 while True:
1008 while True:
1009 event = None
1009 event = None
1010 now = currentTimeMillis()
1010 now = currentTimeMillis()
1011 if len(self.list) == 0 and self.nextTime > now:
1011 if len(self.list) == 0 and self.nextTime > now:
1012 self.zeroconf.wait(self.nextTime - now)
1012 self.zeroconf.wait(self.nextTime - now)
1013 if globals()['_GLOBAL_DONE'] or self.done:
1013 if globals()['_GLOBAL_DONE'] or self.done:
1014 return
1014 return
1015 now = currentTimeMillis()
1015 now = currentTimeMillis()
1016
1016
1017 if self.nextTime <= now:
1017 if self.nextTime <= now:
1018 out = DNSOutgoing(_FLAGS_QR_QUERY)
1018 out = DNSOutgoing(_FLAGS_QR_QUERY)
1019 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
1019 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
1020 for record in self.services.values():
1020 for record in self.services.values():
1021 if not record.isExpired(now):
1021 if not record.isExpired(now):
1022 out.addAnswerAtTime(record, now)
1022 out.addAnswerAtTime(record, now)
1023 self.zeroconf.send(out)
1023 self.zeroconf.send(out)
1024 self.nextTime = now + self.delay
1024 self.nextTime = now + self.delay
1025 self.delay = min(20 * 1000, self.delay * 2)
1025 self.delay = min(20 * 1000, self.delay * 2)
1026
1026
1027 if len(self.list) > 0:
1027 if len(self.list) > 0:
1028 event = self.list.pop(0)
1028 event = self.list.pop(0)
1029
1029
1030 if event is not None:
1030 if event is not None:
1031 event(self.zeroconf)
1031 event(self.zeroconf)
1032
1032
1033
1033
1034 class ServiceInfo(object):
1034 class ServiceInfo(object):
1035 """Service information"""
1035 """Service information"""
1036
1036
1037 def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None):
1037 def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None):
1038 """Create a service description.
1038 """Create a service description.
1039
1039
1040 type: fully qualified service type name
1040 type: fully qualified service type name
1041 name: fully qualified service name
1041 name: fully qualified service name
1042 address: IP address as unsigned short, network byte order
1042 address: IP address as unsigned short, network byte order
1043 port: port that the service runs on
1043 port: port that the service runs on
1044 weight: weight of the service
1044 weight: weight of the service
1045 priority: priority of the service
1045 priority: priority of the service
1046 properties: dictionary of properties (or a string holding the bytes for the text field)
1046 properties: dictionary of properties (or a string holding the bytes for the text field)
1047 server: fully qualified name for service host (defaults to name)"""
1047 server: fully qualified name for service host (defaults to name)"""
1048
1048
1049 if not name.endswith(type):
1049 if not name.endswith(type):
1050 raise BadTypeInNameException
1050 raise BadTypeInNameException
1051 self.type = type
1051 self.type = type
1052 self.name = name
1052 self.name = name
1053 self.address = address
1053 self.address = address
1054 self.port = port
1054 self.port = port
1055 self.weight = weight
1055 self.weight = weight
1056 self.priority = priority
1056 self.priority = priority
1057 if server:
1057 if server:
1058 self.server = server
1058 self.server = server
1059 else:
1059 else:
1060 self.server = name
1060 self.server = name
1061 self.setProperties(properties)
1061 self.setProperties(properties)
1062
1062
1063 def setProperties(self, properties):
1063 def setProperties(self, properties):
1064 """Sets properties and text of this info from a dictionary"""
1064 """Sets properties and text of this info from a dictionary"""
1065 if isinstance(properties, dict):
1065 if isinstance(properties, dict):
1066 self.properties = properties
1066 self.properties = properties
1067 list = []
1067 list = []
1068 result = ''
1068 result = ''
1069 for key in properties:
1069 for key in properties:
1070 value = properties[key]
1070 value = properties[key]
1071 if value is None:
1071 if value is None:
1072 suffix = ''
1072 suffix = ''
1073 elif isinstance(value, str):
1073 elif isinstance(value, str):
1074 suffix = value
1074 suffix = value
1075 elif isinstance(value, int):
1075 elif isinstance(value, int):
1076 if value:
1076 if value:
1077 suffix = 'true'
1077 suffix = 'true'
1078 else:
1078 else:
1079 suffix = 'false'
1079 suffix = 'false'
1080 else:
1080 else:
1081 suffix = ''
1081 suffix = ''
1082 list.append('='.join((key, suffix)))
1082 list.append('='.join((key, suffix)))
1083 for item in list:
1083 for item in list:
1084 result = ''.join((result, struct.pack('!c', chr(len(item))), item))
1084 result = ''.join((result, struct.pack('!c', chr(len(item))), item))
1085 self.text = result
1085 self.text = result
1086 else:
1086 else:
1087 self.text = properties
1087 self.text = properties
1088
1088
1089 def setText(self, text):
1089 def setText(self, text):
1090 """Sets properties and text given a text field"""
1090 """Sets properties and text given a text field"""
1091 self.text = text
1091 self.text = text
1092 try:
1092 try:
1093 result = {}
1093 result = {}
1094 end = len(text)
1094 end = len(text)
1095 index = 0
1095 index = 0
1096 strs = []
1096 strs = []
1097 while index < end:
1097 while index < end:
1098 length = ord(text[index])
1098 length = ord(text[index])
1099 index += 1
1099 index += 1
1100 strs.append(text[index:index+length])
1100 strs.append(text[index:index+length])
1101 index += length
1101 index += length
1102
1102
1103 for s in strs:
1103 for s in strs:
1104 eindex = s.find('=')
1104 eindex = s.find('=')
1105 if eindex == -1:
1105 if eindex == -1:
1106 # No equals sign at all
1106 # No equals sign at all
1107 key = s
1107 key = s
1108 value = 0
1108 value = 0
1109 else:
1109 else:
1110 key = s[:eindex]
1110 key = s[:eindex]
1111 value = s[eindex+1:]
1111 value = s[eindex+1:]
1112 if value == 'true':
1112 if value == 'true':
1113 value = 1
1113 value = 1
1114 elif value == 'false' or not value:
1114 elif value == 'false' or not value:
1115 value = 0
1115 value = 0
1116
1116
1117 # Only update non-existent properties
1117 # Only update non-existent properties
1118 if key and result.get(key) == None:
1118 if key and result.get(key) == None:
1119 result[key] = value
1119 result[key] = value
1120
1120
1121 self.properties = result
1121 self.properties = result
1122 except Exception:
1122 except Exception:
1123 traceback.print_exc()
1123 traceback.print_exc()
1124 self.properties = None
1124 self.properties = None
1125
1125
1126 def getType(self):
1126 def getType(self):
1127 """Type accessor"""
1127 """Type accessor"""
1128 return self.type
1128 return self.type
1129
1129
1130 def getName(self):
1130 def getName(self):
1131 """Name accessor"""
1131 """Name accessor"""
1132 if self.type is not None and self.name.endswith("." + self.type):
1132 if self.type is not None and self.name.endswith("." + self.type):
1133 return self.name[:len(self.name) - len(self.type) - 1]
1133 return self.name[:len(self.name) - len(self.type) - 1]
1134 return self.name
1134 return self.name
1135
1135
1136 def getAddress(self):
1136 def getAddress(self):
1137 """Address accessor"""
1137 """Address accessor"""
1138 return self.address
1138 return self.address
1139
1139
1140 def getPort(self):
1140 def getPort(self):
1141 """Port accessor"""
1141 """Port accessor"""
1142 return self.port
1142 return self.port
1143
1143
1144 def getPriority(self):
1144 def getPriority(self):
1145 """Priority accessor"""
1145 """Priority accessor"""
1146 return self.priority
1146 return self.priority
1147
1147
1148 def getWeight(self):
1148 def getWeight(self):
1149 """Weight accessor"""
1149 """Weight accessor"""
1150 return self.weight
1150 return self.weight
1151
1151
1152 def getProperties(self):
1152 def getProperties(self):
1153 """Properties accessor"""
1153 """Properties accessor"""
1154 return self.properties
1154 return self.properties
1155
1155
1156 def getText(self):
1156 def getText(self):
1157 """Text accessor"""
1157 """Text accessor"""
1158 return self.text
1158 return self.text
1159
1159
1160 def getServer(self):
1160 def getServer(self):
1161 """Server accessor"""
1161 """Server accessor"""
1162 return self.server
1162 return self.server
1163
1163
1164 def updateRecord(self, zeroconf, now, record):
1164 def updateRecord(self, zeroconf, now, record):
1165 """Updates service information from a DNS record"""
1165 """Updates service information from a DNS record"""
1166 if record is not None and not record.isExpired(now):
1166 if record is not None and not record.isExpired(now):
1167 if record.type == _TYPE_A:
1167 if record.type == _TYPE_A:
1168 #if record.name == self.name:
1168 #if record.name == self.name:
1169 if record.name == self.server:
1169 if record.name == self.server:
1170 self.address = record.address
1170 self.address = record.address
1171 elif record.type == _TYPE_SRV:
1171 elif record.type == _TYPE_SRV:
1172 if record.name == self.name:
1172 if record.name == self.name:
1173 self.server = record.server
1173 self.server = record.server
1174 self.port = record.port
1174 self.port = record.port
1175 self.weight = record.weight
1175 self.weight = record.weight
1176 self.priority = record.priority
1176 self.priority = record.priority
1177 #self.address = None
1177 #self.address = None
1178 self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
1178 self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
1179 elif record.type == _TYPE_TXT:
1179 elif record.type == _TYPE_TXT:
1180 if record.name == self.name:
1180 if record.name == self.name:
1181 self.setText(record.text)
1181 self.setText(record.text)
1182
1182
1183 def request(self, zeroconf, timeout):
1183 def request(self, zeroconf, timeout):
1184 """Returns true if the service could be discovered on the
1184 """Returns true if the service could be discovered on the
1185 network, and updates this object with details discovered.
1185 network, and updates this object with details discovered.
1186 """
1186 """
1187 now = currentTimeMillis()
1187 now = currentTimeMillis()
1188 delay = _LISTENER_TIME
1188 delay = _LISTENER_TIME
1189 next = now + delay
1189 next = now + delay
1190 last = now + timeout
1190 last = now + timeout
1191 result = 0
1191 result = 0
1192 try:
1192 try:
1193 zeroconf.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
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:
1194 while self.server is None or self.address is None or self.text is None:
1195 if last <= now:
1195 if last <= now:
1196 return 0
1196 return 0
1197 if next <= now:
1197 if next <= now:
1198 out = DNSOutgoing(_FLAGS_QR_QUERY)
1198 out = DNSOutgoing(_FLAGS_QR_QUERY)
1199 out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
1199 out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
1200 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_SRV, _CLASS_IN), now)
1200 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_SRV, _CLASS_IN), now)
1201 out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
1201 out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
1202 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_TXT, _CLASS_IN), now)
1202 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_TXT, _CLASS_IN), now)
1203 if self.server is not None:
1203 if self.server is not None:
1204 out.addQuestion(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
1204 out.addQuestion(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
1205 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN), now)
1205 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN), now)
1206 zeroconf.send(out)
1206 zeroconf.send(out)
1207 next = now + delay
1207 next = now + delay
1208 delay = delay * 2
1208 delay = delay * 2
1209
1209
1210 zeroconf.wait(min(next, last) - now)
1210 zeroconf.wait(min(next, last) - now)
1211 now = currentTimeMillis()
1211 now = currentTimeMillis()
1212 result = 1
1212 result = 1
1213 finally:
1213 finally:
1214 zeroconf.removeListener(self)
1214 zeroconf.removeListener(self)
1215
1215
1216 return result
1216 return result
1217
1217
1218 def __eq__(self, other):
1218 def __eq__(self, other):
1219 """Tests equality of service name"""
1219 """Tests equality of service name"""
1220 if isinstance(other, ServiceInfo):
1220 if isinstance(other, ServiceInfo):
1221 return other.name == self.name
1221 return other.name == self.name
1222 return 0
1222 return 0
1223
1223
1224 def __ne__(self, other):
1224 def __ne__(self, other):
1225 """Non-equality test"""
1225 """Non-equality test"""
1226 return not self.__eq__(other)
1226 return not self.__eq__(other)
1227
1227
1228 def __repr__(self):
1228 def __repr__(self):
1229 """String representation"""
1229 """String representation"""
1230 result = "service[%s,%s:%s," % (self.name, socket.inet_ntoa(self.getAddress()), self.port)
1230 result = "service[%s,%s:%s," % (self.name, socket.inet_ntoa(self.getAddress()), self.port)
1231 if self.text is None:
1231 if self.text is None:
1232 result += "None"
1232 result += "None"
1233 else:
1233 else:
1234 if len(self.text) < 20:
1234 if len(self.text) < 20:
1235 result += self.text
1235 result += self.text
1236 else:
1236 else:
1237 result += self.text[:17] + "..."
1237 result += self.text[:17] + "..."
1238 result += "]"
1238 result += "]"
1239 return result
1239 return result
1240
1240
1241
1241
1242 class Zeroconf(object):
1242 class Zeroconf(object):
1243 """Implementation of Zeroconf Multicast DNS Service Discovery
1243 """Implementation of Zeroconf Multicast DNS Service Discovery
1244
1244
1245 Supports registration, unregistration, queries and browsing.
1245 Supports registration, unregistration, queries and browsing.
1246 """
1246 """
1247 def __init__(self, bindaddress=None):
1247 def __init__(self, bindaddress=None):
1248 """Creates an instance of the Zeroconf class, establishing
1248 """Creates an instance of the Zeroconf class, establishing
1249 multicast communications, listening and reaping threads."""
1249 multicast communications, listening and reaping threads."""
1250 globals()['_GLOBAL_DONE'] = 0
1250 globals()['_GLOBAL_DONE'] = 0
1251 if bindaddress is None:
1251 if bindaddress is None:
1252 self.intf = socket.gethostbyname(socket.gethostname())
1252 self.intf = socket.gethostbyname(socket.gethostname())
1253 else:
1253 else:
1254 self.intf = bindaddress
1254 self.intf = bindaddress
1255 self.group = ('', _MDNS_PORT)
1255 self.group = ('', _MDNS_PORT)
1256 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1256 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1257 try:
1257 try:
1258 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1258 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1259 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1259 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1260 except Exception:
1260 except Exception:
1261 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1261 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1262 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1262 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1263 # Volume 2"), but some BSD-derived systems require
1263 # Volume 2"), but some BSD-derived systems require
1264 # SO_REUSEPORT to be specified explicitly. Also, not all
1264 # SO_REUSEPORT to be specified explicitly. Also, not all
1265 # versions of Python have SO_REUSEPORT available. So
1265 # versions of Python have SO_REUSEPORT available. So
1266 # if you're on a BSD-based system, and haven't upgraded
1266 # if you're on a BSD-based system, and haven't upgraded
1267 # to Python 2.3 yet, you may find this library doesn't
1267 # to Python 2.3 yet, you may find this library doesn't
1268 # work as expected.
1268 # work as expected.
1269 #
1269 #
1270 pass
1270 pass
1271 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, "\xff")
1271 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, "\xff")
1272 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, "\x01")
1272 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, "\x01")
1273 try:
1273 try:
1274 self.socket.bind(self.group)
1274 self.socket.bind(self.group)
1275 except Exception:
1275 except Exception:
1276 # Some versions of linux raise an exception even though
1276 # Some versions of linux raise an exception even though
1277 # SO_REUSEADDR and SO_REUSEPORT have been set, so ignore it
1277 # SO_REUSEADDR and SO_REUSEPORT have been set, so ignore it
1278 pass
1278 pass
1279 self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1279 self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1280
1280
1281 self.listeners = []
1281 self.listeners = []
1282 self.browsers = []
1282 self.browsers = []
1283 self.services = {}
1283 self.services = {}
1284 self.servicetypes = {}
1284 self.servicetypes = {}
1285
1285
1286 self.cache = DNSCache()
1286 self.cache = DNSCache()
1287
1287
1288 self.condition = threading.Condition()
1288 self.condition = threading.Condition()
1289
1289
1290 self.engine = Engine(self)
1290 self.engine = Engine(self)
1291 self.listener = Listener(self)
1291 self.listener = Listener(self)
1292 self.reaper = Reaper(self)
1292 self.reaper = Reaper(self)
1293
1293
1294 def isLoopback(self):
1294 def isLoopback(self):
1295 return self.intf.startswith("127.0.0.1")
1295 return self.intf.startswith("127.0.0.1")
1296
1296
1297 def isLinklocal(self):
1297 def isLinklocal(self):
1298 return self.intf.startswith("169.254.")
1298 return self.intf.startswith("169.254.")
1299
1299
1300 def wait(self, timeout):
1300 def wait(self, timeout):
1301 """Calling thread waits for a given number of milliseconds or
1301 """Calling thread waits for a given number of milliseconds or
1302 until notified."""
1302 until notified."""
1303 self.condition.acquire()
1303 self.condition.acquire()
1304 self.condition.wait(timeout/1000)
1304 self.condition.wait(timeout/1000)
1305 self.condition.release()
1305 self.condition.release()
1306
1306
1307 def notifyAll(self):
1307 def notifyAll(self):
1308 """Notifies all waiting threads"""
1308 """Notifies all waiting threads"""
1309 self.condition.acquire()
1309 self.condition.acquire()
1310 self.condition.notifyAll()
1310 self.condition.notifyAll()
1311 self.condition.release()
1311 self.condition.release()
1312
1312
1313 def getServiceInfo(self, type, name, timeout=3000):
1313 def getServiceInfo(self, type, name, timeout=3000):
1314 """Returns network's service information for a particular
1314 """Returns network's service information for a particular
1315 name and type, or None if no service matches by the timeout,
1315 name and type, or None if no service matches by the timeout,
1316 which defaults to 3 seconds."""
1316 which defaults to 3 seconds."""
1317 info = ServiceInfo(type, name)
1317 info = ServiceInfo(type, name)
1318 if info.request(self, timeout):
1318 if info.request(self, timeout):
1319 return info
1319 return info
1320 return None
1320 return None
1321
1321
1322 def addServiceListener(self, type, listener):
1322 def addServiceListener(self, type, listener):
1323 """Adds a listener for a particular service type. This object
1323 """Adds a listener for a particular service type. This object
1324 will then have its updateRecord method called when information
1324 will then have its updateRecord method called when information
1325 arrives for that type."""
1325 arrives for that type."""
1326 self.removeServiceListener(listener)
1326 self.removeServiceListener(listener)
1327 self.browsers.append(ServiceBrowser(self, type, listener))
1327 self.browsers.append(ServiceBrowser(self, type, listener))
1328
1328
1329 def removeServiceListener(self, listener):
1329 def removeServiceListener(self, listener):
1330 """Removes a listener from the set that is currently listening."""
1330 """Removes a listener from the set that is currently listening."""
1331 for browser in self.browsers:
1331 for browser in self.browsers:
1332 if browser.listener == listener:
1332 if browser.listener == listener:
1333 browser.cancel()
1333 browser.cancel()
1334 del(browser)
1334 del(browser)
1335
1335
1336 def registerService(self, info, ttl=_DNS_TTL):
1336 def registerService(self, info, ttl=_DNS_TTL):
1337 """Registers service information to the network with a default TTL
1337 """Registers service information to the network with a default TTL
1338 of 60 seconds. Zeroconf will then respond to requests for
1338 of 60 seconds. Zeroconf will then respond to requests for
1339 information for that service. The name of the service may be
1339 information for that service. The name of the service may be
1340 changed if needed to make it unique on the network."""
1340 changed if needed to make it unique on the network."""
1341 self.checkService(info)
1341 self.checkService(info)
1342 self.services[info.name.lower()] = info
1342 self.services[info.name.lower()] = info
1343 if self.servicetypes.has_key(info.type):
1343 if self.servicetypes.has_key(info.type):
1344 self.servicetypes[info.type]+=1
1344 self.servicetypes[info.type]+=1
1345 else:
1345 else:
1346 self.servicetypes[info.type]=1
1346 self.servicetypes[info.type]=1
1347 now = currentTimeMillis()
1347 now = currentTimeMillis()
1348 nextTime = now
1348 nextTime = now
1349 i = 0
1349 i = 0
1350 while i < 3:
1350 while i < 3:
1351 if now < nextTime:
1351 if now < nextTime:
1352 self.wait(nextTime - now)
1352 self.wait(nextTime - now)
1353 now = currentTimeMillis()
1353 now = currentTimeMillis()
1354 continue
1354 continue
1355 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1355 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1356 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0)
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)
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)
1358 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0)
1359 if info.address:
1359 if info.address:
1360 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, ttl, info.address), 0)
1360 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, ttl, info.address), 0)
1361 self.send(out)
1361 self.send(out)
1362 i += 1
1362 i += 1
1363 nextTime += _REGISTER_TIME
1363 nextTime += _REGISTER_TIME
1364
1364
1365 def unregisterService(self, info):
1365 def unregisterService(self, info):
1366 """Unregister a service."""
1366 """Unregister a service."""
1367 try:
1367 try:
1368 del(self.services[info.name.lower()])
1368 del(self.services[info.name.lower()])
1369 if self.servicetypes[info.type]>1:
1369 if self.servicetypes[info.type]>1:
1370 self.servicetypes[info.type]-=1
1370 self.servicetypes[info.type]-=1
1371 else:
1371 else:
1372 del self.servicetypes[info.type]
1372 del self.servicetypes[info.type]
1373 except KeyError:
1373 except KeyError:
1374 pass
1374 pass
1375 now = currentTimeMillis()
1375 now = currentTimeMillis()
1376 nextTime = now
1376 nextTime = now
1377 i = 0
1377 i = 0
1378 while i < 3:
1378 while i < 3:
1379 if now < nextTime:
1379 if now < nextTime:
1380 self.wait(nextTime - now)
1380 self.wait(nextTime - now)
1381 now = currentTimeMillis()
1381 now = currentTimeMillis()
1382 continue
1382 continue
1383 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1383 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1384 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
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)
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)
1386 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1387 if info.address:
1387 if info.address:
1388 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1388 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1389 self.send(out)
1389 self.send(out)
1390 i += 1
1390 i += 1
1391 nextTime += _UNREGISTER_TIME
1391 nextTime += _UNREGISTER_TIME
1392
1392
1393 def unregisterAllServices(self):
1393 def unregisterAllServices(self):
1394 """Unregister all registered services."""
1394 """Unregister all registered services."""
1395 if len(self.services) > 0:
1395 if len(self.services) > 0:
1396 now = currentTimeMillis()
1396 now = currentTimeMillis()
1397 nextTime = now
1397 nextTime = now
1398 i = 0
1398 i = 0
1399 while i < 3:
1399 while i < 3:
1400 if now < nextTime:
1400 if now < nextTime:
1401 self.wait(nextTime - now)
1401 self.wait(nextTime - now)
1402 now = currentTimeMillis()
1402 now = currentTimeMillis()
1403 continue
1403 continue
1404 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1404 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1405 for info in self.services.values():
1405 for info in self.services.values():
1406 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
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)
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)
1408 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1409 if info.address:
1409 if info.address:
1410 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1410 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1411 self.send(out)
1411 self.send(out)
1412 i += 1
1412 i += 1
1413 nextTime += _UNREGISTER_TIME
1413 nextTime += _UNREGISTER_TIME
1414
1414
1415 def checkService(self, info):
1415 def checkService(self, info):
1416 """Checks the network for a unique service name, modifying the
1416 """Checks the network for a unique service name, modifying the
1417 ServiceInfo passed in if it is not unique."""
1417 ServiceInfo passed in if it is not unique."""
1418 now = currentTimeMillis()
1418 now = currentTimeMillis()
1419 nextTime = now
1419 nextTime = now
1420 i = 0
1420 i = 0
1421 while i < 3:
1421 while i < 3:
1422 for record in self.cache.entriesWithName(info.type):
1422 for record in self.cache.entriesWithName(info.type):
1423 if record.type == _TYPE_PTR and not record.isExpired(now) and record.alias == info.name:
1423 if record.type == _TYPE_PTR and not record.isExpired(now) and record.alias == info.name:
1424 if (info.name.find('.') < 0):
1424 if (info.name.find('.') < 0):
1425 info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type
1425 info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type
1426 self.checkService(info)
1426 self.checkService(info)
1427 return
1427 return
1428 raise NonUniqueNameException
1428 raise NonUniqueNameException
1429 if now < nextTime:
1429 if now < nextTime:
1430 self.wait(nextTime - now)
1430 self.wait(nextTime - now)
1431 now = currentTimeMillis()
1431 now = currentTimeMillis()
1432 continue
1432 continue
1433 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1433 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1434 self.debug = out
1434 self.debug = out
1435 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1435 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1436 out.addAuthoritativeAnswer(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name))
1436 out.addAuthoritativeAnswer(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name))
1437 self.send(out)
1437 self.send(out)
1438 i += 1
1438 i += 1
1439 nextTime += _CHECK_TIME
1439 nextTime += _CHECK_TIME
1440
1440
1441 def addListener(self, listener, question):
1441 def addListener(self, listener, question):
1442 """Adds a listener for a given question. The listener will have
1442 """Adds a listener for a given question. The listener will have
1443 its updateRecord method called when information is available to
1443 its updateRecord method called when information is available to
1444 answer the question."""
1444 answer the question."""
1445 now = currentTimeMillis()
1445 now = currentTimeMillis()
1446 self.listeners.append(listener)
1446 self.listeners.append(listener)
1447 if question is not None:
1447 if question is not None:
1448 for record in self.cache.entriesWithName(question.name):
1448 for record in self.cache.entriesWithName(question.name):
1449 if question.answeredBy(record) and not record.isExpired(now):
1449 if question.answeredBy(record) and not record.isExpired(now):
1450 listener.updateRecord(self, now, record)
1450 listener.updateRecord(self, now, record)
1451 self.notifyAll()
1451 self.notifyAll()
1452
1452
1453 def removeListener(self, listener):
1453 def removeListener(self, listener):
1454 """Removes a listener."""
1454 """Removes a listener."""
1455 try:
1455 try:
1456 self.listeners.remove(listener)
1456 self.listeners.remove(listener)
1457 self.notifyAll()
1457 self.notifyAll()
1458 except Exception:
1458 except Exception:
1459 pass
1459 pass
1460
1460
1461 def updateRecord(self, now, rec):
1461 def updateRecord(self, now, rec):
1462 """Used to notify listeners of new information that has updated
1462 """Used to notify listeners of new information that has updated
1463 a record."""
1463 a record."""
1464 for listener in self.listeners:
1464 for listener in self.listeners:
1465 listener.updateRecord(self, now, rec)
1465 listener.updateRecord(self, now, rec)
1466 self.notifyAll()
1466 self.notifyAll()
1467
1467
1468 def handleResponse(self, msg):
1468 def handleResponse(self, msg):
1469 """Deal with incoming response packets. All answers
1469 """Deal with incoming response packets. All answers
1470 are held in the cache, and listeners are notified."""
1470 are held in the cache, and listeners are notified."""
1471 now = currentTimeMillis()
1471 now = currentTimeMillis()
1472 for record in msg.answers:
1472 for record in msg.answers:
1473 expired = record.isExpired(now)
1473 expired = record.isExpired(now)
1474 if record in self.cache.entries():
1474 if record in self.cache.entries():
1475 if expired:
1475 if expired:
1476 self.cache.remove(record)
1476 self.cache.remove(record)
1477 else:
1477 else:
1478 entry = self.cache.get(record)
1478 entry = self.cache.get(record)
1479 if entry is not None:
1479 if entry is not None:
1480 entry.resetTTL(record)
1480 entry.resetTTL(record)
1481 record = entry
1481 record = entry
1482 else:
1482 else:
1483 self.cache.add(record)
1483 self.cache.add(record)
1484
1484
1485 self.updateRecord(now, record)
1485 self.updateRecord(now, record)
1486
1486
1487 def handleQuery(self, msg, addr, port):
1487 def handleQuery(self, msg, addr, port):
1488 """Deal with incoming query packets. Provides a response if
1488 """Deal with incoming query packets. Provides a response if
1489 possible."""
1489 possible."""
1490 out = None
1490 out = None
1491
1491
1492 # Support unicast client responses
1492 # Support unicast client responses
1493 #
1493 #
1494 if port != _MDNS_PORT:
1494 if port != _MDNS_PORT:
1495 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
1495 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
1496 for question in msg.questions:
1496 for question in msg.questions:
1497 out.addQuestion(question)
1497 out.addQuestion(question)
1498
1498
1499 for question in msg.questions:
1499 for question in msg.questions:
1500 if question.type == _TYPE_PTR:
1500 if question.type == _TYPE_PTR:
1501 if question.name == "_services._dns-sd._udp.local.":
1501 if question.name == "_services._dns-sd._udp.local.":
1502 for stype in self.servicetypes.keys():
1502 for stype in self.servicetypes.keys():
1503 if out is None:
1503 if out is None:
1504 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1504 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1505 out.addAnswer(msg, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
1505 out.addAnswer(msg, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
1506 for service in self.services.values():
1506 for service in self.services.values():
1507 if question.name == service.type:
1507 if question.name == service.type:
1508 if out is None:
1508 if out is None:
1509 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1509 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1510 out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name))
1510 out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name))
1511 else:
1511 else:
1512 try:
1512 try:
1513 if out is None:
1513 if out is None:
1514 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1514 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1515
1515
1516 # Answer A record queries for any service addresses we know
1516 # Answer A record queries for any service addresses we know
1517 if question.type == _TYPE_A or question.type == _TYPE_ANY:
1517 if question.type == _TYPE_A or question.type == _TYPE_ANY:
1518 for service in self.services.values():
1518 for service in self.services.values():
1519 if service.server == question.name.lower():
1519 if service.server == question.name.lower():
1520 out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1520 out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1521
1521
1522 service = self.services.get(question.name.lower(), None)
1522 service = self.services.get(question.name.lower(), None)
1523 if not service: continue
1523 if not service: continue
1524
1524
1525 if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
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))
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:
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))
1528 out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text))
1529 if question.type == _TYPE_SRV:
1529 if question.type == _TYPE_SRV:
1530 out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1530 out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1531 except Exception:
1531 except Exception:
1532 traceback.print_exc()
1532 traceback.print_exc()
1533
1533
1534 if out is not None and out.answers:
1534 if out is not None and out.answers:
1535 out.id = msg.id
1535 out.id = msg.id
1536 self.send(out, addr, port)
1536 self.send(out, addr, port)
1537
1537
1538 def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
1538 def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
1539 """Sends an outgoing packet."""
1539 """Sends an outgoing packet."""
1540 # This is a quick test to see if we can parse the packets we generate
1540 # This is a quick test to see if we can parse the packets we generate
1541 #temp = DNSIncoming(out.packet())
1541 #temp = DNSIncoming(out.packet())
1542 try:
1542 try:
1543 self.socket.sendto(out.packet(), 0, (addr, port))
1543 self.socket.sendto(out.packet(), 0, (addr, port))
1544 except Exception:
1544 except Exception:
1545 # Ignore this, it may be a temporary loss of network connection
1545 # Ignore this, it may be a temporary loss of network connection
1546 pass
1546 pass
1547
1547
1548 def close(self):
1548 def close(self):
1549 """Ends the background threads, and prevent this instance from
1549 """Ends the background threads, and prevent this instance from
1550 servicing further queries."""
1550 servicing further queries."""
1551 if globals()['_GLOBAL_DONE'] == 0:
1551 if globals()['_GLOBAL_DONE'] == 0:
1552 globals()['_GLOBAL_DONE'] = 1
1552 globals()['_GLOBAL_DONE'] = 1
1553 self.notifyAll()
1553 self.notifyAll()
1554 self.engine.notify()
1554 self.engine.notify()
1555 self.unregisterAllServices()
1555 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'))
1556 self.socket.setsockopt(socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1557 self.socket.close()
1557 self.socket.close()
1558
1558
1559 # Test a few module features, including service registration, service
1559 # Test a few module features, including service registration, service
1560 # query (for Zoe), and service unregistration.
1560 # query (for Zoe), and service unregistration.
1561
1561
1562 if __name__ == '__main__':
1562 if __name__ == '__main__':
1563 print("Multicast DNS Service Discovery for Python, version", __version__)
1563 print("Multicast DNS Service Discovery for Python, version", __version__)
1564 r = Zeroconf()
1564 r = Zeroconf()
1565 print("1. Testing registration of a service...")
1565 print("1. Testing registration of a service...")
1566 desc = {'version':'0.10','a':'test value', 'b':'another value'}
1566 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)
1567 info = ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
1568 print(" Registering service...")
1568 print(" Registering service...")
1569 r.registerService(info)
1569 r.registerService(info)
1570 print(" Registration done.")
1570 print(" Registration done.")
1571 print("2. Testing query of service information...")
1571 print("2. Testing query of service information...")
1572 print(" Getting ZOE service:", str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local.")))
1572 print(" Getting ZOE service:", str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local.")))
1573 print(" Query done.")
1573 print(" Query done.")
1574 print("3. Testing query of own service...")
1574 print("3. Testing query of own service...")
1575 print(" Getting self:", str(r.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.")))
1575 print(" Getting self:", str(r.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.")))
1576 print(" Query done.")
1576 print(" Query done.")
1577 print("4. Testing unregister of service information...")
1577 print("4. Testing unregister of service information...")
1578 r.unregisterService(info)
1578 r.unregisterService(info)
1579 print(" Unregister done.")
1579 print(" Unregister done.")
1580 r.close()
1580 r.close()
1581
1581
1582 # no-check-code
1582 # no-check-code
@@ -1,204 +1,213
1 # zeroconf.py - zeroconf support for Mercurial
1 # zeroconf.py - zeroconf support for Mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
8 '''discover and advertise repositories on the local network
7 '''discover and advertise repositories on the local network
9
8
10 Zeroconf-enabled repositories will be announced in a network without
9 Zeroconf-enabled repositories will be announced in a network without
11 the need to configure a server or a service. They can be discovered
10 the need to configure a server or a service. They can be discovered
12 without knowing their actual IP address.
11 without knowing their actual IP address.
13
12
14 To allow other people to discover your repository using run
13 To allow other people to discover your repository using run
15 :hg:`serve` in your repository::
14 :hg:`serve` in your repository::
16
15
17 $ cd test
16 $ cd test
18 $ hg serve
17 $ hg serve
19
18
20 You can discover Zeroconf-enabled repositories by running
19 You can discover Zeroconf-enabled repositories by running
21 :hg:`paths`::
20 :hg:`paths`::
22
21
23 $ hg paths
22 $ hg paths
24 zc-test = http://example.com:8000/test
23 zc-test = http://example.com:8000/test
25 '''
24 '''
25 from __future__ import absolute_import
26
26
27 import socket, time, os
27 import os
28 import socket
29 import time
28
30
29 import Zeroconf
31 from . import Zeroconf
30 from mercurial import ui, hg, encoding, dispatch
32 from mercurial import (
31 from mercurial import extensions
33 dispatch,
32 from mercurial.hgweb import server as servermod
34 encoding,
35 extensions,
36 hg,
37 ui,
38 )
39 from mercurial.hgweb import (
40 server as servermod
41 )
33
42
34 # Note for extension authors: ONLY specify testedwith = 'internal' for
43 # Note for extension authors: ONLY specify testedwith = 'internal' for
35 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
36 # be specifying the version(s) of Mercurial they are tested with, or
45 # be specifying the version(s) of Mercurial they are tested with, or
37 # leave the attribute unspecified.
46 # leave the attribute unspecified.
38 testedwith = 'internal'
47 testedwith = 'internal'
39
48
40 # publish
49 # publish
41
50
42 server = None
51 server = None
43 localip = None
52 localip = None
44
53
45 def getip():
54 def getip():
46 # finds external-facing interface without sending any packets (Linux)
55 # finds external-facing interface without sending any packets (Linux)
47 try:
56 try:
48 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
57 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
49 s.connect(('1.0.0.1', 0))
58 s.connect(('1.0.0.1', 0))
50 ip = s.getsockname()[0]
59 ip = s.getsockname()[0]
51 return ip
60 return ip
52 except socket.error:
61 except socket.error:
53 pass
62 pass
54
63
55 # Generic method, sometimes gives useless results
64 # Generic method, sometimes gives useless results
56 try:
65 try:
57 dumbip = socket.gethostbyaddr(socket.gethostname())[2][0]
66 dumbip = socket.gethostbyaddr(socket.gethostname())[2][0]
58 if not dumbip.startswith('127.') and ':' not in dumbip:
67 if not dumbip.startswith('127.') and ':' not in dumbip:
59 return dumbip
68 return dumbip
60 except (socket.gaierror, socket.herror):
69 except (socket.gaierror, socket.herror):
61 dumbip = '127.0.0.1'
70 dumbip = '127.0.0.1'
62
71
63 # works elsewhere, but actually sends a packet
72 # works elsewhere, but actually sends a packet
64 try:
73 try:
65 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
74 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
66 s.connect(('1.0.0.1', 1))
75 s.connect(('1.0.0.1', 1))
67 ip = s.getsockname()[0]
76 ip = s.getsockname()[0]
68 return ip
77 return ip
69 except socket.error:
78 except socket.error:
70 pass
79 pass
71
80
72 return dumbip
81 return dumbip
73
82
74 def publish(name, desc, path, port):
83 def publish(name, desc, path, port):
75 global server, localip
84 global server, localip
76 if not server:
85 if not server:
77 ip = getip()
86 ip = getip()
78 if ip.startswith('127.'):
87 if ip.startswith('127.'):
79 # if we have no internet connection, this can happen.
88 # if we have no internet connection, this can happen.
80 return
89 return
81 localip = socket.inet_aton(ip)
90 localip = socket.inet_aton(ip)
82 server = Zeroconf.Zeroconf(ip)
91 server = Zeroconf.Zeroconf(ip)
83
92
84 hostname = socket.gethostname().split('.')[0]
93 hostname = socket.gethostname().split('.')[0]
85 host = hostname + ".local"
94 host = hostname + ".local"
86 name = "%s-%s" % (hostname, name)
95 name = "%s-%s" % (hostname, name)
87
96
88 # advertise to browsers
97 # advertise to browsers
89 svc = Zeroconf.ServiceInfo('_http._tcp.local.',
98 svc = Zeroconf.ServiceInfo('_http._tcp.local.',
90 name + '._http._tcp.local.',
99 name + '._http._tcp.local.',
91 server = host,
100 server = host,
92 port = port,
101 port = port,
93 properties = {'description': desc,
102 properties = {'description': desc,
94 'path': "/" + path},
103 'path': "/" + path},
95 address = localip, weight = 0, priority = 0)
104 address = localip, weight = 0, priority = 0)
96 server.registerService(svc)
105 server.registerService(svc)
97
106
98 # advertise to Mercurial clients
107 # advertise to Mercurial clients
99 svc = Zeroconf.ServiceInfo('_hg._tcp.local.',
108 svc = Zeroconf.ServiceInfo('_hg._tcp.local.',
100 name + '._hg._tcp.local.',
109 name + '._hg._tcp.local.',
101 server = host,
110 server = host,
102 port = port,
111 port = port,
103 properties = {'description': desc,
112 properties = {'description': desc,
104 'path': "/" + path},
113 'path': "/" + path},
105 address = localip, weight = 0, priority = 0)
114 address = localip, weight = 0, priority = 0)
106 server.registerService(svc)
115 server.registerService(svc)
107
116
108 def zc_create_server(create_server, ui, app):
117 def zc_create_server(create_server, ui, app):
109 httpd = create_server(ui, app)
118 httpd = create_server(ui, app)
110 port = httpd.port
119 port = httpd.port
111
120
112 try:
121 try:
113 repos = app.repos
122 repos = app.repos
114 except AttributeError:
123 except AttributeError:
115 # single repo
124 # single repo
116 with app._obtainrepo() as repo:
125 with app._obtainrepo() as repo:
117 name = app.reponame or os.path.basename(repo.root)
126 name = app.reponame or os.path.basename(repo.root)
118 path = repo.ui.config("web", "prefix", "").strip('/')
127 path = repo.ui.config("web", "prefix", "").strip('/')
119 desc = repo.ui.config("web", "description", name)
128 desc = repo.ui.config("web", "description", name)
120 publish(name, desc, path, port)
129 publish(name, desc, path, port)
121 else:
130 else:
122 # webdir
131 # webdir
123 prefix = app.ui.config("web", "prefix", "").strip('/') + '/'
132 prefix = app.ui.config("web", "prefix", "").strip('/') + '/'
124 for repo, path in repos:
133 for repo, path in repos:
125 u = app.ui.copy()
134 u = app.ui.copy()
126 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
135 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
127 name = os.path.basename(repo)
136 name = os.path.basename(repo)
128 path = (prefix + repo).strip('/')
137 path = (prefix + repo).strip('/')
129 desc = u.config('web', 'description', name)
138 desc = u.config('web', 'description', name)
130 publish(name, desc, path, port)
139 publish(name, desc, path, port)
131 return httpd
140 return httpd
132
141
133 # listen
142 # listen
134
143
135 class listener(object):
144 class listener(object):
136 def __init__(self):
145 def __init__(self):
137 self.found = {}
146 self.found = {}
138 def removeService(self, server, type, name):
147 def removeService(self, server, type, name):
139 if repr(name) in self.found:
148 if repr(name) in self.found:
140 del self.found[repr(name)]
149 del self.found[repr(name)]
141 def addService(self, server, type, name):
150 def addService(self, server, type, name):
142 self.found[repr(name)] = server.getServiceInfo(type, name)
151 self.found[repr(name)] = server.getServiceInfo(type, name)
143
152
144 def getzcpaths():
153 def getzcpaths():
145 ip = getip()
154 ip = getip()
146 if ip.startswith('127.'):
155 if ip.startswith('127.'):
147 return
156 return
148 server = Zeroconf.Zeroconf(ip)
157 server = Zeroconf.Zeroconf(ip)
149 l = listener()
158 l = listener()
150 Zeroconf.ServiceBrowser(server, "_hg._tcp.local.", l)
159 Zeroconf.ServiceBrowser(server, "_hg._tcp.local.", l)
151 time.sleep(1)
160 time.sleep(1)
152 server.close()
161 server.close()
153 for value in l.found.values():
162 for value in l.found.values():
154 name = value.name[:value.name.index('.')]
163 name = value.name[:value.name.index('.')]
155 url = "http://%s:%s%s" % (socket.inet_ntoa(value.address), value.port,
164 url = "http://%s:%s%s" % (socket.inet_ntoa(value.address), value.port,
156 value.properties.get("path", "/"))
165 value.properties.get("path", "/"))
157 yield "zc-" + name, url
166 yield "zc-" + name, url
158
167
159 def config(orig, self, section, key, default=None, untrusted=False):
168 def config(orig, self, section, key, default=None, untrusted=False):
160 if section == "paths" and key.startswith("zc-"):
169 if section == "paths" and key.startswith("zc-"):
161 for name, path in getzcpaths():
170 for name, path in getzcpaths():
162 if name == key:
171 if name == key:
163 return path
172 return path
164 return orig(self, section, key, default, untrusted)
173 return orig(self, section, key, default, untrusted)
165
174
166 def configitems(orig, self, section, *args, **kwargs):
175 def configitems(orig, self, section, *args, **kwargs):
167 repos = orig(self, section, *args, **kwargs)
176 repos = orig(self, section, *args, **kwargs)
168 if section == "paths":
177 if section == "paths":
169 repos += getzcpaths()
178 repos += getzcpaths()
170 return repos
179 return repos
171
180
172 def configsuboptions(orig, self, section, name, *args, **kwargs):
181 def configsuboptions(orig, self, section, name, *args, **kwargs):
173 opt, sub = orig(self, section, name, *args, **kwargs)
182 opt, sub = orig(self, section, name, *args, **kwargs)
174 if section == "paths" and name.startswith("zc-"):
183 if section == "paths" and name.startswith("zc-"):
175 # We have to find the URL in the zeroconf paths. We can't cons up any
184 # We have to find the URL in the zeroconf paths. We can't cons up any
176 # suboptions, so we use any that we found in the original config.
185 # suboptions, so we use any that we found in the original config.
177 for zcname, zcurl in getzcpaths():
186 for zcname, zcurl in getzcpaths():
178 if zcname == name:
187 if zcname == name:
179 return zcurl, sub
188 return zcurl, sub
180 return opt, sub
189 return opt, sub
181
190
182 def defaultdest(orig, source):
191 def defaultdest(orig, source):
183 for name, path in getzcpaths():
192 for name, path in getzcpaths():
184 if path == source:
193 if path == source:
185 return name.encode(encoding.encoding)
194 return name.encode(encoding.encoding)
186 return orig(source)
195 return orig(source)
187
196
188 def cleanupafterdispatch(orig, ui, options, cmd, cmdfunc):
197 def cleanupafterdispatch(orig, ui, options, cmd, cmdfunc):
189 try:
198 try:
190 return orig(ui, options, cmd, cmdfunc)
199 return orig(ui, options, cmd, cmdfunc)
191 finally:
200 finally:
192 # we need to call close() on the server to notify() the various
201 # we need to call close() on the server to notify() the various
193 # threading Conditions and allow the background threads to exit
202 # threading Conditions and allow the background threads to exit
194 global server
203 global server
195 if server:
204 if server:
196 server.close()
205 server.close()
197
206
198 extensions.wrapfunction(dispatch, '_runcommand', cleanupafterdispatch)
207 extensions.wrapfunction(dispatch, '_runcommand', cleanupafterdispatch)
199
208
200 extensions.wrapfunction(ui.ui, 'config', config)
209 extensions.wrapfunction(ui.ui, 'config', config)
201 extensions.wrapfunction(ui.ui, 'configitems', configitems)
210 extensions.wrapfunction(ui.ui, 'configitems', configitems)
202 extensions.wrapfunction(ui.ui, 'configsuboptions', configsuboptions)
211 extensions.wrapfunction(ui.ui, 'configsuboptions', configsuboptions)
203 extensions.wrapfunction(hg, 'defaultdest', defaultdest)
212 extensions.wrapfunction(hg, 'defaultdest', defaultdest)
204 extensions.wrapfunction(servermod, 'create_server', zc_create_server)
213 extensions.wrapfunction(servermod, 'create_server', zc_create_server)
@@ -1,173 +1,171
1 #require test-repo
1 #require test-repo
2
2
3 $ cd "$TESTDIR"/..
3 $ cd "$TESTDIR"/..
4
4
5 $ hg files 'set:(**.py)' | sed 's|\\|/|g' | xargs python contrib/check-py3-compat.py
5 $ hg files 'set:(**.py)' | sed 's|\\|/|g' | xargs python contrib/check-py3-compat.py
6 contrib/casesmash.py not using absolute_import
6 contrib/casesmash.py not using absolute_import
7 contrib/check-code.py not using absolute_import
7 contrib/check-code.py not using absolute_import
8 contrib/check-code.py requires print_function
8 contrib/check-code.py requires print_function
9 contrib/check-config.py not using absolute_import
9 contrib/check-config.py not using absolute_import
10 contrib/check-config.py requires print_function
10 contrib/check-config.py requires print_function
11 contrib/debugcmdserver.py not using absolute_import
11 contrib/debugcmdserver.py not using absolute_import
12 contrib/debugcmdserver.py requires print_function
12 contrib/debugcmdserver.py requires print_function
13 contrib/debugshell.py not using absolute_import
13 contrib/debugshell.py not using absolute_import
14 contrib/fixpax.py not using absolute_import
14 contrib/fixpax.py not using absolute_import
15 contrib/fixpax.py requires print_function
15 contrib/fixpax.py requires print_function
16 contrib/hgclient.py not using absolute_import
16 contrib/hgclient.py not using absolute_import
17 contrib/hgclient.py requires print_function
17 contrib/hgclient.py requires print_function
18 contrib/hgfixes/fix_bytes.py not using absolute_import
18 contrib/hgfixes/fix_bytes.py not using absolute_import
19 contrib/hgfixes/fix_bytesmod.py not using absolute_import
19 contrib/hgfixes/fix_bytesmod.py not using absolute_import
20 contrib/hgfixes/fix_leftover_imports.py not using absolute_import
20 contrib/hgfixes/fix_leftover_imports.py not using absolute_import
21 contrib/import-checker.py not using absolute_import
21 contrib/import-checker.py not using absolute_import
22 contrib/import-checker.py requires print_function
22 contrib/import-checker.py requires print_function
23 contrib/memory.py not using absolute_import
23 contrib/memory.py not using absolute_import
24 contrib/perf.py not using absolute_import
24 contrib/perf.py not using absolute_import
25 contrib/python-hook-examples.py not using absolute_import
25 contrib/python-hook-examples.py not using absolute_import
26 contrib/revsetbenchmarks.py not using absolute_import
26 contrib/revsetbenchmarks.py not using absolute_import
27 contrib/revsetbenchmarks.py requires print_function
27 contrib/revsetbenchmarks.py requires print_function
28 contrib/showstack.py not using absolute_import
28 contrib/showstack.py not using absolute_import
29 contrib/synthrepo.py not using absolute_import
29 contrib/synthrepo.py not using absolute_import
30 contrib/win32/hgwebdir_wsgi.py not using absolute_import
30 contrib/win32/hgwebdir_wsgi.py not using absolute_import
31 doc/check-seclevel.py not using absolute_import
31 doc/check-seclevel.py not using absolute_import
32 doc/gendoc.py not using absolute_import
32 doc/gendoc.py not using absolute_import
33 doc/hgmanpage.py not using absolute_import
33 doc/hgmanpage.py not using absolute_import
34 hgext/__init__.py not using absolute_import
34 hgext/__init__.py not using absolute_import
35 hgext/color.py not using absolute_import
35 hgext/color.py not using absolute_import
36 hgext/convert/__init__.py not using absolute_import
36 hgext/convert/__init__.py not using absolute_import
37 hgext/convert/bzr.py not using absolute_import
37 hgext/convert/bzr.py not using absolute_import
38 hgext/convert/common.py not using absolute_import
38 hgext/convert/common.py not using absolute_import
39 hgext/convert/convcmd.py not using absolute_import
39 hgext/convert/convcmd.py not using absolute_import
40 hgext/convert/cvs.py not using absolute_import
40 hgext/convert/cvs.py not using absolute_import
41 hgext/convert/cvsps.py not using absolute_import
41 hgext/convert/cvsps.py not using absolute_import
42 hgext/convert/darcs.py not using absolute_import
42 hgext/convert/darcs.py not using absolute_import
43 hgext/convert/filemap.py not using absolute_import
43 hgext/convert/filemap.py not using absolute_import
44 hgext/convert/git.py not using absolute_import
44 hgext/convert/git.py not using absolute_import
45 hgext/convert/gnuarch.py not using absolute_import
45 hgext/convert/gnuarch.py not using absolute_import
46 hgext/convert/hg.py not using absolute_import
46 hgext/convert/hg.py not using absolute_import
47 hgext/convert/monotone.py not using absolute_import
47 hgext/convert/monotone.py not using absolute_import
48 hgext/convert/p4.py not using absolute_import
48 hgext/convert/p4.py not using absolute_import
49 hgext/convert/subversion.py not using absolute_import
49 hgext/convert/subversion.py not using absolute_import
50 hgext/convert/transport.py not using absolute_import
50 hgext/convert/transport.py not using absolute_import
51 hgext/eol.py not using absolute_import
51 hgext/eol.py not using absolute_import
52 hgext/extdiff.py not using absolute_import
52 hgext/extdiff.py not using absolute_import
53 hgext/factotum.py not using absolute_import
53 hgext/factotum.py not using absolute_import
54 hgext/fetch.py not using absolute_import
54 hgext/fetch.py not using absolute_import
55 hgext/gpg.py not using absolute_import
55 hgext/gpg.py not using absolute_import
56 hgext/graphlog.py not using absolute_import
56 hgext/graphlog.py not using absolute_import
57 hgext/hgcia.py not using absolute_import
57 hgext/hgcia.py not using absolute_import
58 hgext/hgk.py not using absolute_import
58 hgext/hgk.py not using absolute_import
59 hgext/highlight/__init__.py not using absolute_import
59 hgext/highlight/__init__.py not using absolute_import
60 hgext/highlight/highlight.py not using absolute_import
60 hgext/highlight/highlight.py not using absolute_import
61 hgext/histedit.py not using absolute_import
61 hgext/histedit.py not using absolute_import
62 hgext/keyword.py not using absolute_import
62 hgext/keyword.py not using absolute_import
63 hgext/largefiles/__init__.py not using absolute_import
63 hgext/largefiles/__init__.py not using absolute_import
64 hgext/largefiles/basestore.py not using absolute_import
64 hgext/largefiles/basestore.py not using absolute_import
65 hgext/largefiles/lfcommands.py not using absolute_import
65 hgext/largefiles/lfcommands.py not using absolute_import
66 hgext/largefiles/lfutil.py not using absolute_import
66 hgext/largefiles/lfutil.py not using absolute_import
67 hgext/largefiles/localstore.py not using absolute_import
67 hgext/largefiles/localstore.py not using absolute_import
68 hgext/largefiles/overrides.py not using absolute_import
68 hgext/largefiles/overrides.py not using absolute_import
69 hgext/largefiles/proto.py not using absolute_import
69 hgext/largefiles/proto.py not using absolute_import
70 hgext/largefiles/remotestore.py not using absolute_import
70 hgext/largefiles/remotestore.py not using absolute_import
71 hgext/largefiles/reposetup.py not using absolute_import
71 hgext/largefiles/reposetup.py not using absolute_import
72 hgext/largefiles/uisetup.py not using absolute_import
72 hgext/largefiles/uisetup.py not using absolute_import
73 hgext/largefiles/wirestore.py not using absolute_import
73 hgext/largefiles/wirestore.py not using absolute_import
74 hgext/mq.py not using absolute_import
74 hgext/mq.py not using absolute_import
75 hgext/notify.py not using absolute_import
75 hgext/notify.py not using absolute_import
76 hgext/pager.py not using absolute_import
76 hgext/pager.py not using absolute_import
77 hgext/patchbomb.py not using absolute_import
77 hgext/patchbomb.py not using absolute_import
78 hgext/purge.py not using absolute_import
78 hgext/purge.py not using absolute_import
79 hgext/rebase.py not using absolute_import
79 hgext/rebase.py not using absolute_import
80 hgext/record.py not using absolute_import
80 hgext/record.py not using absolute_import
81 hgext/relink.py not using absolute_import
81 hgext/relink.py not using absolute_import
82 hgext/schemes.py not using absolute_import
82 hgext/schemes.py not using absolute_import
83 hgext/share.py not using absolute_import
83 hgext/share.py not using absolute_import
84 hgext/shelve.py not using absolute_import
84 hgext/shelve.py not using absolute_import
85 hgext/strip.py not using absolute_import
85 hgext/strip.py not using absolute_import
86 hgext/transplant.py not using absolute_import
86 hgext/transplant.py not using absolute_import
87 hgext/win32mbcs.py not using absolute_import
87 hgext/win32mbcs.py not using absolute_import
88 hgext/win32text.py not using absolute_import
88 hgext/win32text.py not using absolute_import
89 hgext/zeroconf/Zeroconf.py not using absolute_import
90 hgext/zeroconf/__init__.py not using absolute_import
91 i18n/check-translation.py not using absolute_import
89 i18n/check-translation.py not using absolute_import
92 i18n/polib.py not using absolute_import
90 i18n/polib.py not using absolute_import
93 mercurial/cmdutil.py not using absolute_import
91 mercurial/cmdutil.py not using absolute_import
94 mercurial/commands.py not using absolute_import
92 mercurial/commands.py not using absolute_import
95 setup.py not using absolute_import
93 setup.py not using absolute_import
96 tests/filterpyflakes.py requires print_function
94 tests/filterpyflakes.py requires print_function
97 tests/generate-working-copy-states.py requires print_function
95 tests/generate-working-copy-states.py requires print_function
98 tests/get-with-headers.py requires print_function
96 tests/get-with-headers.py requires print_function
99 tests/heredoctest.py requires print_function
97 tests/heredoctest.py requires print_function
100 tests/hypothesishelpers.py not using absolute_import
98 tests/hypothesishelpers.py not using absolute_import
101 tests/hypothesishelpers.py requires print_function
99 tests/hypothesishelpers.py requires print_function
102 tests/killdaemons.py not using absolute_import
100 tests/killdaemons.py not using absolute_import
103 tests/md5sum.py not using absolute_import
101 tests/md5sum.py not using absolute_import
104 tests/mockblackbox.py not using absolute_import
102 tests/mockblackbox.py not using absolute_import
105 tests/printenv.py not using absolute_import
103 tests/printenv.py not using absolute_import
106 tests/readlink.py not using absolute_import
104 tests/readlink.py not using absolute_import
107 tests/readlink.py requires print_function
105 tests/readlink.py requires print_function
108 tests/revlog-formatv0.py not using absolute_import
106 tests/revlog-formatv0.py not using absolute_import
109 tests/run-tests.py not using absolute_import
107 tests/run-tests.py not using absolute_import
110 tests/seq.py not using absolute_import
108 tests/seq.py not using absolute_import
111 tests/seq.py requires print_function
109 tests/seq.py requires print_function
112 tests/silenttestrunner.py not using absolute_import
110 tests/silenttestrunner.py not using absolute_import
113 tests/silenttestrunner.py requires print_function
111 tests/silenttestrunner.py requires print_function
114 tests/sitecustomize.py not using absolute_import
112 tests/sitecustomize.py not using absolute_import
115 tests/svn-safe-append.py not using absolute_import
113 tests/svn-safe-append.py not using absolute_import
116 tests/svnxml.py not using absolute_import
114 tests/svnxml.py not using absolute_import
117 tests/test-ancestor.py requires print_function
115 tests/test-ancestor.py requires print_function
118 tests/test-atomictempfile.py not using absolute_import
116 tests/test-atomictempfile.py not using absolute_import
119 tests/test-batching.py not using absolute_import
117 tests/test-batching.py not using absolute_import
120 tests/test-batching.py requires print_function
118 tests/test-batching.py requires print_function
121 tests/test-bdiff.py not using absolute_import
119 tests/test-bdiff.py not using absolute_import
122 tests/test-bdiff.py requires print_function
120 tests/test-bdiff.py requires print_function
123 tests/test-context.py not using absolute_import
121 tests/test-context.py not using absolute_import
124 tests/test-context.py requires print_function
122 tests/test-context.py requires print_function
125 tests/test-demandimport.py not using absolute_import
123 tests/test-demandimport.py not using absolute_import
126 tests/test-demandimport.py requires print_function
124 tests/test-demandimport.py requires print_function
127 tests/test-dispatch.py not using absolute_import
125 tests/test-dispatch.py not using absolute_import
128 tests/test-dispatch.py requires print_function
126 tests/test-dispatch.py requires print_function
129 tests/test-doctest.py not using absolute_import
127 tests/test-doctest.py not using absolute_import
130 tests/test-duplicateoptions.py not using absolute_import
128 tests/test-duplicateoptions.py not using absolute_import
131 tests/test-duplicateoptions.py requires print_function
129 tests/test-duplicateoptions.py requires print_function
132 tests/test-filecache.py not using absolute_import
130 tests/test-filecache.py not using absolute_import
133 tests/test-filecache.py requires print_function
131 tests/test-filecache.py requires print_function
134 tests/test-filelog.py not using absolute_import
132 tests/test-filelog.py not using absolute_import
135 tests/test-filelog.py requires print_function
133 tests/test-filelog.py requires print_function
136 tests/test-hg-parseurl.py not using absolute_import
134 tests/test-hg-parseurl.py not using absolute_import
137 tests/test-hg-parseurl.py requires print_function
135 tests/test-hg-parseurl.py requires print_function
138 tests/test-hgweb-auth.py not using absolute_import
136 tests/test-hgweb-auth.py not using absolute_import
139 tests/test-hgweb-auth.py requires print_function
137 tests/test-hgweb-auth.py requires print_function
140 tests/test-hgwebdir-paths.py not using absolute_import
138 tests/test-hgwebdir-paths.py not using absolute_import
141 tests/test-hybridencode.py not using absolute_import
139 tests/test-hybridencode.py not using absolute_import
142 tests/test-hybridencode.py requires print_function
140 tests/test-hybridencode.py requires print_function
143 tests/test-lrucachedict.py not using absolute_import
141 tests/test-lrucachedict.py not using absolute_import
144 tests/test-lrucachedict.py requires print_function
142 tests/test-lrucachedict.py requires print_function
145 tests/test-manifest.py not using absolute_import
143 tests/test-manifest.py not using absolute_import
146 tests/test-minirst.py not using absolute_import
144 tests/test-minirst.py not using absolute_import
147 tests/test-minirst.py requires print_function
145 tests/test-minirst.py requires print_function
148 tests/test-parseindex2.py not using absolute_import
146 tests/test-parseindex2.py not using absolute_import
149 tests/test-parseindex2.py requires print_function
147 tests/test-parseindex2.py requires print_function
150 tests/test-pathencode.py not using absolute_import
148 tests/test-pathencode.py not using absolute_import
151 tests/test-pathencode.py requires print_function
149 tests/test-pathencode.py requires print_function
152 tests/test-propertycache.py not using absolute_import
150 tests/test-propertycache.py not using absolute_import
153 tests/test-propertycache.py requires print_function
151 tests/test-propertycache.py requires print_function
154 tests/test-revlog-ancestry.py not using absolute_import
152 tests/test-revlog-ancestry.py not using absolute_import
155 tests/test-revlog-ancestry.py requires print_function
153 tests/test-revlog-ancestry.py requires print_function
156 tests/test-run-tests.py not using absolute_import
154 tests/test-run-tests.py not using absolute_import
157 tests/test-simplemerge.py not using absolute_import
155 tests/test-simplemerge.py not using absolute_import
158 tests/test-status-inprocess.py not using absolute_import
156 tests/test-status-inprocess.py not using absolute_import
159 tests/test-status-inprocess.py requires print_function
157 tests/test-status-inprocess.py requires print_function
160 tests/test-symlink-os-yes-fs-no.py not using absolute_import
158 tests/test-symlink-os-yes-fs-no.py not using absolute_import
161 tests/test-trusted.py not using absolute_import
159 tests/test-trusted.py not using absolute_import
162 tests/test-trusted.py requires print_function
160 tests/test-trusted.py requires print_function
163 tests/test-ui-color.py not using absolute_import
161 tests/test-ui-color.py not using absolute_import
164 tests/test-ui-color.py requires print_function
162 tests/test-ui-color.py requires print_function
165 tests/test-ui-config.py not using absolute_import
163 tests/test-ui-config.py not using absolute_import
166 tests/test-ui-config.py requires print_function
164 tests/test-ui-config.py requires print_function
167 tests/test-ui-verbosity.py not using absolute_import
165 tests/test-ui-verbosity.py not using absolute_import
168 tests/test-ui-verbosity.py requires print_function
166 tests/test-ui-verbosity.py requires print_function
169 tests/test-url.py not using absolute_import
167 tests/test-url.py not using absolute_import
170 tests/test-url.py requires print_function
168 tests/test-url.py requires print_function
171 tests/test-walkrepo.py requires print_function
169 tests/test-walkrepo.py requires print_function
172 tests/test-wireproto.py requires print_function
170 tests/test-wireproto.py requires print_function
173 tests/tinyproxy.py requires print_function
171 tests/tinyproxy.py requires print_function
General Comments 0
You need to be logged in to leave comments. Login now