View Javadoc
1   /*
2    * Copyright 2016 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    *   https://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.EventLoop;
19  import io.netty.handler.codec.dns.DnsRecord;
20  import io.netty.util.internal.StringUtil;
21  
22  import java.io.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.IOException;
25  import java.io.ObjectInputStream;
26  import java.io.ObjectOutputStream;
27  import java.net.InetAddress;
28  import java.net.UnknownHostException;
29  import java.util.AbstractList;
30  import java.util.Collections;
31  import java.util.List;
32  import java.util.concurrent.ConcurrentMap;
33  
34  import static io.netty.util.internal.ObjectUtil.checkNotNull;
35  import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
36  
37  /**
38   * Default implementation of {@link DnsCache}, backed by a {@link ConcurrentMap}.
39   * If any additional {@link DnsRecord} is used, no caching takes place.
40   */
41  public class DefaultDnsCache implements DnsCache {
42  
43      private final Cache<DefaultDnsCacheEntry> resolveCache = new Cache<DefaultDnsCacheEntry>() {
44  
45          @Override
46          protected boolean shouldReplaceAll(DefaultDnsCacheEntry entry) {
47              return entry.cause() != null;
48          }
49  
50          @Override
51          protected boolean equals(DefaultDnsCacheEntry entry, DefaultDnsCacheEntry otherEntry) {
52              if (entry.address() != null) {
53                  return entry.address().equals(otherEntry.address());
54              }
55              if (otherEntry.address() != null) {
56                  return false;
57              }
58              return entry.cause().equals(otherEntry.cause());
59          }
60      };
61  
62      private final int minTtl;
63      private final int maxTtl;
64      private final int negativeTtl;
65  
66      /**
67       * Create a cache that respects the TTL returned by the DNS server
68       * and doesn't cache negative responses.
69       */
70      public DefaultDnsCache() {
71          this(0, Cache.MAX_SUPPORTED_TTL_SECS, 0);
72      }
73  
74      /**
75       * Create a cache.
76       * @param minTtl the minimum TTL
77       * @param maxTtl the maximum TTL
78       * @param negativeTtl the TTL for failed queries
79       */
80      public DefaultDnsCache(int minTtl, int maxTtl, int negativeTtl) {
81          this.minTtl = Math.min(Cache.MAX_SUPPORTED_TTL_SECS, checkPositiveOrZero(minTtl, "minTtl"));
82          this.maxTtl = Math.min(Cache.MAX_SUPPORTED_TTL_SECS, checkPositiveOrZero(maxTtl, "maxTtl"));
83          if (minTtl > maxTtl) {
84              throw new IllegalArgumentException(
85                      "minTtl: " + minTtl + ", maxTtl: " + maxTtl + " (expected: 0 <= minTtl <= maxTtl)");
86          }
87          this.negativeTtl = Math.min(Cache.MAX_SUPPORTED_TTL_SECS, checkPositiveOrZero(negativeTtl, "negativeTtl"));
88      }
89  
90      /**
91       * Returns the minimum TTL of the cached DNS resource records (in seconds).
92       *
93       * @see #maxTtl()
94       */
95      public int minTtl() {
96          return minTtl;
97      }
98  
99      /**
100      * Returns the maximum TTL of the cached DNS resource records (in seconds).
101      *
102      * @see #minTtl()
103      */
104     public int maxTtl() {
105         return maxTtl;
106     }
107 
108     /**
109      * Returns the TTL of the cache for the failed DNS queries (in seconds). The default value is {@code 0}, which
110      * disables the cache for negative results.
111      */
112     public int negativeTtl() {
113         return negativeTtl;
114     }
115 
116     @Override
117     public void clear() {
118         resolveCache.clear();
119     }
120 
121     @Override
122     public boolean clear(String hostname) {
123         checkNotNull(hostname, "hostname");
124         return resolveCache.clear(appendDot(hostname));
125     }
126 
127     private static boolean emptyAdditionals(DnsRecord[] additionals) {
128         return additionals == null || additionals.length == 0;
129     }
130 
131     @Override
132     public List<? extends DnsCacheEntry> get(String hostname, DnsRecord[] additionals) {
133         checkNotNull(hostname, "hostname");
134         if (!emptyAdditionals(additionals)) {
135             return Collections.<DnsCacheEntry>emptyList();
136         }
137 
138         final List<? extends DnsCacheEntry> entries = resolveCache.get(appendDot(hostname));
139         if (entries == null || entries.isEmpty()) {
140             return entries;
141         }
142         return new DnsCacheEntryList(entries);
143     }
144 
145     @Override
146     public DnsCacheEntry cache(String hostname, DnsRecord[] additionals,
147                                InetAddress address, long originalTtl, EventLoop loop) {
148         checkNotNull(hostname, "hostname");
149         checkNotNull(address, "address");
150         checkNotNull(loop, "loop");
151         DefaultDnsCacheEntry e = new DefaultDnsCacheEntry(hostname, address);
152         if (maxTtl == 0 || !emptyAdditionals(additionals)) {
153             return e;
154         }
155         resolveCache.cache(appendDot(hostname), e, Math.max(minTtl, (int) Math.min(maxTtl, originalTtl)), loop);
156         return e;
157     }
158 
159     @Override
160     public DnsCacheEntry cache(String hostname, DnsRecord[] additionals, Throwable cause, EventLoop loop) {
161         checkNotNull(hostname, "hostname");
162         checkNotNull(cause, "cause");
163         checkNotNull(loop, "loop");
164 
165         DefaultDnsCacheEntry e = new DefaultDnsCacheEntry(hostname, cause);
166         if (negativeTtl == 0 || !emptyAdditionals(additionals)) {
167             return e;
168         }
169 
170         resolveCache.cache(appendDot(hostname), e, negativeTtl, loop);
171         return e;
172     }
173 
174     @Override
175     public String toString() {
176         return new StringBuilder()
177                 .append("DefaultDnsCache(minTtl=")
178                 .append(minTtl).append(", maxTtl=")
179                 .append(maxTtl).append(", negativeTtl=")
180                 .append(negativeTtl).append(", cached resolved hostname=")
181                 .append(resolveCache.size()).append(')')
182                 .toString();
183     }
184 
185     private static final class DefaultDnsCacheEntry implements DnsCacheEntry {
186         private final String hostname;
187         private final InetAddress address;
188         private final Throwable cause;
189         private final int hash;
190 
191         DefaultDnsCacheEntry(String hostname, InetAddress address) {
192             this.hostname = hostname;
193             this.address = address;
194             cause = null;
195             hash = System.identityHashCode(this);
196         }
197 
198         DefaultDnsCacheEntry(String hostname, Throwable cause) {
199             this.hostname = hostname;
200             this.cause = cause;
201             address = null;
202             hash = System.identityHashCode(this);
203         }
204 
205         private DefaultDnsCacheEntry(DefaultDnsCacheEntry entry) {
206             this.hostname = entry.hostname;
207             if (entry.cause == null) {
208                 this.address = entry.address;
209                 this.cause = null;
210             } else {
211                 this.address = null;
212                 this.cause = copyThrowable(entry.cause);
213             }
214             this.hash = entry.hash;
215         }
216 
217         @Override
218         public InetAddress address() {
219             return address;
220         }
221 
222         @Override
223         public Throwable cause() {
224             return cause;
225         }
226 
227         String hostname() {
228             return hostname;
229         }
230 
231         @Override
232         public String toString() {
233             if (cause != null) {
234                 return hostname + '/' + cause;
235             } else {
236                 return address.toString();
237             }
238         }
239 
240         @Override
241         public int hashCode() {
242             return hash;
243         }
244 
245         @Override
246         public boolean equals(Object obj) {
247             return (obj instanceof DefaultDnsCacheEntry) && ((DefaultDnsCacheEntry) obj).hash == hash;
248         }
249 
250         DnsCacheEntry copyIfNeeded() {
251             if (cause == null) {
252                 return this;
253             }
254             return new DefaultDnsCacheEntry(this);
255         }
256     }
257 
258     private static String appendDot(String hostname) {
259         return StringUtil.endsWith(hostname, '.') ? hostname : hostname + '.';
260     }
261 
262     private static Throwable copyThrowable(Throwable error) {
263         if (error.getClass() == UnknownHostException.class) {
264             // Fast-path as this is the only type of Throwable that our implementation ever add to the cache.
265             UnknownHostException copy = new UnknownHostException(error.getMessage()) {
266                 @Override
267                 public Throwable fillInStackTrace() {
268                     // noop.
269                     return this;
270                 }
271             };
272             copy.initCause(error.getCause());
273             copy.setStackTrace(error.getStackTrace());
274             return copy;
275         }
276         ObjectOutputStream oos = null;
277         ObjectInputStream ois = null;
278 
279         try {
280             // Throwable is Serializable so lets just do a deep copy.
281             ByteArrayOutputStream baos = new ByteArrayOutputStream();
282             oos = new ObjectOutputStream(baos);
283             oos.writeObject(error);
284 
285             ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
286             ois = new ObjectInputStream(bais);
287             return (Throwable) ois.readObject();
288         } catch (IOException e) {
289             throw new IllegalStateException(e);
290         } catch (ClassNotFoundException e) {
291             throw new IllegalStateException(e);
292         } finally {
293             if (oos != null) {
294                 try {
295                     oos.close();
296                 } catch (IOException ignore) {
297                     // noop
298                 }
299             }
300             if (ois != null) {
301                 try {
302                     ois.close();
303                 } catch (IOException ignore) {
304                     // noop
305                 }
306             }
307         }
308     }
309 
310     private static final class DnsCacheEntryList extends AbstractList<DnsCacheEntry> {
311         private final List<? extends DnsCacheEntry> entries;
312 
313         DnsCacheEntryList(List<? extends DnsCacheEntry> entries) {
314             this.entries = entries;
315         }
316 
317         @Override
318         public DnsCacheEntry get(int index) {
319             DefaultDnsCacheEntry entry = (DefaultDnsCacheEntry) entries.get(index);
320             // As we dont know what exactly the user is doing with the returned exception (for example
321             // using addSuppressed(...) and so hold up a lot of memory until the entry expires) we do
322             // create a copy.
323             return entry.copyIfNeeded();
324         }
325 
326         @Override
327         public int size() {
328             return entries.size();
329         }
330 
331         @Override
332         public int hashCode() {
333             // Just delegate to super to make checkstyle happy
334             return super.hashCode();
335         }
336 
337         @Override
338         public boolean equals(Object o) {
339             if (o instanceof DnsCacheEntryList) {
340                 // Fast-path.
341                 return entries.equals(((DnsCacheEntryList) o).entries);
342             }
343             return super.equals(o);
344         }
345     };
346 }