View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.resolver.dns;
17  
18  import io.netty.channel.AddressedEnvelope;
19  import io.netty.channel.Channel;
20  import io.netty.channel.ChannelFuture;
21  import io.netty.channel.ChannelFutureListener;
22  import io.netty.channel.ChannelPromise;
23  import io.netty.handler.codec.dns.DatagramDnsQuery;
24  import io.netty.handler.codec.dns.AbstractDnsOptPseudoRrRecord;
25  import io.netty.handler.codec.dns.DnsQuery;
26  import io.netty.handler.codec.dns.DnsQuestion;
27  import io.netty.handler.codec.dns.DnsRecord;
28  import io.netty.handler.codec.dns.DnsResponse;
29  import io.netty.handler.codec.dns.DnsSection;
30  import io.netty.util.concurrent.Future;
31  import io.netty.util.concurrent.FutureListener;
32  import io.netty.util.concurrent.GenericFutureListener;
33  import io.netty.util.concurrent.Promise;
34  import io.netty.util.concurrent.ScheduledFuture;
35  import io.netty.util.internal.logging.InternalLogger;
36  import io.netty.util.internal.logging.InternalLoggerFactory;
37  
38  import java.net.InetSocketAddress;
39  import java.util.concurrent.TimeUnit;
40  
41  import static io.netty.util.internal.ObjectUtil.checkNotNull;
42  
43  final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>> {
44  
45      private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsQueryContext.class);
46  
47      private final DnsNameResolver parent;
48      private final Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise;
49      private final int id;
50      private final DnsQuestion question;
51      private final DnsRecord[] additionals;
52      private final DnsRecord optResource;
53      private final InetSocketAddress nameServerAddr;
54  
55      private final boolean recursionDesired;
56      private volatile ScheduledFuture<?> timeoutFuture;
57  
58      DnsQueryContext(DnsNameResolver parent,
59                      InetSocketAddress nameServerAddr,
60                      DnsQuestion question,
61                      DnsRecord[] additionals,
62                      Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise) {
63  
64          this.parent = checkNotNull(parent, "parent");
65          this.nameServerAddr = checkNotNull(nameServerAddr, "nameServerAddr");
66          this.question = checkNotNull(question, "question");
67          this.additionals = checkNotNull(additionals, "additionals");
68          this.promise = checkNotNull(promise, "promise");
69          recursionDesired = parent.isRecursionDesired();
70          id = parent.queryContextManager.add(this);
71  
72          // Ensure we remove the id from the QueryContextManager once the query completes.
73          promise.addListener(this);
74  
75          if (parent.isOptResourceEnabled()) {
76              optResource = new AbstractDnsOptPseudoRrRecord(parent.maxPayloadSize(), 0, 0) {
77                  // We may want to remove this in the future and let the user just specify the opt record in the query.
78              };
79          } else {
80              optResource = null;
81          }
82      }
83  
84      InetSocketAddress nameServerAddr() {
85          return nameServerAddr;
86      }
87  
88      DnsQuestion question() {
89          return question;
90      }
91  
92      void query(ChannelPromise writePromise) {
93          final DnsQuestion question = question();
94          final InetSocketAddress nameServerAddr = nameServerAddr();
95          final DatagramDnsQuery query = new DatagramDnsQuery(null, nameServerAddr, id);
96  
97          query.setRecursionDesired(recursionDesired);
98  
99          query.addRecord(DnsSection.QUESTION, question);
100 
101         for (DnsRecord record: additionals) {
102             query.addRecord(DnsSection.ADDITIONAL, record);
103         }
104 
105         if (optResource != null) {
106             query.addRecord(DnsSection.ADDITIONAL, optResource);
107         }
108 
109         if (logger.isDebugEnabled()) {
110             logger.debug("{} WRITE: [{}: {}], {}", parent.ch, id, nameServerAddr, question);
111         }
112 
113         sendQuery(query, writePromise);
114     }
115 
116     private void sendQuery(final DnsQuery query, final ChannelPromise writePromise) {
117         if (parent.channelFuture.isDone()) {
118             writeQuery(query, writePromise);
119         } else {
120             parent.channelFuture.addListener(new GenericFutureListener<Future<? super Channel>>() {
121                 @Override
122                 public void operationComplete(Future<? super Channel> future) {
123                     if (future.isSuccess()) {
124                         writeQuery(query, writePromise);
125                     } else {
126                         Throwable cause = future.cause();
127                         promise.tryFailure(cause);
128                         writePromise.setFailure(cause);
129                     }
130                 }
131             });
132         }
133     }
134 
135     private void writeQuery(final DnsQuery query, final ChannelPromise writePromise) {
136         final ChannelFuture writeFuture = parent.ch.writeAndFlush(query, writePromise);
137         if (writeFuture.isDone()) {
138             onQueryWriteCompletion(writeFuture);
139         } else {
140             writeFuture.addListener(new ChannelFutureListener() {
141                 @Override
142                 public void operationComplete(ChannelFuture future) {
143                     onQueryWriteCompletion(writeFuture);
144                 }
145             });
146         }
147     }
148 
149     private void onQueryWriteCompletion(ChannelFuture writeFuture) {
150         if (!writeFuture.isSuccess()) {
151             setFailure("failed to send a query", writeFuture.cause());
152             return;
153         }
154 
155         // Schedule a query timeout task if necessary.
156         final long queryTimeoutMillis = parent.queryTimeoutMillis();
157         if (queryTimeoutMillis > 0) {
158             timeoutFuture = parent.ch.eventLoop().schedule(new Runnable() {
159                 @Override
160                 public void run() {
161                     if (promise.isDone()) {
162                         // Received a response before the query times out.
163                         return;
164                     }
165 
166                     setFailure("query timed out after " + queryTimeoutMillis + " milliseconds", null);
167                 }
168             }, queryTimeoutMillis, TimeUnit.MILLISECONDS);
169         }
170     }
171 
172     void finish(AddressedEnvelope<? extends DnsResponse, InetSocketAddress> envelope) {
173         final DnsResponse res = envelope.content();
174         if (res.count(DnsSection.QUESTION) != 1) {
175             logger.warn("Received a DNS response with invalid number of questions: {}", envelope);
176             return;
177         }
178 
179         if (!question().equals(res.recordAt(DnsSection.QUESTION))) {
180             logger.warn("Received a mismatching DNS response: {}", envelope);
181             return;
182         }
183 
184         setSuccess(envelope);
185     }
186 
187     private void setSuccess(AddressedEnvelope<? extends DnsResponse, InetSocketAddress> envelope) {
188         Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise = this.promise;
189         @SuppressWarnings("unchecked")
190         AddressedEnvelope<DnsResponse, InetSocketAddress> castResponse =
191                 (AddressedEnvelope<DnsResponse, InetSocketAddress>) envelope.retain();
192         if (!promise.trySuccess(castResponse)) {
193             // We failed to notify the promise as it was failed before, thus we need to release the envelope
194             envelope.release();
195         }
196     }
197 
198     private void setFailure(String message, Throwable cause) {
199         final InetSocketAddress nameServerAddr = nameServerAddr();
200 
201         final StringBuilder buf = new StringBuilder(message.length() + 64);
202         buf.append('[')
203            .append(nameServerAddr)
204            .append("] ")
205            .append(message)
206            .append(" (no stack trace available)");
207 
208         final DnsNameResolverException e;
209         if (cause == null) {
210             // This was caused by an timeout so use DnsNameResolverTimeoutException to allow the user to
211             // handle it special (like retry the query).
212             e = new DnsNameResolverTimeoutException(nameServerAddr, question(), buf.toString());
213         } else {
214             e = new DnsNameResolverException(nameServerAddr, question(), buf.toString(), cause);
215         }
216         promise.tryFailure(e);
217     }
218 
219     @Override
220     public void operationComplete(Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> future) {
221         // Cancel the timeout task.
222         final ScheduledFuture<?> timeoutFuture = this.timeoutFuture;
223         if (timeoutFuture != null) {
224             this.timeoutFuture = null;
225             timeoutFuture.cancel(false);
226         }
227 
228         // Remove the id from the manager as soon as the query completes. This may be because of success, failure or
229         // cancellation
230         parent.queryContextManager.remove(nameServerAddr, id);
231     }
232 }