From 8d4db050f36929430e6699eec640de8eab1f3df2 Mon Sep 17 00:00:00 2001 From: Stephane Landelle Date: Mon, 14 Dec 2015 15:27:49 +0100 Subject: [PATCH] Have hosts file support for DnsNameResolver, close #4074 Motivation: On contrary to `DefaultNameResolver`, `DnsNameResolver` doesn't currently honor hosts file. Modifications: * Introduce `HostsFileParser` that parses `/etc/hosts` or `C:\Windows\system32\drivers\etc\hosts` depending on the platform * Introduce `HostsFileEntriesResolver` that uses the former to resolve host names * Make `DnsNameResolver` check his `HostsFileEntriesResolver` prior to trying to resolve names against the DNS server * Introduce `DnsNameResolverBuilder` so we now have a builder for `DnsNameResolver`s * Additionally introduce a `CompositeNameResolver` that takes several `NameResolver`s and tries to resolve names by delegating sequentially * Change `DnsNameResolver.asAddressResolver` to return a composite and honor hosts file Result: Hosts file support when using `DnsNameResolver`. Consistent behavior with JDK implementation. --- .../io/netty/util/internal/ObjectUtil.java | 44 ++ .../resolver/dns/DnsAddressResolverGroup.java | 6 +- .../netty/resolver/dns/DnsNameResolver.java | 390 ++++-------------- .../resolver/dns/DnsNameResolverBuilder.java | 312 ++++++++++++++ .../dns/DnsNameResolverException.java | 18 +- .../resolver/dns/DnsNameResolverTest.java | 227 +++++----- .../netty/resolver/CompositeNameResolver.java | 108 +++++ .../DefaultHostsFileEntriesResolver.java | 32 ++ .../netty/resolver/DefaultNameResolver.java | 1 - .../resolver/HostsFileEntriesResolver.java | 31 ++ .../io/netty/resolver/HostsFileParser.java | 171 ++++++++ .../io/netty/resolver/InetNameResolver.java | 3 +- .../resolver/InetSocketAddressResolver.java | 6 +- .../netty/resolver/HostsFileParserTest.java | 53 +++ 14 files changed, 977 insertions(+), 425 deletions(-) create mode 100644 resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java create mode 100644 resolver/src/main/java/io/netty/resolver/CompositeNameResolver.java create mode 100644 resolver/src/main/java/io/netty/resolver/DefaultHostsFileEntriesResolver.java create mode 100644 resolver/src/main/java/io/netty/resolver/HostsFileEntriesResolver.java create mode 100644 resolver/src/main/java/io/netty/resolver/HostsFileParser.java create mode 100644 resolver/src/test/java/io/netty/resolver/HostsFileParserTest.java diff --git a/common/src/main/java/io/netty/util/internal/ObjectUtil.java b/common/src/main/java/io/netty/util/internal/ObjectUtil.java index db127c109b..f5e19515e1 100644 --- a/common/src/main/java/io/netty/util/internal/ObjectUtil.java +++ b/common/src/main/java/io/netty/util/internal/ObjectUtil.java @@ -32,4 +32,48 @@ public final class ObjectUtil { } return arg; } + + /** + * Checks that the given argument is strictly positive. If it is, throws {@link IllegalArgumentException}. + * Otherwise, returns the argument. + */ + public static int checkPositive(int i, String name) { + if (i <= 0) { + throw new IllegalArgumentException(name + ": " + i + " (expected: > 0)"); + } + return i; + } + + /** + * Checks that the given argument is strictly positive. If it is, throws {@link IllegalArgumentException}. + * Otherwise, returns the argument. + */ + public static long checkPositive(long i, String name) { + if (i <= 0) { + throw new IllegalArgumentException(name + ": " + i + " (expected: > 0)"); + } + return i; + } + + /** + * Checks that the given argument is positive or zero. If it is, throws {@link IllegalArgumentException}. + * Otherwise, returns the argument. + */ + public static int checkPositiveOrZero(int i, String name) { + if (i < 0) { + throw new IllegalArgumentException(name + ": " + i + " (expected: >= 0)"); + } + return i; + } + + /** + * Checks that the given argument is neither null nor empty. + * If it is, throws {@link NullPointerException} or {@link IllegalArgumentException}. + * Otherwise, returns the argument. + */ + public static T[] checkNonEmpty(T[] array, String name) { + checkNotNull(array, name); + checkPositive(array.length, name + ".length"); + return array; + } } diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java index 7f53ec37e3..ffc747a648 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java @@ -81,7 +81,11 @@ public class DnsAddressResolverGroup extends AddressResolverGroup channelFactory, InetSocketAddress localAddress, DnsServerAddresses nameServerAddresses) throws Exception { - return new DnsNameResolver(eventLoop, channelFactory, localAddress, nameServerAddresses) + return new DnsNameResolverBuilder(eventLoop) + .channelFactory(channelFactory) + .localAddress(localAddress) + .nameServerAddresses(nameServerAddresses) + .build() .asAddressResolver(); } } diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java index cfd7a7dab8..ba4d9f5eab 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java @@ -25,7 +25,6 @@ import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoop; import io.netty.channel.FixedRecvByteBufAllocator; -import io.netty.channel.ReflectiveChannelFactory; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.InternetProtocolFamily; import io.netty.handler.codec.dns.DatagramDnsQueryEncoder; @@ -33,6 +32,7 @@ import io.netty.handler.codec.dns.DatagramDnsResponse; import io.netty.handler.codec.dns.DatagramDnsResponseDecoder; import io.netty.handler.codec.dns.DnsQuestion; import io.netty.handler.codec.dns.DnsResponse; +import io.netty.resolver.HostsFileEntriesResolver; import io.netty.resolver.InetNameResolver; import io.netty.util.NetUtil; import io.netty.util.ReferenceCountUtil; @@ -56,7 +56,7 @@ import java.util.Map.Entry; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; -import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.*; /** * A DNS-based {@link InetNameResolver}. @@ -67,7 +67,7 @@ public class DnsNameResolver extends InetNameResolver { static final InetSocketAddress ANY_LOCAL_ADDR = new InetSocketAddress(0); - private static final InternetProtocolFamily[] DEFAULT_RESOLVE_ADDRESS_TYPES = new InternetProtocolFamily[2]; + static final InternetProtocolFamily[] DEFAULT_RESOLVE_ADDRESS_TYPES = new InternetProtocolFamily[2]; static { // Note that we did not use SystemPropertyUtil.getBoolean() here to emulate the behavior of JDK. @@ -97,7 +97,7 @@ public class DnsNameResolver extends InetNameResolver { /** * Cache for {@link #doResolve(String, Promise)} and {@link #doResolveAll(String, Promise)}. */ - final ConcurrentMap> resolveCache = PlatformDependent.newConcurrentHashMap(); + private final ConcurrentMap> resolveCache = PlatformDependent.newConcurrentHashMap(); private final FastThreadLocal nameServerAddrStream = new FastThreadLocal() { @@ -107,68 +107,18 @@ public class DnsNameResolver extends InetNameResolver { } }; - private final DnsResponseHandler responseHandler = new DnsResponseHandler(); - - private volatile long queryTimeoutMillis = 5000; - + private final long queryTimeoutMillis; // The default TTL values here respect the TTL returned by the DNS server and do not cache the negative response. - private volatile int minTtl; - private volatile int maxTtl = Integer.MAX_VALUE; - private volatile int negativeTtl; - private volatile int maxQueriesPerResolve = 3; - private volatile boolean traceEnabled = true; - - private volatile InternetProtocolFamily[] resolveAddressTypes = DEFAULT_RESOLVE_ADDRESS_TYPES; - private volatile boolean recursionDesired = true; - - private volatile int maxPayloadSize; - private volatile boolean optResourceEnabled = true; - - /** - * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers. - * - * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers - * @param channelType the type of the {@link DatagramChannel} to create - * @param nameServerAddresses the addresses of the DNS server. For each DNS query, a new stream is created from - * this to determine which DNS server should be contacted for the next retry in case - * of failure. - */ - public DnsNameResolver( - EventLoop eventLoop, Class channelType, - DnsServerAddresses nameServerAddresses) { - this(eventLoop, channelType, ANY_LOCAL_ADDR, nameServerAddresses); - } - - /** - * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers. - * - * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers - * @param channelType the type of the {@link DatagramChannel} to create - * @param localAddress the local address of the {@link DatagramChannel} - * @param nameServerAddresses the addresses of the DNS server. For each DNS query, a new stream is created from - * this to determine which DNS server should be contacted for the next retry in case - * of failure. - */ - public DnsNameResolver( - EventLoop eventLoop, Class channelType, - InetSocketAddress localAddress, DnsServerAddresses nameServerAddresses) { - this(eventLoop, new ReflectiveChannelFactory(channelType), localAddress, nameServerAddresses); - } - - /** - * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers. - * - * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers - * @param channelFactory the {@link ChannelFactory} that will create a {@link DatagramChannel} - * @param nameServerAddresses the addresses of the DNS server. For each DNS query, a new stream is created from - * this to determine which DNS server should be contacted for the next retry in case - * of failure. - */ - public DnsNameResolver( - EventLoop eventLoop, ChannelFactory channelFactory, - DnsServerAddresses nameServerAddresses) { - this(eventLoop, channelFactory, ANY_LOCAL_ADDR, nameServerAddresses); - } + private final int minTtl; + private final int maxTtl; + private final int negativeTtl; + private final int maxQueriesPerResolve; + private final boolean traceEnabled; + private final InternetProtocolFamily[] resolvedAddressTypes; + private final boolean recursionDesired; + private final int maxPayloadSize; + private final boolean optResourceEnabled; + private final HostsFileEntriesResolver hostsFileEntriesResolver; /** * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers. @@ -179,22 +129,58 @@ public class DnsNameResolver extends InetNameResolver { * @param nameServerAddresses the addresses of the DNS server. For each DNS query, a new stream is created from * this to determine which DNS server should be contacted for the next retry in case * of failure. + * @param minTtl the minimum TTL of cached DNS records + * @param maxTtl the maximum TTL of cached DNS records + * @param negativeTtl the TTL for failed cached queries + * @param queryTimeoutMillis timeout of each DNS query in millis + * @param resolvedAddressTypes list of the protocol families + * @param recursionDesired if recursion desired flag must be set + * @param maxQueriesPerResolve the maximum allowed number of DNS queries for a given name resolution + * @param traceEnabled if trace is enabled + * @param maxPayloadSize the capacity of the datagram packet buffer + * @param optResourceEnabled if automatic inclusion of a optional records is enabled + * @param hostsFileEntriesResolver the {@link HostsFileEntriesResolver} used to check for local aliases */ public DnsNameResolver( - EventLoop eventLoop, ChannelFactory channelFactory, - InetSocketAddress localAddress, DnsServerAddresses nameServerAddresses) { + EventLoop eventLoop, + ChannelFactory channelFactory, + InetSocketAddress localAddress, + DnsServerAddresses nameServerAddresses, + int minTtl, + int maxTtl, + int negativeTtl, + long queryTimeoutMillis, + InternetProtocolFamily[] resolvedAddressTypes, + boolean recursionDesired, + int maxQueriesPerResolve, + boolean traceEnabled, + int maxPayloadSize, + boolean optResourceEnabled, + HostsFileEntriesResolver hostsFileEntriesResolver) { super(eventLoop); - checkNotNull(channelFactory, "channelFactory"); - checkNotNull(nameServerAddresses, "nameServerAddresses"); checkNotNull(localAddress, "localAddress"); + this.nameServerAddresses = checkNotNull(nameServerAddresses, "nameServerAddresses"); + this.minTtl = checkPositiveOrZero(minTtl, "minTtl"); + this.maxTtl = checkPositiveOrZero(maxTtl, "maxTtl"); + if (minTtl > maxTtl) { + throw new IllegalArgumentException( + "minTtl: " + minTtl + ", maxTtl: " + maxTtl + " (expected: 0 <= minTtl <= maxTtl)"); + } + this.negativeTtl = checkPositiveOrZero(negativeTtl, "negativeTtl"); + this.queryTimeoutMillis = checkPositive(queryTimeoutMillis, "queryTimeoutMillis"); + this.resolvedAddressTypes = checkNonEmpty(resolvedAddressTypes, "resolvedAddressTypes"); + this.recursionDesired = recursionDesired; + this.maxQueriesPerResolve = checkPositive(maxQueriesPerResolve, "maxQueriesPerResolve"); + this.traceEnabled = traceEnabled; + this.maxPayloadSize = checkPositive(maxPayloadSize, "maxPayloadSize"); + this.optResourceEnabled = optResourceEnabled; + this.hostsFileEntriesResolver = checkNotNull(hostsFileEntriesResolver, "hostsFileEntriesResolver"); - this.nameServerAddresses = nameServerAddresses; bindFuture = newChannel(channelFactory, localAddress); ch = (DatagramChannel) bindFuture.channel(); - - setMaxPayloadSize(4096); + ch.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(maxPayloadSize)); } private ChannelFuture newChannel( @@ -203,6 +189,7 @@ public class DnsNameResolver extends InetNameResolver { Bootstrap b = new Bootstrap(); b.group(executor()); b.channelFactory(channelFactory); + final DnsResponseHandler responseHandler = new DnsResponseHandler(); b.handler(new ChannelInitializer() { @Override protected void initChannel(DatagramChannel ch) throws Exception { @@ -225,7 +212,6 @@ public class DnsNameResolver extends InetNameResolver { * Returns the minimum TTL of the cached DNS resource records (in seconds). * * @see #maxTtl() - * @see #setTtl(int, int) */ public int minTtl() { return minTtl; @@ -235,236 +221,56 @@ public class DnsNameResolver extends InetNameResolver { * Returns the maximum TTL of the cached DNS resource records (in seconds). * * @see #minTtl() - * @see #setTtl(int, int) */ public int maxTtl() { return maxTtl; } - /** - * Sets the minimum and maximum TTL of the cached DNS resource records (in seconds). If the TTL of the DNS resource - * record returned by the DNS server is less than the minimum TTL or greater than the maximum TTL, this resolver - * will ignore the TTL from the DNS server and use the minimum TTL or the maximum TTL instead respectively. - * The default value is {@code 0} and {@link Integer#MAX_VALUE}, which practically tells this resolver to respect - * the TTL from the DNS server. - * - * @return {@code this} - * - * @see #minTtl() - * @see #maxTtl() - */ - public DnsNameResolver setTtl(int minTtl, int maxTtl) { - if (minTtl < 0) { - throw new IllegalArgumentException("minTtl: " + minTtl + " (expected: >= 0)"); - } - if (maxTtl < 0) { - throw new IllegalArgumentException("maxTtl: " + maxTtl + " (expected: >= 0)"); - } - if (minTtl > maxTtl) { - throw new IllegalArgumentException( - "minTtl: " + minTtl + ", maxTtl: " + maxTtl + " (expected: 0 <= minTtl <= maxTtl)"); - } - - this.maxTtl = maxTtl; - this.minTtl = minTtl; - - return this; - } - /** * Returns the TTL of the cache for the failed DNS queries (in seconds). The default value is {@code 0}, which * disables the cache for negative results. - * - * @see #setNegativeTtl(int) */ public int negativeTtl() { return negativeTtl; } - /** - * Sets the TTL of the cache for the failed DNS queries (in seconds). - * - * @return {@code this} - * - * @see #negativeTtl() - */ - public DnsNameResolver setNegativeTtl(int negativeTtl) { - if (negativeTtl < 0) { - throw new IllegalArgumentException("negativeTtl: " + negativeTtl + " (expected: >= 0)"); - } - - this.negativeTtl = negativeTtl; - - return this; - } - /** * Returns the timeout of each DNS query performed by this resolver (in milliseconds). * The default value is 5 seconds. - * - * @see #setQueryTimeoutMillis(long) */ public long queryTimeoutMillis() { return queryTimeoutMillis; } - /** - * Sets the timeout of each DNS query performed by this resolver (in milliseconds). - * - * @return {@code this} - * - * @see #queryTimeoutMillis() - */ - public DnsNameResolver setQueryTimeoutMillis(long queryTimeoutMillis) { - if (queryTimeoutMillis < 0) { - throw new IllegalArgumentException("queryTimeoutMillis: " + queryTimeoutMillis + " (expected: >= 0)"); - } - - this.queryTimeoutMillis = queryTimeoutMillis; - - return this; - } - /** * Returns the list of the protocol families of the address resolved by {@link #resolve(String)} * in the order of preference. * The default value depends on the value of the system property {@code "java.net.preferIPv6Addresses"}. - * - * @see #setResolveAddressTypes(InternetProtocolFamily...) */ - public List resolveAddressTypes() { - return Arrays.asList(resolveAddressTypes); + public List resolvedAddressTypes() { + return Arrays.asList(resolvedAddressTypes); } InternetProtocolFamily[] resolveAddressTypesUnsafe() { - return resolveAddressTypes; - } - - /** - * Sets the list of the protocol families of the address resolved by {@link #resolve(String)}. - * Usually, both {@link InternetProtocolFamily#IPv4} and {@link InternetProtocolFamily#IPv6} are specified in the - * order of preference. To enforce the resolve to retrieve the address of a specific protocol family, specify - * only a single {@link InternetProtocolFamily}. - * - * @return {@code this} - * - * @see #resolveAddressTypes() - */ - public DnsNameResolver setResolveAddressTypes(InternetProtocolFamily... resolveAddressTypes) { - checkNotNull(resolveAddressTypes, "resolveAddressTypes"); - - final List list = - new ArrayList(InternetProtocolFamily.values().length); - - for (InternetProtocolFamily f: resolveAddressTypes) { - if (f == null) { - break; - } - - // Avoid duplicate entries. - if (list.contains(f)) { - continue; - } - - list.add(f); - } - - if (list.isEmpty()) { - throw new IllegalArgumentException("no protocol family specified"); - } - - this.resolveAddressTypes = list.toArray(new InternetProtocolFamily[list.size()]); - - return this; - } - - /** - * Sets the list of the protocol families of the address resolved by {@link #resolve(String)}. - * Usually, both {@link InternetProtocolFamily#IPv4} and {@link InternetProtocolFamily#IPv6} are specified in the - * order of preference. To enforce the resolve to retrieve the address of a specific protocol family, specify - * only a single {@link InternetProtocolFamily}. - * - * @return {@code this} - * - * @see #resolveAddressTypes() - */ - public DnsNameResolver setResolveAddressTypes(Iterable resolveAddressTypes) { - checkNotNull(resolveAddressTypes, "resolveAddressTypes"); - - final List list = - new ArrayList(InternetProtocolFamily.values().length); - - for (InternetProtocolFamily f: resolveAddressTypes) { - if (f == null) { - break; - } - - // Avoid duplicate entries. - if (list.contains(f)) { - continue; - } - - list.add(f); - } - - if (list.isEmpty()) { - throw new IllegalArgumentException("no protocol family specified"); - } - - this.resolveAddressTypes = list.toArray(new InternetProtocolFamily[list.size()]); - - return this; + return resolvedAddressTypes; } /** * Returns {@code true} if and only if this resolver sends a DNS query with the RD (recursion desired) flag set. * The default value is {@code true}. - * - * @see #setRecursionDesired(boolean) */ public boolean isRecursionDesired() { return recursionDesired; } - /** - * Sets if this resolver has to send a DNS query with the RD (recursion desired) flag set. - * - * @return {@code this} - * - * @see #isRecursionDesired() - */ - public DnsNameResolver setRecursionDesired(boolean recursionDesired) { - this.recursionDesired = recursionDesired; - return this; - } - /** * Returns the maximum allowed number of DNS queries to send when resolving a host name. * The default value is {@code 8}. - * - * @see #setMaxQueriesPerResolve(int) */ public int maxQueriesPerResolve() { return maxQueriesPerResolve; } - /** - * Sets the maximum allowed number of DNS queries to send when resolving a host name. - * - * @return {@code this} - * - * @see #maxQueriesPerResolve() - */ - public DnsNameResolver setMaxQueriesPerResolve(int maxQueriesPerResolve) { - if (maxQueriesPerResolve <= 0) { - throw new IllegalArgumentException("maxQueriesPerResolve: " + maxQueriesPerResolve + " (expected: > 0)"); - } - - this.maxQueriesPerResolve = maxQueriesPerResolve; - - return this; - } - /** * Returns if this resolver should generate the detailed trace information in an exception message so that * it is easier to understand the cause of resolution failure. The default value if {@code true}. @@ -473,57 +279,13 @@ public class DnsNameResolver extends InetNameResolver { return traceEnabled; } - /** - * Sets if this resolver should generate the detailed trace information in an exception message so that - * it is easier to understand the cause of resolution failure. - */ - public DnsNameResolver setTraceEnabled(boolean traceEnabled) { - this.traceEnabled = traceEnabled; - return this; - } - /** * Returns the capacity of the datagram packet buffer (in bytes). The default value is {@code 4096} bytes. - * - * @see #setMaxPayloadSize(int) */ public int maxPayloadSize() { return maxPayloadSize; } - /** - * Sets the capacity of the datagram packet buffer (in bytes). The default value is {@code 4096} bytes. - * - * @return {@code this} - * - * @see #maxPayloadSize() - */ - public DnsNameResolver setMaxPayloadSize(int maxPayloadSize) { - if (maxPayloadSize <= 0) { - throw new IllegalArgumentException("maxPayloadSize: " + maxPayloadSize + " (expected: > 0)"); - } - - if (this.maxPayloadSize == maxPayloadSize) { - // Same value; no need to instantiate DnsClass and RecvByteBufAllocator again. - return this; - } - - this.maxPayloadSize = maxPayloadSize; - ch.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(maxPayloadSize)); - - return this; - } - - /** - * Enable the automatic inclusion of a optional records that tries to give the remote DNS server a hint about how - * much data the resolver can read per response. Some DNSServer may not support this and so fail to answer - * queries. If you find problems you may want to disable this. - */ - public DnsNameResolver setOptResourceEnabled(boolean optResourceEnabled) { - this.optResourceEnabled = optResourceEnabled; - return this; - } - /** * Returns the automatic inclusion of a optional records that tries to give the remote DNS server a hint about how * much data the resolver can read per response is enabled. @@ -532,6 +294,14 @@ public class DnsNameResolver extends InetNameResolver { return optResourceEnabled; } + /** + * Returns the component that tries to resolve hostnames against the hosts file prior to asking to + * remotes DNS servers. + */ + public HostsFileEntriesResolver hostsFileEntriesResolver() { + return hostsFileEntriesResolver; + } + /** * Clears all the resolved addresses cached by this resolver. * @@ -590,6 +360,10 @@ public class DnsNameResolver extends InetNameResolver { return (EventLoop) super.executor(); } + private InetAddress resolveHostsFileEntry(String hostname) { + return hostsFileEntriesResolver != null ? hostsFileEntriesResolver.address(hostname) : null; + } + @Override protected void doResolve(String inetHost, Promise promise) throws Exception { final byte[] bytes = NetUtil.createByteArrayFromIpAddressString(inetHost); @@ -601,6 +375,12 @@ public class DnsNameResolver extends InetNameResolver { final String hostname = hostname(inetHost); + InetAddress hostsFileEntry = resolveHostsFileEntry(hostname); + if (hostsFileEntry != null) { + promise.setSuccess(hostsFileEntry); + return; + } + if (!doResolveCached(hostname, promise)) { doResolveUncached(hostname, promise); } @@ -622,7 +402,7 @@ public class DnsNameResolver extends InetNameResolver { cause = cachedEntries.get(0).cause(); } else { // Find the first entry with the preferred address type. - for (InternetProtocolFamily f : resolveAddressTypes) { + for (InternetProtocolFamily f : resolvedAddressTypes) { for (int i = 0; i < numEntries; i++) { final DnsCacheEntry e = cachedEntries.get(i); if (f.addressType().isInstance(e.address())) { @@ -687,6 +467,12 @@ public class DnsNameResolver extends InetNameResolver { final String hostname = hostname(inetHost); + InetAddress hostsFileEntry = resolveHostsFileEntry(hostname); + if (hostsFileEntry != null) { + promise.setSuccess(Collections.singletonList(hostsFileEntry)); + return; + } + if (!doResolveAllCached(hostname, promise)) { doResolveAllUncached(hostname, promise); } @@ -707,7 +493,7 @@ public class DnsNameResolver extends InetNameResolver { if (cachedEntries.get(0).cause() != null) { cause = cachedEntries.get(0).cause(); } else { - for (InternetProtocolFamily f : resolveAddressTypes) { + for (InternetProtocolFamily f : resolvedAddressTypes) { for (int i = 0; i < numEntries; i++) { final DnsCacheEntry e = cachedEntries.get(i); if (f.addressType().isInstance(e.address())) { diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java new file mode 100644 index 0000000000..2e3aa78d36 --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java @@ -0,0 +1,312 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import io.netty.channel.ChannelFactory; +import io.netty.channel.EventLoop; +import io.netty.channel.ReflectiveChannelFactory; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.resolver.HostsFileEntriesResolver; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * A {@link DnsNameResolver} builder. + */ +public final class DnsNameResolverBuilder { + + private final EventLoop eventLoop; + private ChannelFactory channelFactory; + private InetSocketAddress localAddress = DnsNameResolver.ANY_LOCAL_ADDR; + private DnsServerAddresses nameServerAddresses; + private int minTtl; + private int maxTtl = Integer.MAX_VALUE; + private int negativeTtl; + private long queryTimeoutMillis = 5000; + private InternetProtocolFamily[] resolvedAddressTypes = DnsNameResolver.DEFAULT_RESOLVE_ADDRESS_TYPES; + private boolean recursionDesired = true; + private int maxQueriesPerResolve = 3; + private boolean traceEnabled; + private int maxPayloadSize = 4096; + private boolean optResourceEnabled = true; + private HostsFileEntriesResolver hostsFileEntriesResolver = HostsFileEntriesResolver.DEFAULT; + + /** + * Creates a new builder. + * + * @param eventLoop the {@link EventLoop} the {@link EventLoop} which will perform the communication with the DNS + * servers. + */ + public DnsNameResolverBuilder(EventLoop eventLoop) { + this.eventLoop = eventLoop; + } + + /** + * Sets the {@link ChannelFactory} that will create a {@link DatagramChannel}. + * + * @param channelFactory the {@link ChannelFactory} + * @return {@code this} + */ + public DnsNameResolverBuilder channelFactory(ChannelFactory channelFactory) { + this.channelFactory = channelFactory; + return this; + } + + /** + * Sets the {@link ChannelFactory} as a {@link ReflectiveChannelFactory} of this type. + * Use as an alternative to {@link #channelFactory(ChannelFactory)}. + * + * @param channelType + * @return {@code this} + */ + public DnsNameResolverBuilder channelType(Class channelType) { + return channelFactory(new ReflectiveChannelFactory(channelType)); + } + + /** + * Sets the local address of the {@link DatagramChannel} + * + * @param localAddress the local address + * @return {@code this} + */ + public DnsNameResolverBuilder localAddress(InetSocketAddress localAddress) { + this.localAddress = localAddress; + return this; + } + + /** + * Sets the addresses of the DNS server. + * + * @param nameServerAddresses the DNS server addresses + * @return {@code this} + */ + public DnsNameResolverBuilder nameServerAddresses(DnsServerAddresses nameServerAddresses) { + this.nameServerAddresses = nameServerAddresses; + return this; + } + + /** + * Sets the minimum and maximum TTL of the cached DNS resource records (in seconds). If the TTL of the DNS + * resource record returned by the DNS server is less than the minimum TTL or greater than the maximum TTL, + * this resolver will ignore the TTL from the DNS server and use the minimum TTL or the maximum TTL instead + * respectively. + * The default value is {@code 0} and {@link Integer#MAX_VALUE}, which practically tells this resolver to + * respect the TTL from the DNS server. + * + * @param minTtl the minimum TTL + * @param maxTtl the maximum TTL + * @return {@code this} + */ + public DnsNameResolverBuilder ttl(int minTtl, int maxTtl) { + this.maxTtl = maxTtl; + this.minTtl = minTtl; + return this; + } + + /** + * Sets the TTL of the cache for the failed DNS queries (in seconds). + * + * @param negativeTtl the TTL for failed cached queries + * @return {@code this} + */ + public DnsNameResolverBuilder negativeTtl(int negativeTtl) { + this.negativeTtl = negativeTtl; + return this; + } + + /** + * Sets the timeout of each DNS query performed by this resolver (in milliseconds). + * + * @param queryTimeoutMillis the query timeout + * @return {@code this} + */ + public DnsNameResolverBuilder queryTimeoutMillis(long queryTimeoutMillis) { + this.queryTimeoutMillis = queryTimeoutMillis; + return this; + } + + /** + * Sets the list of the protocol families of the address resolved. + * Usually, both {@link InternetProtocolFamily#IPv4} and {@link InternetProtocolFamily#IPv6} are specified in + * the order of preference. To enforce the resolve to retrieve the address of a specific protocol family, + * specify only a single {@link InternetProtocolFamily}. + * + * @param resolvedAddressTypes the address types + * @return {@code this} + */ + public DnsNameResolverBuilder resolvedAddressTypes(InternetProtocolFamily... resolvedAddressTypes) { + checkNotNull(resolvedAddressTypes, "resolvedAddressTypes"); + + final List list = + new ArrayList(InternetProtocolFamily.values().length); + + for (InternetProtocolFamily f : resolvedAddressTypes) { + if (f == null) { + break; + } + + // Avoid duplicate entries. + if (list.contains(f)) { + continue; + } + + list.add(f); + } + + if (list.isEmpty()) { + throw new IllegalArgumentException("no protocol family specified"); + } + + this.resolvedAddressTypes = list.toArray(new InternetProtocolFamily[list.size()]); + + return this; + } + + /** + * Sets the list of the protocol families of the address resolved. + * Usually, both {@link InternetProtocolFamily#IPv4} and {@link InternetProtocolFamily#IPv6} are specified in + * the order of preference. To enforce the resolve to retrieve the address of a specific protocol family, + * specify only a single {@link InternetProtocolFamily}. + * + * @param resolvedAddressTypes the address types + * @return {@code this} + */ + public DnsNameResolverBuilder resolvedAddressTypes(Iterable resolvedAddressTypes) { + checkNotNull(resolvedAddressTypes, "resolveAddressTypes"); + + final List list = + new ArrayList(InternetProtocolFamily.values().length); + + for (InternetProtocolFamily f : resolvedAddressTypes) { + if (f == null) { + break; + } + + // Avoid duplicate entries. + if (list.contains(f)) { + continue; + } + + list.add(f); + } + + if (list.isEmpty()) { + throw new IllegalArgumentException("no protocol family specified"); + } + + this.resolvedAddressTypes = list.toArray(new InternetProtocolFamily[list.size()]); + + return this; + } + + /** + * Sets if this resolver has to send a DNS query with the RD (recursion desired) flag set. + * + * @param recursionDesired true if recursion is desired + * @return {@code this} + */ + public DnsNameResolverBuilder recursionDesired(boolean recursionDesired) { + this.recursionDesired = recursionDesired; + return this; + } + + /** + * Sets the maximum allowed number of DNS queries to send when resolving a host name. + * + * @param maxQueriesPerResolve the max number of queries + * @return {@code this} + */ + public DnsNameResolverBuilder maxQueriesPerResolve(int maxQueriesPerResolve) { + this.maxQueriesPerResolve = maxQueriesPerResolve; + return this; + } + + /** + * Sets if this resolver should generate the detailed trace information in an exception message so that + * it is easier to understand the cause of resolution failure. + * + * @param traceEnabled true if trace is enabled + * @return {@code this} + */ + public DnsNameResolverBuilder traceEnabled(boolean traceEnabled) { + this.traceEnabled = traceEnabled; + return this; + } + + /** + * Sets the capacity of the datagram packet buffer (in bytes). The default value is {@code 4096} bytes. + * + * @param maxPayloadSize the capacity of the datagram packet buffer + * @return {@code this} + */ + public DnsNameResolverBuilder maxPayloadSize(int maxPayloadSize) { + this.maxPayloadSize = maxPayloadSize; + return this; + } + + /** + * Enable the automatic inclusion of a optional records that tries to give the remote DNS server a hint about + * how much data the resolver can read per response. Some DNSServer may not support this and so fail to answer + * queries. If you find problems you may want to disable this. + * + * @param optResourceEnabled if optional records inclusion is enabled + * @return {@code this} + */ + public DnsNameResolverBuilder optResourceEnabled(boolean optResourceEnabled) { + this.optResourceEnabled = optResourceEnabled; + return this; + } + + /** + * @param hostsFileEntriesResolver the {@link HostsFileEntriesResolver} used to first check + * if the hostname is locally aliased. + * @param hostsFileEntriesResolver the {@link HostsFileEntriesResolver} + * @return {@code this} + */ + public DnsNameResolverBuilder hostsFileEntriesResolver(HostsFileEntriesResolver hostsFileEntriesResolver) { + this.hostsFileEntriesResolver = hostsFileEntriesResolver; + return this; + } + + /** + * Returns a new {@link DnsNameResolver} instance. + * + * @return a {@link DnsNameResolver} + */ + public DnsNameResolver build() { + return new DnsNameResolver( + eventLoop, + channelFactory, + localAddress, + nameServerAddresses, + minTtl, + maxTtl, + negativeTtl, + queryTimeoutMillis, + resolvedAddressTypes, + recursionDesired, + maxQueriesPerResolve, + traceEnabled, + maxPayloadSize, + optResourceEnabled, + hostsFileEntriesResolver); + } +} diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java index 3393ac2b1f..05eb8757bd 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java @@ -31,11 +31,6 @@ public final class DnsNameResolverException extends RuntimeException { private final InetSocketAddress remoteAddress; private final DnsQuestion question; - public DnsNameResolverException(InetSocketAddress remoteAddress, DnsQuestion question) { - this.remoteAddress = validateRemoteAddress(remoteAddress); - this.question = validateQuestion(question); - } - public DnsNameResolverException(InetSocketAddress remoteAddress, DnsQuestion question, String message) { super(message); this.remoteAddress = validateRemoteAddress(remoteAddress); @@ -49,12 +44,6 @@ public final class DnsNameResolverException extends RuntimeException { this.question = validateQuestion(question); } - public DnsNameResolverException(InetSocketAddress remoteAddress, DnsQuestion question, Throwable cause) { - super(cause); - this.remoteAddress = validateRemoteAddress(remoteAddress); - this.question = validateQuestion(question); - } - private static InetSocketAddress validateRemoteAddress(InetSocketAddress remoteAddress) { return ObjectUtil.checkNotNull(remoteAddress, "remoteAddress"); } @@ -63,6 +52,13 @@ public final class DnsNameResolverException extends RuntimeException { return ObjectUtil.checkNotNull(question, "question"); } + /** + * Returns the {@link InetSocketAddress} of the DNS query that has failed. + */ + public InetSocketAddress remoteAddress() { + return remoteAddress; + } + /** * Returns the {@link DnsQuestion} of the DNS query that has failed. */ diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java index 858215aae6..87389699df 100644 --- a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java @@ -58,7 +58,6 @@ import org.apache.mina.filter.codec.ProtocolEncoder; import org.apache.mina.filter.codec.ProtocolEncoderOutput; import org.apache.mina.transport.socket.DatagramAcceptor; import org.apache.mina.transport.socket.DatagramSessionConfig; -import org.junit.After; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -265,15 +264,23 @@ public class DnsNameResolverTest { private static final TestDnsServer dnsServer = new TestDnsServer(); private static final EventLoopGroup group = new NioEventLoopGroup(1); - private static DnsNameResolver resolver; + + private DnsNameResolverBuilder newResolver() { + return new DnsNameResolverBuilder(group.next()) + .channelType(NioDatagramChannel.class) + .nameServerAddresses(DnsServerAddresses.singleton(dnsServer.localAddress())) + .maxQueriesPerResolve(1) + .optResourceEnabled(false); + } + + private DnsNameResolverBuilder newResolver(InternetProtocolFamily... resolvedAddressTypes) { + return newResolver() + .resolvedAddressTypes(resolvedAddressTypes); + } @BeforeClass public static void init() throws Exception { dnsServer.start(); - resolver = new DnsNameResolver(group.next(), NioDatagramChannel.class, - DnsServerAddresses.singleton(dnsServer.localAddress())); - resolver.setMaxQueriesPerResolve(1); - resolver.setOptResourceEnabled(false); } @AfterClass public static void destroy() { @@ -281,37 +288,40 @@ public class DnsNameResolverTest { group.shutdownGracefully(); } - @After - public void reset() throws Exception { - resolver.clearCache(); - } - @Test public void testResolveAorAAAA() throws Exception { - testResolve0(EXCLUSIONS_RESOLVE_A, InternetProtocolFamily.IPv4, InternetProtocolFamily.IPv6); + DnsNameResolver resolver = newResolver(InternetProtocolFamily.IPv4, InternetProtocolFamily.IPv6).build(); + try { + testResolve0(resolver, EXCLUSIONS_RESOLVE_A); + } finally { + resolver.close(); + } } @Test public void testResolveAAAAorA() throws Exception { - testResolve0(EXCLUSIONS_RESOLVE_A, InternetProtocolFamily.IPv6, InternetProtocolFamily.IPv4); + DnsNameResolver resolver = newResolver(InternetProtocolFamily.IPv6, InternetProtocolFamily.IPv4).build(); + try { + testResolve0(resolver, EXCLUSIONS_RESOLVE_A); + } finally { + resolver.close(); + } } @Test public void testResolveA() throws Exception { - final int oldMinTtl = resolver.minTtl(); - final int oldMaxTtl = resolver.maxTtl(); - - // Cache for eternity. - resolver.setTtl(Integer.MAX_VALUE, Integer.MAX_VALUE); - + DnsNameResolver resolver = newResolver(InternetProtocolFamily.IPv4) + // Cache for eternity + .ttl(Integer.MAX_VALUE, Integer.MAX_VALUE) + .build(); try { - final Map resultA = testResolve0(EXCLUSIONS_RESOLVE_A, InternetProtocolFamily.IPv4); + final Map resultA = testResolve0(resolver, EXCLUSIONS_RESOLVE_A); // Now, try to resolve again to see if it's cached. // This test works because the DNS servers usually randomizes the order of the records in a response. // If cached, the resolved addresses must be always same, because we reuse the same response. - final Map resultB = testResolve0(EXCLUSIONS_RESOLVE_A, InternetProtocolFamily.IPv4); + final Map resultB = testResolve0(resolver, EXCLUSIONS_RESOLVE_A); // Ensure the result from the cache is identical from the uncached one. assertThat(resultB.size(), is(resultA.size())); @@ -325,61 +335,56 @@ public class DnsNameResolverTest { assertThat(actual, is(expected)); } } finally { - // Restore the TTL configuration. - resolver.setTtl(oldMinTtl, oldMaxTtl); + resolver.close(); } } @Test public void testResolveAAAA() throws Exception { - testResolve0(EXCLUSIONS_RESOLVE_AAAA, InternetProtocolFamily.IPv6); + DnsNameResolver resolver = newResolver(InternetProtocolFamily.IPv6).build(); + try { + testResolve0(resolver, EXCLUSIONS_RESOLVE_AAAA); + } finally { + resolver.close(); + } } - private static Map testResolve0( - Set excludedDomains, InternetProtocolFamily... famililies) throws InterruptedException { - - final List oldResolveAddressTypes = resolver.resolveAddressTypes(); + private Map testResolve0(DnsNameResolver resolver, Set excludedDomains) + throws InterruptedException { assertThat(resolver.isRecursionDesired(), is(true)); - assertThat(oldResolveAddressTypes.size(), is(InternetProtocolFamily.values().length)); - - resolver.setResolveAddressTypes(famililies); final Map results = new HashMap(); - try { - final Map> futures = - new LinkedHashMap>(); + final Map> futures = + new LinkedHashMap>(); - for (String name : DOMAINS) { - if (excludedDomains.contains(name)) { - continue; - } - - resolve(futures, name); + for (String name : DOMAINS) { + if (excludedDomains.contains(name)) { + continue; } - for (Entry> e : futures.entrySet()) { - String unresolved = e.getKey(); - InetAddress resolved = e.getValue().sync().getNow(); + resolve(resolver, futures, name); + } - logger.info("{}: {}", unresolved, resolved.getHostAddress()); + for (Entry> e : futures.entrySet()) { + String unresolved = e.getKey(); + InetAddress resolved = e.getValue().sync().getNow(); - assertThat(resolved.getHostName(), is(unresolved)); + logger.info("{}: {}", unresolved, resolved.getHostAddress()); - boolean typeMatches = false; - for (InternetProtocolFamily f: famililies) { - Class resolvedType = resolved.getClass(); - if (f.addressType().isAssignableFrom(resolvedType)) { - typeMatches = true; - } + assertThat(resolved.getHostName(), is(unresolved)); + + boolean typeMatches = false; + for (InternetProtocolFamily f: resolver.resolvedAddressTypes()) { + Class resolvedType = resolved.getClass(); + if (f.addressType().isAssignableFrom(resolvedType)) { + typeMatches = true; } - - assertThat(typeMatches, is(true)); - - results.put(resolved.getHostName(), resolved); } - } finally { - resolver.setResolveAddressTypes(oldResolveAddressTypes); + + assertThat(typeMatches, is(true)); + + results.put(resolved.getHostName(), resolved); } return results; @@ -387,61 +392,65 @@ public class DnsNameResolverTest { @Test public void testQueryMx() throws Exception { - assertThat(resolver.isRecursionDesired(), is(true)); + DnsNameResolver resolver = newResolver().build(); + try { + assertThat(resolver.isRecursionDesired(), is(true)); - Map>> futures = - new LinkedHashMap>>(); - for (String name: DOMAINS) { - if (EXCLUSIONS_QUERY_MX.contains(name)) { - continue; - } - - queryMx(futures, name); - } - - for (Entry>> e: futures.entrySet()) { - String hostname = e.getKey(); - Future> f = e.getValue().awaitUninterruptibly(); - - DnsResponse response = f.getNow().content(); - assertThat(response.code(), is(DnsResponseCode.NOERROR)); - - final int answerCount = response.count(DnsSection.ANSWER); - final List mxList = new ArrayList(answerCount); - for (int i = 0; i < answerCount; i ++) { - final DnsRecord r = response.recordAt(DnsSection.ANSWER, i); - if (r.type() == DnsRecordType.MX) { - mxList.add(r); + Map>> futures = + new LinkedHashMap>>(); + for (String name: DOMAINS) { + if (EXCLUSIONS_QUERY_MX.contains(name)) { + continue; } + + queryMx(resolver, futures, name); } - assertThat(mxList.size(), is(greaterThan(0))); - StringBuilder buf = new StringBuilder(); - for (DnsRecord r: mxList) { - ByteBuf recordContent = ((ByteBufHolder) r).content(); + for (Entry>> e: futures.entrySet()) { + String hostname = e.getKey(); + Future> f = e.getValue().awaitUninterruptibly(); - buf.append(StringUtil.NEWLINE); - buf.append('\t'); - buf.append(r.name()); - buf.append(' '); - buf.append(r.type().name()); - buf.append(' '); - buf.append(recordContent.readUnsignedShort()); - buf.append(' '); - buf.append(DnsNameResolverContext.decodeDomainName(recordContent)); + DnsResponse response = f.getNow().content(); + assertThat(response.code(), is(DnsResponseCode.NOERROR)); + + final int answerCount = response.count(DnsSection.ANSWER); + final List mxList = new ArrayList(answerCount); + for (int i = 0; i < answerCount; i ++) { + final DnsRecord r = response.recordAt(DnsSection.ANSWER, i); + if (r.type() == DnsRecordType.MX) { + mxList.add(r); + } + } + + assertThat(mxList.size(), is(greaterThan(0))); + StringBuilder buf = new StringBuilder(); + for (DnsRecord r: mxList) { + ByteBuf recordContent = ((ByteBufHolder) r).content(); + + buf.append(StringUtil.NEWLINE); + buf.append('\t'); + buf.append(r.name()); + buf.append(' '); + buf.append(r.type().name()); + buf.append(' '); + buf.append(recordContent.readUnsignedShort()); + buf.append(' '); + buf.append(DnsNameResolverContext.decodeDomainName(recordContent)); + } + + logger.info("{} has the following MX records:{}", hostname, buf); + response.release(); } - - logger.info("{} has the following MX records:{}", hostname, buf); - response.release(); + } finally { + resolver.close(); } } @Test public void testNegativeTtl() throws Exception { - final int oldNegativeTtl = resolver.negativeTtl(); - resolver.setNegativeTtl(10); + final DnsNameResolver resolver = newResolver().negativeTtl(10).build(); try { - resolveNonExistentDomain(); + resolveNonExistentDomain(resolver); final int size = 10000; final List exceptions = new ArrayList(); @@ -451,7 +460,7 @@ public class DnsNameResolverTest { @Override public void run() { for (int i = 0; i < size; i++) { - exceptions.add(resolveNonExistentDomain()); + exceptions.add(resolveNonExistentDomain(resolver)); if (isInterrupted()) { break; } @@ -469,11 +478,11 @@ public class DnsNameResolverTest { assertThat(exceptions, hasSize(size)); } finally { - resolver.setNegativeTtl(oldNegativeTtl); + resolver.close(); } } - private static UnknownHostException resolveNonExistentDomain() { + private UnknownHostException resolveNonExistentDomain(DnsNameResolver resolver) { try { resolver.resolve("non-existent.netty.io").sync(); fail(); @@ -486,17 +495,23 @@ public class DnsNameResolverTest { @Test public void testResolveIp() { - InetAddress address = resolver.resolve("10.0.0.1").syncUninterruptibly().getNow(); + DnsNameResolver resolver = newResolver().build(); + try { + InetAddress address = resolver.resolve("10.0.0.1").syncUninterruptibly().getNow(); - assertEquals("10.0.0.1", address.getHostName()); + assertEquals("10.0.0.1", address.getHostName()); + } finally { + resolver.close(); + } } - private static void resolve(Map> futures, String hostname) { + private void resolve(DnsNameResolver resolver, Map> futures, String hostname) { futures.put(hostname, resolver.resolve(hostname)); } private static void queryMx( + DnsNameResolver resolver, Map>> futures, String hostname) throws Exception { futures.put(hostname, resolver.query(new DefaultDnsQuestion(hostname, DnsRecordType.MX))); diff --git a/resolver/src/main/java/io/netty/resolver/CompositeNameResolver.java b/resolver/src/main/java/io/netty/resolver/CompositeNameResolver.java new file mode 100644 index 0000000000..7f5abdf0d7 --- /dev/null +++ b/resolver/src/main/java/io/netty/resolver/CompositeNameResolver.java @@ -0,0 +1,108 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver; + +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.Promise; + +import java.util.Arrays; +import java.util.List; + +import static io.netty.util.internal.ObjectUtil.*; + +/** + * A composite {@link SimpleNameResolver} that resolves a host name against a sequence of {@link NameResolver}s. + * + * In case of a failure, only the last one will be reported. + */ +public final class CompositeNameResolver extends SimpleNameResolver { + + private final NameResolver[] resolvers; + + /** + * @param executor the {@link EventExecutor} which is used to notify the listeners of the {@link Future} returned + * by {@link #resolve(String)} + * @param resolvers the {@link NameResolver}s to be tried sequentially + */ + public CompositeNameResolver(EventExecutor executor, NameResolver... resolvers) { + super(executor); + checkNotNull(resolvers, "resolvers"); + for (int i = 0; i < resolvers.length; i++) { + if (resolvers[i] == null) { + throw new NullPointerException("resolvers[" + i + ']'); + } + } + if (resolvers.length < 2) { + throw new IllegalArgumentException("resolvers: " + Arrays.asList(resolvers) + + " (expected: at least 2 resolvers)"); + } + this.resolvers = resolvers.clone(); + } + + @Override + protected void doResolve(String inetHost, Promise promise) throws Exception { + doResolveRec(inetHost, promise, 0, null); + } + + private void doResolveRec(final String inetHost, + final Promise promise, + final int resolverIndex, + Throwable lastFailure) throws Exception { + if (resolverIndex >= resolvers.length) { + promise.setFailure(lastFailure); + } else { + NameResolver resolver = resolvers[resolverIndex]; + resolver.resolve(inetHost).addListener(new FutureListener() { + @Override + public void operationComplete(Future future) throws Exception { + if (future.isSuccess()) { + promise.setSuccess(future.getNow()); + } else { + doResolveRec(inetHost, promise, resolverIndex + 1, future.cause()); + } + } + }); + } + } + + @Override + protected void doResolveAll(String inetHost, Promise> promise) throws Exception { + doResolveAllRec(inetHost, promise, 0, null); + } + + private void doResolveAllRec(final String inetHost, + final Promise> promise, + final int resolverIndex, + Throwable lastFailure) throws Exception { + if (resolverIndex >= resolvers.length) { + promise.setFailure(lastFailure); + } else { + NameResolver resolver = resolvers[resolverIndex]; + resolver.resolveAll(inetHost).addListener(new FutureListener>() { + @Override + public void operationComplete(Future> future) throws Exception { + if (future.isSuccess()) { + promise.setSuccess(future.getNow()); + } else { + doResolveAllRec(inetHost, promise, resolverIndex + 1, future.cause()); + } + } + }); + } + } +} diff --git a/resolver/src/main/java/io/netty/resolver/DefaultHostsFileEntriesResolver.java b/resolver/src/main/java/io/netty/resolver/DefaultHostsFileEntriesResolver.java new file mode 100644 index 0000000000..3d19042e5c --- /dev/null +++ b/resolver/src/main/java/io/netty/resolver/DefaultHostsFileEntriesResolver.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver; + +import java.net.InetAddress; +import java.util.Map; + +/** + * Default {@link HostsFileEntriesResolver} that resolves hosts file entries only once. + */ +public final class DefaultHostsFileEntriesResolver implements HostsFileEntriesResolver { + + private final Map entries = HostsFileParser.parseSilently(); + + @Override + public InetAddress address(String inetHost) { + return entries.get(inetHost); + } +} diff --git a/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java b/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java index 751e463ae5..944cea2776 100644 --- a/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java +++ b/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java @@ -20,7 +20,6 @@ import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Promise; import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.Arrays; import java.util.List; diff --git a/resolver/src/main/java/io/netty/resolver/HostsFileEntriesResolver.java b/resolver/src/main/java/io/netty/resolver/HostsFileEntriesResolver.java new file mode 100644 index 0000000000..35bae47acb --- /dev/null +++ b/resolver/src/main/java/io/netty/resolver/HostsFileEntriesResolver.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver; + +import java.net.InetAddress; + +/** + * Resolves a hostname against the hosts file entries. + */ +public interface HostsFileEntriesResolver { + + /** + * Default instance: a {@link DefaultHostsFileEntriesResolver}. + */ + HostsFileEntriesResolver DEFAULT = new DefaultHostsFileEntriesResolver(); + + InetAddress address(String inetHost); +} diff --git a/resolver/src/main/java/io/netty/resolver/HostsFileParser.java b/resolver/src/main/java/io/netty/resolver/HostsFileParser.java new file mode 100644 index 0000000000..38272d3dac --- /dev/null +++ b/resolver/src/main/java/io/netty/resolver/HostsFileParser.java @@ -0,0 +1,171 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver; + +import io.netty.util.NetUtil; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.HashMap; +import java.util.Map; + +import static io.netty.util.internal.ObjectUtil.*; + +/** + * A parser for hosts files. + */ +public final class HostsFileParser { + + private static final String WINDOWS_DEFAULT_SYSTEM_ROOT = "C:\\Windows"; + private static final String WINDOWS_HOSTS_FILE_RELATIVE_PATH = "\\system32\\drivers\\etc\\hosts"; + private static final String X_PLATFORMS_HOSTS_FILE_PATH = "/etc/hosts"; + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(HostsFileParser.class); + + private static File locateHostsFile() { + File hostsFile; + if (PlatformDependent.isWindows()) { + hostsFile = new File(System.getenv("SystemRoot") + WINDOWS_HOSTS_FILE_RELATIVE_PATH); + if (!hostsFile.exists()) { + hostsFile = new File(WINDOWS_DEFAULT_SYSTEM_ROOT + WINDOWS_HOSTS_FILE_RELATIVE_PATH); + } + } else { + hostsFile = new File(X_PLATFORMS_HOSTS_FILE_PATH); + } + return hostsFile; + } + + /** + * Parse hosts file at standard OS location. + * + * @return a map of hostname or alias to {@link InetAddress} + */ + public static Map parseSilently() { + File hostsFile = locateHostsFile(); + try { + return parse(hostsFile); + } catch (IOException e) { + logger.warn("Failed to load and parse hosts file at " + hostsFile.getPath(), e); + return Collections.emptyMap(); + } + } + + /** + * Parse hosts file at standard OS location. + * + * @return a map of hostname or alias to {@link InetAddress} + * @throws IOException file could not be read + */ + public static Map parse() throws IOException { + return parse(locateHostsFile()); + } + + /** + * Parse a hosts file. + * + * @param file the file to be parsed + * @return a map of hostname or alias to {@link InetAddress} + * @throws IOException file could not be read + */ + public static Map parse(File file) throws IOException { + checkNotNull(file, "file"); + if (file.exists() && file.isFile()) { + return parse(new BufferedReader(new FileReader(file))); + } else { + return Collections.emptyMap(); + } + } + + /** + * Parse a reader of hosts file format. + * + * @param reader the file to be parsed + * @return a map of hostname or alias to {@link InetAddress} + * @throws IOException file could not be read + */ + public static Map parse(Reader reader) throws IOException { + checkNotNull(reader, "reader"); + BufferedReader buff = new BufferedReader(reader); + try { + Map entries = new HashMap(); + String line; + while ((line = buff.readLine()) != null) { + // remove comment + int commentPosition = line.indexOf('#'); + if (commentPosition != -1) { + line = line.substring(0, commentPosition); + } + // skip empty lines + line = line.trim(); + if (line.isEmpty()) { + continue; + } + + // split + List lineParts = new ArrayList(); + for (String s: line.split("[ \t]+")) { + if (!s.isEmpty()) { + lineParts.add(s); + } + } + + // a valid line should be [IP, hostname, alias*] + if (lineParts.size() < 2) { + // skip invalid line + continue; + } + + byte[] ipBytes = NetUtil.createByteArrayFromIpAddressString(lineParts.get(0)); + + if (ipBytes == null) { + // skip invalid IP + continue; + } + + InetAddress inetAddress = InetAddress.getByAddress(ipBytes); + + // loop over hostname and aliases + for (int i = 1; i < lineParts.size(); i ++) { + String hostname = lineParts.get(i); + if (!entries.containsKey(hostname)) { + // trying to map a host to multiple IPs is wrong + // only the first entry is honored + entries.put(hostname, inetAddress); + } + } + } + return entries; + } finally { + buff.close(); + } + } + + /** + * Can't be instantiated. + */ + private HostsFileParser() { + } +} diff --git a/resolver/src/main/java/io/netty/resolver/InetNameResolver.java b/resolver/src/main/java/io/netty/resolver/InetNameResolver.java index debc694aa1..c0ca21c72c 100644 --- a/resolver/src/main/java/io/netty/resolver/InetNameResolver.java +++ b/resolver/src/main/java/io/netty/resolver/InetNameResolver.java @@ -37,7 +37,8 @@ public abstract class InetNameResolver extends SimpleNameResolver { } /** - * Creates a new {@link AddressResolver} that will use this name resolver underneath. + * Return a {@link AddressResolver} that will use this name resolver underneath. + * It's cached internally, so the same instance is always returned. */ public AddressResolver asAddressResolver() { AddressResolver result = addressResolver; diff --git a/resolver/src/main/java/io/netty/resolver/InetSocketAddressResolver.java b/resolver/src/main/java/io/netty/resolver/InetSocketAddressResolver.java index a6114b0d9b..80d4c51f81 100644 --- a/resolver/src/main/java/io/netty/resolver/InetSocketAddressResolver.java +++ b/resolver/src/main/java/io/netty/resolver/InetSocketAddressResolver.java @@ -17,7 +17,7 @@ package io.netty.resolver; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.concurrent.FutureListener; import io.netty.util.concurrent.Promise; import java.net.InetAddress; @@ -53,7 +53,7 @@ public class InetSocketAddressResolver extends AbstractAddressResolver>() { + .addListener(new FutureListener() { @Override public void operationComplete(Future future) throws Exception { if (future.isSuccess()) { @@ -71,7 +71,7 @@ public class InetSocketAddressResolver extends AbstractAddressResolver>>() { + .addListener(new FutureListener>() { @Override public void operationComplete(Future> future) throws Exception { if (future.isSuccess()) { diff --git a/resolver/src/test/java/io/netty/resolver/HostsFileParserTest.java b/resolver/src/test/java/io/netty/resolver/HostsFileParserTest.java new file mode 100644 index 0000000000..d326133895 --- /dev/null +++ b/resolver/src/test/java/io/netty/resolver/HostsFileParserTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver; + +import org.junit.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.net.InetAddress; +import java.util.Map; + +import static org.junit.Assert.*; + +public class HostsFileParserTest { + + @Test + public void testParse() throws IOException { + String hostsString = new StringBuilder() + .append("127.0.0.1 host1").append("\n") // single hostname, separated with blanks + .append("\n") // empty line + .append("192.168.0.1\thost2").append("\n") // single hostname, separated with tabs + .append("#comment").append("\n") // comment at the beginning of the line + .append(" #comment ").append("\n") // comment in the middle of the line + .append("192.168.0.2 host3 #comment").append("\n") // comment after hostname + .append("192.168.0.3 host4 host5 host6").append("\n") // multiple aliases + .append("192.168.0.4 host4").append("\n") // host mapped to a second address, must be ignored + .toString(); + + Map entries = HostsFileParser.parse(new BufferedReader(new StringReader(hostsString))); + + assertEquals("Expected 6 entries", 6, entries.size()); + assertEquals("127.0.0.1", entries.get("host1").getHostAddress()); + assertEquals("192.168.0.1", entries.get("host2").getHostAddress()); + assertEquals("192.168.0.2", entries.get("host3").getHostAddress()); + assertEquals("192.168.0.3", entries.get("host4").getHostAddress()); + assertEquals("192.168.0.3", entries.get("host5").getHostAddress()); + assertEquals("192.168.0.3", entries.get("host6").getHostAddress()); + } +}