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.GenericFutureListener;
32  import io.netty.util.concurrent.Promise;
33  import io.netty.util.concurrent.ScheduledFuture;
34  import io.netty.util.internal.logging.InternalLogger;
35  import io.netty.util.internal.logging.InternalLoggerFactory;
36  
37  import java.net.InetSocketAddress;
38  import java.util.concurrent.TimeUnit;
39  
40  import static io.netty.util.internal.ObjectUtil.checkNotNull;
41  
42  final class DnsQueryContext {
43  
44      private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsQueryContext.class);
45  
46      private final DnsNameResolver parent;
47      private final Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise;
48      private final int id;
49      private final DnsQuestion question;
50      private final DnsRecord[] additionals;
51      private final DnsRecord optResource;
52      private final InetSocketAddress nameServerAddr;
53  
54      private final boolean recursionDesired;
55      private volatile ScheduledFuture<?> timeoutFuture;
56  
57      DnsQueryContext(DnsNameResolver parent,
58                      InetSocketAddress nameServerAddr,
59                      DnsQuestion question,
60                      DnsRecord[] additionals,
61                      Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise) {
62  
63          this.parent = checkNotNull(parent, "parent");
64          this.nameServerAddr = checkNotNull(nameServerAddr, "nameServerAddr");
65          this.question = checkNotNull(question, "question");
66          this.additionals = checkNotNull(additionals, "additionals");
67          this.promise = checkNotNull(promise, "promise");
68          recursionDesired = parent.isRecursionDesired();
69          id = parent.queryContextManager.add(this);
70  
71          if (parent.isOptResourceEnabled()) {
72              optResource = new AbstractDnsOptPseudoRrRecord(parent.maxPayloadSize(), 0, 0) {
73                  // We may want to remove this in the future and let the user just specify the opt record in the query.
74              };
75          } else {
76              optResource = null;
77          }
78      }
79  
80      InetSocketAddress nameServerAddr() {
81          return nameServerAddr;
82      }
83  
84      DnsQuestion question() {
85          return question;
86      }
87  
88      void query(ChannelPromise writePromise) {
89          final DnsQuestion question = question();
90          final InetSocketAddress nameServerAddr = nameServerAddr();
91          final DatagramDnsQuery query = new DatagramDnsQuery(null, nameServerAddr, id);
92  
93          query.setRecursionDesired(recursionDesired);
94  
95          query.addRecord(DnsSection.QUESTION, question);
96  
97          for (DnsRecord record: additionals) {
98              query.addRecord(DnsSection.ADDITIONAL, record);
99          }
100 
101         if (optResource != null) {
102             query.addRecord(DnsSection.ADDITIONAL, optResource);
103         }
104 
105         if (logger.isDebugEnabled()) {
106             logger.debug("{} WRITE: [{}: {}], {}", parent.ch, id, nameServerAddr, question);
107         }
108 
109         sendQuery(query, writePromise);
110     }
111 
112     private void sendQuery(final DnsQuery query, final ChannelPromise writePromise) {
113         if (parent.channelFuture.isDone()) {
114             writeQuery(query, writePromise);
115         } else {
116             parent.channelFuture.addListener(new GenericFutureListener<Future<? super Channel>>() {
117                 @Override
118                 public void operationComplete(Future<? super Channel> future) throws Exception {
119                     if (future.isSuccess()) {
120                         writeQuery(query, writePromise);
121                     } else {
122                         Throwable cause = future.cause();
123                         promise.tryFailure(cause);
124                         writePromise.setFailure(cause);
125                     }
126                 }
127             });
128         }
129     }
130 
131     private void writeQuery(final DnsQuery query, final ChannelPromise writePromise) {
132         final ChannelFuture writeFuture = parent.ch.writeAndFlush(query, writePromise);
133         if (writeFuture.isDone()) {
134             onQueryWriteCompletion(writeFuture);
135         } else {
136             writeFuture.addListener(new ChannelFutureListener() {
137                 @Override
138                 public void operationComplete(ChannelFuture future) throws Exception {
139                     onQueryWriteCompletion(writeFuture);
140                 }
141             });
142         }
143     }
144 
145     private void onQueryWriteCompletion(ChannelFuture writeFuture) {
146         if (!writeFuture.isSuccess()) {
147             setFailure("failed to send a query", writeFuture.cause());
148             return;
149         }
150 
151         // Schedule a query timeout task if necessary.
152         final long queryTimeoutMillis = parent.queryTimeoutMillis();
153         if (queryTimeoutMillis > 0) {
154             timeoutFuture = parent.ch.eventLoop().schedule(new Runnable() {
155                 @Override
156                 public void run() {
157                     if (promise.isDone()) {
158                         // Received a response before the query times out.
159                         return;
160                     }
161 
162                     setFailure("query timed out after " + queryTimeoutMillis + " milliseconds", null);
163                 }
164             }, queryTimeoutMillis, TimeUnit.MILLISECONDS);
165         }
166     }
167 
168     void finish(AddressedEnvelope<? extends DnsResponse, InetSocketAddress> envelope) {
169         final DnsResponse res = envelope.content();
170         if (res.count(DnsSection.QUESTION) != 1) {
171             logger.warn("Received a DNS response with invalid number of questions: {}", envelope);
172             return;
173         }
174 
175         if (!question().equals(res.recordAt(DnsSection.QUESTION))) {
176             logger.warn("Received a mismatching DNS response: {}", envelope);
177             return;
178         }
179 
180         setSuccess(envelope);
181     }
182 
183     private void setSuccess(AddressedEnvelope<? extends DnsResponse, InetSocketAddress> envelope) {
184         parent.queryContextManager.remove(nameServerAddr(), id);
185 
186         // Cancel the timeout task.
187         final ScheduledFuture<?> timeoutFuture = this.timeoutFuture;
188         if (timeoutFuture != null) {
189             timeoutFuture.cancel(false);
190         }
191 
192         Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise = this.promise;
193         if (promise.setUncancellable()) {
194             @SuppressWarnings("unchecked")
195             AddressedEnvelope<DnsResponse, InetSocketAddress> castResponse =
196                     (AddressedEnvelope<DnsResponse, InetSocketAddress>) envelope.retain();
197             if (!promise.trySuccess(castResponse)) {
198                 // We failed to notify the promise as it was failed before, thus we need to release the envelope
199                 envelope.release();
200             }
201         }
202     }
203 
204     private void setFailure(String message, Throwable cause) {
205         final InetSocketAddress nameServerAddr = nameServerAddr();
206         parent.queryContextManager.remove(nameServerAddr, id);
207 
208         final StringBuilder buf = new StringBuilder(message.length() + 64);
209         buf.append('[')
210            .append(nameServerAddr)
211            .append("] ")
212            .append(message)
213            .append(" (no stack trace available)");
214 
215         final DnsNameResolverException e;
216         if (cause == null) {
217             // This was caused by an timeout so use DnsNameResolverTimeoutException to allow the user to
218             // handle it special (like retry the query).
219             e = new DnsNameResolverTimeoutException(nameServerAddr, question(), buf.toString());
220         } else {
221             e = new DnsNameResolverException(nameServerAddr, question(), buf.toString(), cause);
222         }
223         promise.tryFailure(e);
224     }
225 }