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.
This commit is contained in:
Stephane Landelle 2015-12-14 15:27:49 +01:00 committed by Norman Maurer
parent 4e467f5c6f
commit 8d4db050f3
14 changed files with 977 additions and 425 deletions

View File

@ -32,4 +32,48 @@ public final class ObjectUtil {
} }
return arg; 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> T[] checkNonEmpty(T[] array, String name) {
checkNotNull(array, name);
checkPositive(array.length, name + ".length");
return array;
}
} }

View File

@ -81,7 +81,11 @@ public class DnsAddressResolverGroup extends AddressResolverGroup<InetSocketAddr
EventLoop eventLoop, ChannelFactory<? extends DatagramChannel> channelFactory, EventLoop eventLoop, ChannelFactory<? extends DatagramChannel> channelFactory,
InetSocketAddress localAddress, DnsServerAddresses nameServerAddresses) throws Exception { 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(); .asAddressResolver();
} }
} }

View File

@ -25,7 +25,6 @@ import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoop; import io.netty.channel.EventLoop;
import io.netty.channel.FixedRecvByteBufAllocator; import io.netty.channel.FixedRecvByteBufAllocator;
import io.netty.channel.ReflectiveChannelFactory;
import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.InternetProtocolFamily; import io.netty.channel.socket.InternetProtocolFamily;
import io.netty.handler.codec.dns.DatagramDnsQueryEncoder; 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.DatagramDnsResponseDecoder;
import io.netty.handler.codec.dns.DnsQuestion; import io.netty.handler.codec.dns.DnsQuestion;
import io.netty.handler.codec.dns.DnsResponse; import io.netty.handler.codec.dns.DnsResponse;
import io.netty.resolver.HostsFileEntriesResolver;
import io.netty.resolver.InetNameResolver; import io.netty.resolver.InetNameResolver;
import io.netty.util.NetUtil; import io.netty.util.NetUtil;
import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCountUtil;
@ -56,7 +56,7 @@ import java.util.Map.Entry;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit; 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}. * A DNS-based {@link InetNameResolver}.
@ -67,7 +67,7 @@ public class DnsNameResolver extends InetNameResolver {
static final InetSocketAddress ANY_LOCAL_ADDR = new InetSocketAddress(0); 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 { static {
// Note that we did not use SystemPropertyUtil.getBoolean() here to emulate the behavior of JDK. // 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)}. * Cache for {@link #doResolve(String, Promise)} and {@link #doResolveAll(String, Promise)}.
*/ */
final ConcurrentMap<String, List<DnsCacheEntry>> resolveCache = PlatformDependent.newConcurrentHashMap(); private final ConcurrentMap<String, List<DnsCacheEntry>> resolveCache = PlatformDependent.newConcurrentHashMap();
private final FastThreadLocal<DnsServerAddressStream> nameServerAddrStream = private final FastThreadLocal<DnsServerAddressStream> nameServerAddrStream =
new FastThreadLocal<DnsServerAddressStream>() { new FastThreadLocal<DnsServerAddressStream>() {
@ -107,68 +107,18 @@ public class DnsNameResolver extends InetNameResolver {
} }
}; };
private final DnsResponseHandler responseHandler = new DnsResponseHandler(); private final long queryTimeoutMillis;
private volatile long queryTimeoutMillis = 5000;
// The default TTL values here respect the TTL returned by the DNS server and do not cache the negative response. // 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 final int minTtl;
private volatile int maxTtl = Integer.MAX_VALUE; private final int maxTtl;
private volatile int negativeTtl; private final int negativeTtl;
private volatile int maxQueriesPerResolve = 3; private final int maxQueriesPerResolve;
private volatile boolean traceEnabled = true; private final boolean traceEnabled;
private final InternetProtocolFamily[] resolvedAddressTypes;
private volatile InternetProtocolFamily[] resolveAddressTypes = DEFAULT_RESOLVE_ADDRESS_TYPES; private final boolean recursionDesired;
private volatile boolean recursionDesired = true; private final int maxPayloadSize;
private final boolean optResourceEnabled;
private volatile int maxPayloadSize; private final HostsFileEntriesResolver hostsFileEntriesResolver;
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<? extends DatagramChannel> 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<? extends DatagramChannel> channelType,
InetSocketAddress localAddress, DnsServerAddresses nameServerAddresses) {
this(eventLoop, new ReflectiveChannelFactory<DatagramChannel>(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<? extends DatagramChannel> channelFactory,
DnsServerAddresses nameServerAddresses) {
this(eventLoop, channelFactory, ANY_LOCAL_ADDR, nameServerAddresses);
}
/** /**
* Creates a new DNS-based name resolver that communicates with the specified list of DNS servers. * 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 * @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 * this to determine which DNS server should be contacted for the next retry in case
* of failure. * 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( public DnsNameResolver(
EventLoop eventLoop, ChannelFactory<? extends DatagramChannel> channelFactory, EventLoop eventLoop,
InetSocketAddress localAddress, DnsServerAddresses nameServerAddresses) { ChannelFactory<? extends DatagramChannel> 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); super(eventLoop);
checkNotNull(channelFactory, "channelFactory"); checkNotNull(channelFactory, "channelFactory");
checkNotNull(nameServerAddresses, "nameServerAddresses");
checkNotNull(localAddress, "localAddress"); 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); bindFuture = newChannel(channelFactory, localAddress);
ch = (DatagramChannel) bindFuture.channel(); ch = (DatagramChannel) bindFuture.channel();
ch.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(maxPayloadSize));
setMaxPayloadSize(4096);
} }
private ChannelFuture newChannel( private ChannelFuture newChannel(
@ -203,6 +189,7 @@ public class DnsNameResolver extends InetNameResolver {
Bootstrap b = new Bootstrap(); Bootstrap b = new Bootstrap();
b.group(executor()); b.group(executor());
b.channelFactory(channelFactory); b.channelFactory(channelFactory);
final DnsResponseHandler responseHandler = new DnsResponseHandler();
b.handler(new ChannelInitializer<DatagramChannel>() { b.handler(new ChannelInitializer<DatagramChannel>() {
@Override @Override
protected void initChannel(DatagramChannel ch) throws Exception { 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). * Returns the minimum TTL of the cached DNS resource records (in seconds).
* *
* @see #maxTtl() * @see #maxTtl()
* @see #setTtl(int, int)
*/ */
public int minTtl() { public int minTtl() {
return minTtl; return minTtl;
@ -235,236 +221,56 @@ public class DnsNameResolver extends InetNameResolver {
* Returns the maximum TTL of the cached DNS resource records (in seconds). * Returns the maximum TTL of the cached DNS resource records (in seconds).
* *
* @see #minTtl() * @see #minTtl()
* @see #setTtl(int, int)
*/ */
public int maxTtl() { public int maxTtl() {
return 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 * 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. * disables the cache for negative results.
*
* @see #setNegativeTtl(int)
*/ */
public int negativeTtl() { public int negativeTtl() {
return 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). * Returns the timeout of each DNS query performed by this resolver (in milliseconds).
* The default value is 5 seconds. * The default value is 5 seconds.
*
* @see #setQueryTimeoutMillis(long)
*/ */
public long queryTimeoutMillis() { public long queryTimeoutMillis() {
return 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)} * Returns the list of the protocol families of the address resolved by {@link #resolve(String)}
* in the order of preference. * in the order of preference.
* The default value depends on the value of the system property {@code "java.net.preferIPv6Addresses"}. * The default value depends on the value of the system property {@code "java.net.preferIPv6Addresses"}.
*
* @see #setResolveAddressTypes(InternetProtocolFamily...)
*/ */
public List<InternetProtocolFamily> resolveAddressTypes() { public List<InternetProtocolFamily> resolvedAddressTypes() {
return Arrays.asList(resolveAddressTypes); return Arrays.asList(resolvedAddressTypes);
} }
InternetProtocolFamily[] resolveAddressTypesUnsafe() { InternetProtocolFamily[] resolveAddressTypesUnsafe() {
return resolveAddressTypes; return resolvedAddressTypes;
}
/**
* 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<InternetProtocolFamily> list =
new ArrayList<InternetProtocolFamily>(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<InternetProtocolFamily> resolveAddressTypes) {
checkNotNull(resolveAddressTypes, "resolveAddressTypes");
final List<InternetProtocolFamily> list =
new ArrayList<InternetProtocolFamily>(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;
} }
/** /**
* Returns {@code true} if and only if this resolver sends a DNS query with the RD (recursion desired) flag set. * 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}. * The default value is {@code true}.
*
* @see #setRecursionDesired(boolean)
*/ */
public boolean isRecursionDesired() { public boolean isRecursionDesired() {
return recursionDesired; 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. * Returns the maximum allowed number of DNS queries to send when resolving a host name.
* The default value is {@code 8}. * The default value is {@code 8}.
*
* @see #setMaxQueriesPerResolve(int)
*/ */
public int maxQueriesPerResolve() { public int maxQueriesPerResolve() {
return 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 * 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}. * 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; 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. * Returns the capacity of the datagram packet buffer (in bytes). The default value is {@code 4096} bytes.
*
* @see #setMaxPayloadSize(int)
*/ */
public int maxPayloadSize() { public int maxPayloadSize() {
return 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 * 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. * much data the resolver can read per response is enabled.
@ -532,6 +294,14 @@ public class DnsNameResolver extends InetNameResolver {
return optResourceEnabled; 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. * Clears all the resolved addresses cached by this resolver.
* *
@ -590,6 +360,10 @@ public class DnsNameResolver extends InetNameResolver {
return (EventLoop) super.executor(); return (EventLoop) super.executor();
} }
private InetAddress resolveHostsFileEntry(String hostname) {
return hostsFileEntriesResolver != null ? hostsFileEntriesResolver.address(hostname) : null;
}
@Override @Override
protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception { protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception {
final byte[] bytes = NetUtil.createByteArrayFromIpAddressString(inetHost); final byte[] bytes = NetUtil.createByteArrayFromIpAddressString(inetHost);
@ -601,6 +375,12 @@ public class DnsNameResolver extends InetNameResolver {
final String hostname = hostname(inetHost); final String hostname = hostname(inetHost);
InetAddress hostsFileEntry = resolveHostsFileEntry(hostname);
if (hostsFileEntry != null) {
promise.setSuccess(hostsFileEntry);
return;
}
if (!doResolveCached(hostname, promise)) { if (!doResolveCached(hostname, promise)) {
doResolveUncached(hostname, promise); doResolveUncached(hostname, promise);
} }
@ -622,7 +402,7 @@ public class DnsNameResolver extends InetNameResolver {
cause = cachedEntries.get(0).cause(); cause = cachedEntries.get(0).cause();
} else { } else {
// Find the first entry with the preferred address type. // Find the first entry with the preferred address type.
for (InternetProtocolFamily f : resolveAddressTypes) { for (InternetProtocolFamily f : resolvedAddressTypes) {
for (int i = 0; i < numEntries; i++) { for (int i = 0; i < numEntries; i++) {
final DnsCacheEntry e = cachedEntries.get(i); final DnsCacheEntry e = cachedEntries.get(i);
if (f.addressType().isInstance(e.address())) { if (f.addressType().isInstance(e.address())) {
@ -687,6 +467,12 @@ public class DnsNameResolver extends InetNameResolver {
final String hostname = hostname(inetHost); final String hostname = hostname(inetHost);
InetAddress hostsFileEntry = resolveHostsFileEntry(hostname);
if (hostsFileEntry != null) {
promise.setSuccess(Collections.singletonList(hostsFileEntry));
return;
}
if (!doResolveAllCached(hostname, promise)) { if (!doResolveAllCached(hostname, promise)) {
doResolveAllUncached(hostname, promise); doResolveAllUncached(hostname, promise);
} }
@ -707,7 +493,7 @@ public class DnsNameResolver extends InetNameResolver {
if (cachedEntries.get(0).cause() != null) { if (cachedEntries.get(0).cause() != null) {
cause = cachedEntries.get(0).cause(); cause = cachedEntries.get(0).cause();
} else { } else {
for (InternetProtocolFamily f : resolveAddressTypes) { for (InternetProtocolFamily f : resolvedAddressTypes) {
for (int i = 0; i < numEntries; i++) { for (int i = 0; i < numEntries; i++) {
final DnsCacheEntry e = cachedEntries.get(i); final DnsCacheEntry e = cachedEntries.get(i);
if (f.addressType().isInstance(e.address())) { if (f.addressType().isInstance(e.address())) {

View File

@ -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<? extends DatagramChannel> 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<? extends DatagramChannel> 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<? extends DatagramChannel> channelType) {
return channelFactory(new ReflectiveChannelFactory<DatagramChannel>(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<InternetProtocolFamily> list =
new ArrayList<InternetProtocolFamily>(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<InternetProtocolFamily> resolvedAddressTypes) {
checkNotNull(resolvedAddressTypes, "resolveAddressTypes");
final List<InternetProtocolFamily> list =
new ArrayList<InternetProtocolFamily>(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);
}
}

View File

@ -31,11 +31,6 @@ public final class DnsNameResolverException extends RuntimeException {
private final InetSocketAddress remoteAddress; private final InetSocketAddress remoteAddress;
private final DnsQuestion question; 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) { public DnsNameResolverException(InetSocketAddress remoteAddress, DnsQuestion question, String message) {
super(message); super(message);
this.remoteAddress = validateRemoteAddress(remoteAddress); this.remoteAddress = validateRemoteAddress(remoteAddress);
@ -49,12 +44,6 @@ public final class DnsNameResolverException extends RuntimeException {
this.question = validateQuestion(question); 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) { private static InetSocketAddress validateRemoteAddress(InetSocketAddress remoteAddress) {
return ObjectUtil.checkNotNull(remoteAddress, "remoteAddress"); return ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
} }
@ -63,6 +52,13 @@ public final class DnsNameResolverException extends RuntimeException {
return ObjectUtil.checkNotNull(question, "question"); 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. * Returns the {@link DnsQuestion} of the DNS query that has failed.
*/ */

View File

@ -58,7 +58,6 @@ import org.apache.mina.filter.codec.ProtocolEncoder;
import org.apache.mina.filter.codec.ProtocolEncoderOutput; import org.apache.mina.filter.codec.ProtocolEncoderOutput;
import org.apache.mina.transport.socket.DatagramAcceptor; import org.apache.mina.transport.socket.DatagramAcceptor;
import org.apache.mina.transport.socket.DatagramSessionConfig; import org.apache.mina.transport.socket.DatagramSessionConfig;
import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
@ -265,15 +264,23 @@ public class DnsNameResolverTest {
private static final TestDnsServer dnsServer = new TestDnsServer(); private static final TestDnsServer dnsServer = new TestDnsServer();
private static final EventLoopGroup group = new NioEventLoopGroup(1); 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 @BeforeClass
public static void init() throws Exception { public static void init() throws Exception {
dnsServer.start(); dnsServer.start();
resolver = new DnsNameResolver(group.next(), NioDatagramChannel.class,
DnsServerAddresses.singleton(dnsServer.localAddress()));
resolver.setMaxQueriesPerResolve(1);
resolver.setOptResourceEnabled(false);
} }
@AfterClass @AfterClass
public static void destroy() { public static void destroy() {
@ -281,37 +288,40 @@ public class DnsNameResolverTest {
group.shutdownGracefully(); group.shutdownGracefully();
} }
@After
public void reset() throws Exception {
resolver.clearCache();
}
@Test @Test
public void testResolveAorAAAA() throws Exception { 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 @Test
public void testResolveAAAAorA() throws Exception { 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 @Test
public void testResolveA() throws Exception { public void testResolveA() throws Exception {
final int oldMinTtl = resolver.minTtl(); DnsNameResolver resolver = newResolver(InternetProtocolFamily.IPv4)
final int oldMaxTtl = resolver.maxTtl(); // Cache for eternity
.ttl(Integer.MAX_VALUE, Integer.MAX_VALUE)
// Cache for eternity. .build();
resolver.setTtl(Integer.MAX_VALUE, Integer.MAX_VALUE);
try { try {
final Map<String, InetAddress> resultA = testResolve0(EXCLUSIONS_RESOLVE_A, InternetProtocolFamily.IPv4); final Map<String, InetAddress> resultA = testResolve0(resolver, EXCLUSIONS_RESOLVE_A);
// Now, try to resolve again to see if it's cached. // 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. // 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. // If cached, the resolved addresses must be always same, because we reuse the same response.
final Map<String, InetAddress> resultB = testResolve0(EXCLUSIONS_RESOLVE_A, InternetProtocolFamily.IPv4); final Map<String, InetAddress> resultB = testResolve0(resolver, EXCLUSIONS_RESOLVE_A);
// Ensure the result from the cache is identical from the uncached one. // Ensure the result from the cache is identical from the uncached one.
assertThat(resultB.size(), is(resultA.size())); assertThat(resultB.size(), is(resultA.size()));
@ -325,61 +335,56 @@ public class DnsNameResolverTest {
assertThat(actual, is(expected)); assertThat(actual, is(expected));
} }
} finally { } finally {
// Restore the TTL configuration. resolver.close();
resolver.setTtl(oldMinTtl, oldMaxTtl);
} }
} }
@Test @Test
public void testResolveAAAA() throws Exception { 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<String, InetAddress> testResolve0( private Map<String, InetAddress> testResolve0(DnsNameResolver resolver, Set<String> excludedDomains)
Set<String> excludedDomains, InternetProtocolFamily... famililies) throws InterruptedException { throws InterruptedException {
final List<InternetProtocolFamily> oldResolveAddressTypes = resolver.resolveAddressTypes();
assertThat(resolver.isRecursionDesired(), is(true)); assertThat(resolver.isRecursionDesired(), is(true));
assertThat(oldResolveAddressTypes.size(), is(InternetProtocolFamily.values().length));
resolver.setResolveAddressTypes(famililies);
final Map<String, InetAddress> results = new HashMap<String, InetAddress>(); final Map<String, InetAddress> results = new HashMap<String, InetAddress>();
try { final Map<String, Future<InetAddress>> futures =
final Map<String, Future<InetAddress>> futures = new LinkedHashMap<String, Future<InetAddress>>();
new LinkedHashMap<String, Future<InetAddress>>();
for (String name : DOMAINS) { for (String name : DOMAINS) {
if (excludedDomains.contains(name)) { if (excludedDomains.contains(name)) {
continue; continue;
}
resolve(futures, name);
} }
for (Entry<String, Future<InetAddress>> e : futures.entrySet()) { resolve(resolver, futures, name);
String unresolved = e.getKey(); }
InetAddress resolved = e.getValue().sync().getNow();
logger.info("{}: {}", unresolved, resolved.getHostAddress()); for (Entry<String, Future<InetAddress>> 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; assertThat(resolved.getHostName(), is(unresolved));
for (InternetProtocolFamily f: famililies) {
Class<?> resolvedType = resolved.getClass(); boolean typeMatches = false;
if (f.addressType().isAssignableFrom(resolvedType)) { for (InternetProtocolFamily f: resolver.resolvedAddressTypes()) {
typeMatches = true; 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; return results;
@ -387,61 +392,65 @@ public class DnsNameResolverTest {
@Test @Test
public void testQueryMx() throws Exception { public void testQueryMx() throws Exception {
assertThat(resolver.isRecursionDesired(), is(true)); DnsNameResolver resolver = newResolver().build();
try {
assertThat(resolver.isRecursionDesired(), is(true));
Map<String, Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> futures = Map<String, Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> futures =
new LinkedHashMap<String, Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>>(); new LinkedHashMap<String, Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>>();
for (String name: DOMAINS) { for (String name: DOMAINS) {
if (EXCLUSIONS_QUERY_MX.contains(name)) { if (EXCLUSIONS_QUERY_MX.contains(name)) {
continue; continue;
}
queryMx(futures, name);
}
for (Entry<String, Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> e: futures.entrySet()) {
String hostname = e.getKey();
Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> f = e.getValue().awaitUninterruptibly();
DnsResponse response = f.getNow().content();
assertThat(response.code(), is(DnsResponseCode.NOERROR));
final int answerCount = response.count(DnsSection.ANSWER);
final List<DnsRecord> mxList = new ArrayList<DnsRecord>(answerCount);
for (int i = 0; i < answerCount; i ++) {
final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
if (r.type() == DnsRecordType.MX) {
mxList.add(r);
} }
queryMx(resolver, futures, name);
} }
assertThat(mxList.size(), is(greaterThan(0))); for (Entry<String, Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> e: futures.entrySet()) {
StringBuilder buf = new StringBuilder(); String hostname = e.getKey();
for (DnsRecord r: mxList) { Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> f = e.getValue().awaitUninterruptibly();
ByteBuf recordContent = ((ByteBufHolder) r).content();
buf.append(StringUtil.NEWLINE); DnsResponse response = f.getNow().content();
buf.append('\t'); assertThat(response.code(), is(DnsResponseCode.NOERROR));
buf.append(r.name());
buf.append(' '); final int answerCount = response.count(DnsSection.ANSWER);
buf.append(r.type().name()); final List<DnsRecord> mxList = new ArrayList<DnsRecord>(answerCount);
buf.append(' '); for (int i = 0; i < answerCount; i ++) {
buf.append(recordContent.readUnsignedShort()); final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
buf.append(' '); if (r.type() == DnsRecordType.MX) {
buf.append(DnsNameResolverContext.decodeDomainName(recordContent)); 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();
} }
} finally {
logger.info("{} has the following MX records:{}", hostname, buf); resolver.close();
response.release();
} }
} }
@Test @Test
public void testNegativeTtl() throws Exception { public void testNegativeTtl() throws Exception {
final int oldNegativeTtl = resolver.negativeTtl(); final DnsNameResolver resolver = newResolver().negativeTtl(10).build();
resolver.setNegativeTtl(10);
try { try {
resolveNonExistentDomain(); resolveNonExistentDomain(resolver);
final int size = 10000; final int size = 10000;
final List<UnknownHostException> exceptions = new ArrayList<UnknownHostException>(); final List<UnknownHostException> exceptions = new ArrayList<UnknownHostException>();
@ -451,7 +460,7 @@ public class DnsNameResolverTest {
@Override @Override
public void run() { public void run() {
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
exceptions.add(resolveNonExistentDomain()); exceptions.add(resolveNonExistentDomain(resolver));
if (isInterrupted()) { if (isInterrupted()) {
break; break;
} }
@ -469,11 +478,11 @@ public class DnsNameResolverTest {
assertThat(exceptions, hasSize(size)); assertThat(exceptions, hasSize(size));
} finally { } finally {
resolver.setNegativeTtl(oldNegativeTtl); resolver.close();
} }
} }
private static UnknownHostException resolveNonExistentDomain() { private UnknownHostException resolveNonExistentDomain(DnsNameResolver resolver) {
try { try {
resolver.resolve("non-existent.netty.io").sync(); resolver.resolve("non-existent.netty.io").sync();
fail(); fail();
@ -486,17 +495,23 @@ public class DnsNameResolverTest {
@Test @Test
public void testResolveIp() { 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<String, Future<InetAddress>> futures, String hostname) { private void resolve(DnsNameResolver resolver, Map<String, Future<InetAddress>> futures, String hostname) {
futures.put(hostname, resolver.resolve(hostname)); futures.put(hostname, resolver.resolve(hostname));
} }
private static void queryMx( private static void queryMx(
DnsNameResolver resolver,
Map<String, Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> futures, Map<String, Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> futures,
String hostname) throws Exception { String hostname) throws Exception {
futures.put(hostname, resolver.query(new DefaultDnsQuestion(hostname, DnsRecordType.MX))); futures.put(hostname, resolver.query(new DefaultDnsQuestion(hostname, DnsRecordType.MX)));

View File

@ -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<T> extends SimpleNameResolver<T> {
private final NameResolver<T>[] 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<T>... 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<T> promise) throws Exception {
doResolveRec(inetHost, promise, 0, null);
}
private void doResolveRec(final String inetHost,
final Promise<T> 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<T>() {
@Override
public void operationComplete(Future<T> 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<List<T>> promise) throws Exception {
doResolveAllRec(inetHost, promise, 0, null);
}
private void doResolveAllRec(final String inetHost,
final Promise<List<T>> 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<List<T>>() {
@Override
public void operationComplete(Future<List<T>> future) throws Exception {
if (future.isSuccess()) {
promise.setSuccess(future.getNow());
} else {
doResolveAllRec(inetHost, promise, resolverIndex + 1, future.cause());
}
}
});
}
}
}

View File

@ -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<String, InetAddress> entries = HostsFileParser.parseSilently();
@Override
public InetAddress address(String inetHost) {
return entries.get(inetHost);
}
}

View File

@ -20,7 +20,6 @@ import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Promise; import io.netty.util.concurrent.Promise;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;

View File

@ -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);
}

View File

@ -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<String, InetAddress> 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<String, InetAddress> 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<String, InetAddress> 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<String, InetAddress> parse(Reader reader) throws IOException {
checkNotNull(reader, "reader");
BufferedReader buff = new BufferedReader(reader);
try {
Map<String, InetAddress> entries = new HashMap<String, InetAddress>();
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<String> lineParts = new ArrayList<String>();
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() {
}
}

View File

@ -37,7 +37,8 @@ public abstract class InetNameResolver extends SimpleNameResolver<InetAddress> {
} }
/** /**
* 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<InetSocketAddress> asAddressResolver() { public AddressResolver<InetSocketAddress> asAddressResolver() {
AddressResolver<InetSocketAddress> result = addressResolver; AddressResolver<InetSocketAddress> result = addressResolver;

View File

@ -17,7 +17,7 @@ package io.netty.resolver;
import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future; 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 io.netty.util.concurrent.Promise;
import java.net.InetAddress; import java.net.InetAddress;
@ -53,7 +53,7 @@ public class InetSocketAddressResolver extends AbstractAddressResolver<InetSocke
// Note that InetSocketAddress.getHostName() will never incur a reverse lookup here, // Note that InetSocketAddress.getHostName() will never incur a reverse lookup here,
// because an unresolved address always has a host name. // because an unresolved address always has a host name.
nameResolver.resolve(unresolvedAddress.getHostName()) nameResolver.resolve(unresolvedAddress.getHostName())
.addListener(new GenericFutureListener<Future<InetAddress>>() { .addListener(new FutureListener<InetAddress>() {
@Override @Override
public void operationComplete(Future<InetAddress> future) throws Exception { public void operationComplete(Future<InetAddress> future) throws Exception {
if (future.isSuccess()) { if (future.isSuccess()) {
@ -71,7 +71,7 @@ public class InetSocketAddressResolver extends AbstractAddressResolver<InetSocke
// Note that InetSocketAddress.getHostName() will never incur a reverse lookup here, // Note that InetSocketAddress.getHostName() will never incur a reverse lookup here,
// because an unresolved address always has a host name. // because an unresolved address always has a host name.
nameResolver.resolveAll(unresolvedAddress.getHostName()) nameResolver.resolveAll(unresolvedAddress.getHostName())
.addListener(new GenericFutureListener<Future<List<InetAddress>>>() { .addListener(new FutureListener<List<InetAddress>>() {
@Override @Override
public void operationComplete(Future<List<InetAddress>> future) throws Exception { public void operationComplete(Future<List<InetAddress>> future) throws Exception {
if (future.isSuccess()) { if (future.isSuccess()) {

View File

@ -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<String, InetAddress> 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());
}
}