Imported Upstream version 12.1.0
[contrib/python-twisted.git] / twisted / names / root.py
1 # -*- test-case-name: twisted.names.test.test_rootresolve -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5 """
6 Resolver implementation for querying successive authoritative servers to
7 lookup a record, starting from the root nameservers.
8
9 @author: Jp Calderone
10
11 todo::
12     robustify it
13     documentation
14 """
15
16 import warnings
17
18 from twisted.python.failure import Failure
19 from twisted.internet import defer
20 from twisted.names import dns, common, error
21
22
23 def retry(t, p, *args):
24     """
25     Issue a query one or more times.
26
27     This function is deprecated.  Use one of the resolver classes for retry
28     logic, or implement it yourself.
29     """
30     warnings.warn(
31         "twisted.names.root.retry is deprecated since Twisted 10.0.  Use a "
32         "Resolver object for retry logic.", category=DeprecationWarning,
33         stacklevel=2)
34
35     assert t, "Timeout is required"
36     t = list(t)
37     def errback(failure):
38         failure.trap(defer.TimeoutError)
39         if not t:
40             return failure
41         return p.query(timeout=t.pop(0), *args
42             ).addErrback(errback
43             )
44     return p.query(timeout=t.pop(0), *args
45         ).addErrback(errback
46         )
47
48
49
50 class _DummyController:
51     """
52     A do-nothing DNS controller.  This is useful when all messages received
53     will be responses to previously issued queries.  Anything else received
54     will be ignored.
55     """
56     def messageReceived(self, *args):
57         pass
58
59
60
61 class Resolver(common.ResolverBase):
62     """
63     L{Resolver} implements recursive lookup starting from a specified list of
64     root servers.
65
66     @ivar hints: A C{list} of C{str} giving the dotted quad representation
67         of IP addresses of root servers at which to begin resolving names.
68
69     @ivar _maximumQueries: A C{int} giving the maximum number of queries
70         which will be attempted to resolve a single name.
71
72     @ivar _reactor: A L{IReactorTime} and L{IReactorUDP} provider to use to
73         bind UDP ports and manage timeouts.
74     """
75     def __init__(self, hints, maximumQueries=10, reactor=None):
76         common.ResolverBase.__init__(self)
77         self.hints = hints
78         self._maximumQueries = maximumQueries
79         self._reactor = reactor
80
81
82     def _roots(self):
83         """
84         Return a list of two-tuples representing the addresses of the root
85         servers, as defined by C{self.hints}.
86         """
87         return [(ip, dns.PORT) for ip in self.hints]
88
89
90     def _query(self, query, servers, timeout, filter):
91         """
92         Issue one query and return a L{Deferred} which fires with its response.
93
94         @param query: The query to issue.
95         @type query: L{dns.Query}
96
97         @param servers: The servers which might have an answer for this
98             query.
99         @type servers: L{list} of L{tuple} of L{str} and L{int}
100
101         @param timeout: A timeout on how long to wait for the response.
102         @type timeout: L{tuple} of L{int}
103
104         @param filter: A flag indicating whether to filter the results.  If
105             C{True}, the returned L{Deferred} will fire with a three-tuple of
106             lists of L{RRHeaders} (like the return value of the I{lookup*}
107             methods of L{IResolver}.  IF C{False}, the result will be a
108             L{Message} instance.
109         @type filter: L{bool}
110
111         @return: A L{Deferred} which fires with the response or a timeout
112             error.
113         @rtype: L{Deferred}
114         """
115         from twisted.names import client
116         r = client.Resolver(servers=servers, reactor=self._reactor)
117         d = r.queryUDP([query], timeout)
118         if filter:
119             d.addCallback(r.filterAnswers)
120         return d
121
122
123     def _lookup(self, name, cls, type, timeout):
124         """
125         Implement name lookup by recursively discovering the authoritative
126         server for the name and then asking it, starting at one of the servers
127         in C{self.hints}.
128         """
129         if timeout is None:
130             # A series of timeouts for semi-exponential backoff, summing to an
131             # arbitrary total of 60 seconds.
132             timeout = (1, 3, 11, 45)
133         return self._discoverAuthority(
134             dns.Query(name, type, cls), self._roots(), timeout,
135             self._maximumQueries)
136
137
138     def _discoverAuthority(self, query, servers, timeout, queriesLeft):
139         """
140         Issue a query to a server and follow a delegation if necessary.
141
142         @param query: The query to issue.
143         @type query: L{dns.Query}
144
145         @param servers: The servers which might have an answer for this
146             query.
147         @type servers: L{list} of L{tuple} of L{str} and L{int}
148
149         @param timeout: A C{tuple} of C{int} giving the timeout to use for this
150             query.
151
152         @param queriesLeft: A C{int} giving the number of queries which may
153             yet be attempted to answer this query before the attempt will be
154             abandoned.
155
156         @return: A L{Deferred} which fires with a three-tuple of lists of
157             L{RRHeaders} giving the response, or with a L{Failure} if there is
158             a timeout or response error.
159         """
160         # Stop now if we've hit the query limit.
161         if queriesLeft <= 0:
162             return Failure(
163                 error.ResolverError("Query limit reached without result"))
164
165         d = self._query(query, servers, timeout, False)
166         d.addCallback(
167             self._discoveredAuthority, query, timeout, queriesLeft - 1)
168         return d
169
170
171     def _discoveredAuthority(self, response, query, timeout, queriesLeft):
172         """
173         Interpret the response to a query, checking for error codes and
174         following delegations if necessary.
175
176         @param response: The L{Message} received in response to issuing C{query}.
177         @type response: L{Message}
178
179         @param query: The L{dns.Query} which was issued.
180         @type query: L{dns.Query}.
181
182         @param timeout: The timeout to use if another query is indicated by
183             this response.
184         @type timeout: L{tuple} of L{int}
185
186         @param queriesLeft: A C{int} giving the number of queries which may
187             yet be attempted to answer this query before the attempt will be
188             abandoned.
189
190         @return: A L{Failure} indicating a response error, a three-tuple of
191             lists of L{RRHeaders} giving the response to C{query} or a
192             L{Deferred} which will fire with one of those.
193         """
194         if response.rCode != dns.OK:
195             return Failure(self.exceptionForCode(response.rCode)(response))
196
197         # Turn the answers into a structure that's a little easier to work with.
198         records = {}
199         for answer in response.answers:
200             records.setdefault(answer.name, []).append(answer)
201
202         def findAnswerOrCName(name, type, cls):
203             cname = None
204             for record in records.get(name, []):
205                 if record.cls ==  cls:
206                     if record.type == type:
207                         return record
208                     elif record.type == dns.CNAME:
209                         cname = record
210             # If there were any CNAME records, return the last one.  There's
211             # only supposed to be zero or one, though.
212             return cname
213
214         seen = set()
215         name = query.name
216         record = None
217         while True:
218             seen.add(name)
219             previous = record
220             record = findAnswerOrCName(name, query.type, query.cls)
221             if record is None:
222                 if name == query.name:
223                     # If there's no answer for the original name, then this may
224                     # be a delegation.  Code below handles it.
225                     break
226                 else:
227                     # Try to resolve the CNAME with another query.
228                     d = self._discoverAuthority(
229                         dns.Query(str(name), query.type, query.cls),
230                         self._roots(), timeout, queriesLeft)
231                     # We also want to include the CNAME in the ultimate result,
232                     # otherwise this will be pretty confusing.
233                     def cbResolved((answers, authority, additional)):
234                         answers.insert(0, previous)
235                         return (answers, authority, additional)
236                     d.addCallback(cbResolved)
237                     return d
238             elif record.type == query.type:
239                 return (
240                     response.answers,
241                     response.authority,
242                     response.additional)
243             else:
244                 # It's a CNAME record.  Try to resolve it from the records
245                 # in this response with another iteration around the loop.
246                 if record.payload.name in seen:
247                     raise error.ResolverError("Cycle in CNAME processing")
248                 name = record.payload.name
249
250
251         # Build a map to use to convert NS names into IP addresses.
252         addresses = {}
253         for rr in response.additional:
254             if rr.type == dns.A:
255                 addresses[str(rr.name)] = rr.payload.dottedQuad()
256
257         hints = []
258         traps = []
259         for rr in response.authority:
260             if rr.type == dns.NS:
261                 ns = str(rr.payload.name)
262                 if ns in addresses:
263                     hints.append((addresses[ns], dns.PORT))
264                 else:
265                     traps.append(ns)
266         if hints:
267             return self._discoverAuthority(
268                 query, hints, timeout, queriesLeft)
269         elif traps:
270             d = self.lookupAddress(traps[0], timeout)
271             d.addCallback(
272                 lambda (answers, authority, additional):
273                     answers[0].payload.dottedQuad())
274             d.addCallback(
275                 lambda hint: self._discoverAuthority(
276                     query, [(hint, dns.PORT)], timeout, queriesLeft - 1))
277             return d
278         else:
279             return Failure(error.ResolverError(
280                     "Stuck at response without answers or delegation"))
281
282
283     def discoveredAuthority(self, auth, name, cls, type, timeout):
284         warnings.warn(
285             'twisted.names.root.Resolver.discoveredAuthority is deprecated since '
286             'Twisted 10.0.  Use twisted.names.client.Resolver directly, instead.',
287             category=DeprecationWarning, stacklevel=2)
288         from twisted.names import client
289         q = dns.Query(name, type, cls)
290         r = client.Resolver(servers=[(auth, dns.PORT)])
291         d = r.queryUDP([q], timeout)
292         d.addCallback(r.filterAnswers)
293         return d
294
295
296
297 def lookupNameservers(host, atServer, p=None):
298     warnings.warn(
299         'twisted.names.root.lookupNameservers is deprecated since Twisted '
300         '10.0.  Use twisted.names.root.Resolver.lookupNameservers instead.',
301         category=DeprecationWarning, stacklevel=2)
302     # print 'Nameserver lookup for', host, 'at', atServer, 'with', p
303     if p is None:
304         p = dns.DNSDatagramProtocol(_DummyController())
305         p.noisy = False
306     return retry(
307         (1, 3, 11, 45),                     # Timeouts
308         p,                                  # Protocol instance
309         (atServer, dns.PORT),               # Server to query
310         [dns.Query(host, dns.NS, dns.IN)]   # Question to ask
311     )
312
313 def lookupAddress(host, atServer, p=None):
314     warnings.warn(
315         'twisted.names.root.lookupAddress is deprecated since Twisted '
316         '10.0.  Use twisted.names.root.Resolver.lookupAddress instead.',
317         category=DeprecationWarning, stacklevel=2)
318     # print 'Address lookup for', host, 'at', atServer, 'with', p
319     if p is None:
320         p = dns.DNSDatagramProtocol(_DummyController())
321         p.noisy = False
322     return retry(
323         (1, 3, 11, 45),                     # Timeouts
324         p,                                  # Protocol instance
325         (atServer, dns.PORT),               # Server to query
326         [dns.Query(host, dns.A, dns.IN)]    # Question to ask
327     )
328
329 def extractAuthority(msg, cache):
330     warnings.warn(
331         'twisted.names.root.extractAuthority is deprecated since Twisted '
332         '10.0.  Please inspect the Message object directly.',
333         category=DeprecationWarning, stacklevel=2)
334     records = msg.answers + msg.authority + msg.additional
335     nameservers = [r for r in records if r.type == dns.NS]
336
337     # print 'Records for', soFar, ':', records
338     # print 'NS for', soFar, ':', nameservers
339
340     if not nameservers:
341         return None, nameservers
342     if not records:
343         raise IOError("No records")
344     for r in records:
345         if r.type == dns.A:
346             cache[str(r.name)] = r.payload.dottedQuad()
347     for r in records:
348         if r.type == dns.NS:
349             if str(r.payload.name) in cache:
350                 return cache[str(r.payload.name)], nameservers
351     for addr in records:
352         if addr.type == dns.A and addr.name == r.name:
353             return addr.payload.dottedQuad(), nameservers
354     return None, nameservers
355
356 def discoverAuthority(host, roots, cache=None, p=None):
357     warnings.warn(
358         'twisted.names.root.discoverAuthority is deprecated since Twisted '
359         '10.0.  Use twisted.names.root.Resolver.lookupNameservers instead.',
360         category=DeprecationWarning, stacklevel=4)
361
362     if cache is None:
363         cache = {}
364
365     rootAuths = list(roots)
366
367     parts = host.rstrip('.').split('.')
368     parts.reverse()
369
370     authority = rootAuths.pop()
371
372     soFar = ''
373     for part in parts:
374         soFar = part + '.' + soFar
375         # print '///////',  soFar, authority, p
376         msg = defer.waitForDeferred(lookupNameservers(soFar, authority, p))
377         yield msg
378         msg = msg.getResult()
379
380         newAuth, nameservers = extractAuthority(msg, cache)
381
382         if newAuth is not None:
383             # print "newAuth is not None"
384             authority = newAuth
385         else:
386             if nameservers:
387                 r = str(nameservers[0].payload.name)
388                 # print 'Recursively discovering authority for', r
389                 authority = defer.waitForDeferred(discoverAuthority(r, roots, cache, p))
390                 yield authority
391                 authority = authority.getResult()
392                 # print 'Discovered to be', authority, 'for', r
393 ##            else:
394 ##                # print 'Doing address lookup for', soFar, 'at', authority
395 ##                msg = defer.waitForDeferred(lookupAddress(soFar, authority, p))
396 ##                yield msg
397 ##                msg = msg.getResult()
398 ##                records = msg.answers + msg.authority + msg.additional
399 ##                addresses = [r for r in records if r.type == dns.A]
400 ##                if addresses:
401 ##                    authority = addresses[0].payload.dottedQuad()
402 ##                else:
403 ##                    raise IOError("Resolution error")
404     # print "Yielding authority", authority
405     yield authority
406
407 discoverAuthority = defer.deferredGenerator(discoverAuthority)
408
409 def makePlaceholder(deferred, name):
410     def placeholder(*args, **kw):
411         deferred.addCallback(lambda r: getattr(r, name)(*args, **kw))
412         return deferred
413     return placeholder
414
415 class DeferredResolver:
416     def __init__(self, resolverDeferred):
417         self.waiting = []
418         resolverDeferred.addCallback(self.gotRealResolver)
419
420     def gotRealResolver(self, resolver):
421         w = self.waiting
422         self.__dict__ = resolver.__dict__
423         self.__class__ = resolver.__class__
424         for d in w:
425             d.callback(resolver)
426
427     def __getattr__(self, name):
428         if name.startswith('lookup') or name in ('getHostByName', 'query'):
429             self.waiting.append(defer.Deferred())
430             return makePlaceholder(self.waiting[-1], name)
431         raise AttributeError(name)
432
433 def bootstrap(resolver):
434     """Lookup the root nameserver addresses using the given resolver
435
436     Return a Resolver which will eventually become a C{root.Resolver}
437     instance that has references to all the root servers that we were able
438     to look up.
439     """
440     domains = [chr(ord('a') + i) for i in range(13)]
441     # f = lambda r: (log.msg('Root server address: ' + str(r)), r)[1]
442     f = lambda r: r
443     L = [resolver.getHostByName('%s.root-servers.net' % d).addCallback(f) for d in domains]
444     d = defer.DeferredList(L)
445     d.addCallback(lambda r: Resolver([e[1] for e in r if e[0]]))
446     return DeferredResolver(d)