diff --git a/all/pom.xml b/all/pom.xml index 40861eb070..2fa83cdc45 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -189,6 +189,20 @@ compile true + + ${project.groupId} + netty-resolver + ${project.version} + compile + true + + + ${project.groupId} + netty-resolver-dns + ${project.version} + compile + true + ${project.groupId} netty-transport diff --git a/pom.xml b/pom.xml index 7407184a4b..7dbc088704 100644 --- a/pom.xml +++ b/pom.xml @@ -341,6 +341,8 @@ codec-mqtt codec-socks codec-stomp + resolver + resolver-dns transport transport-rxtx transport-sctp diff --git a/resolver-dns/pom.xml b/resolver-dns/pom.xml new file mode 100644 index 0000000000..486a9260f7 --- /dev/null +++ b/resolver-dns/pom.xml @@ -0,0 +1,49 @@ + + + + + 4.0.0 + + io.netty + netty-parent + 4.1.0.Beta4-SNAPSHOT + + + netty-resolver-dns + jar + + Netty/Resolver/DNS + + + + ${project.groupId} + netty-resolver + ${project.version} + + + ${project.groupId} + netty-codec-dns + ${project.version} + + + ${project.groupId} + netty-transport + ${project.version} + + + + 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 new file mode 100644 index 0000000000..42902164cf --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java @@ -0,0 +1,864 @@ +/* + * Copyright 2014 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.bootstrap.Bootstrap; +import io.netty.channel.ChannelFactory; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +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.DnsClass; +import io.netty.handler.codec.dns.DnsQueryEncoder; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsResource; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.handler.codec.dns.DnsResponseCode; +import io.netty.handler.codec.dns.DnsResponseDecoder; +import io.netty.resolver.NameResolver; +import io.netty.resolver.SimpleNameResolver; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; +import io.netty.util.concurrent.ScheduledFuture; +import io.netty.util.internal.OneTimeTask; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SystemPropertyUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.IDN; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReferenceArray; + +/** + * A DNS-based {@link NameResolver}. + */ +public class DnsNameResolver extends SimpleNameResolver { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsNameResolver.class); + + static final InetSocketAddress ANY_LOCAL_ADDR = new InetSocketAddress(0); + + private 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. + if ("true".equalsIgnoreCase(SystemPropertyUtil.get("java.net.preferIPv6Addresses"))) { + DEFAULT_RESOLVE_ADDRESS_TYPES[0] = InternetProtocolFamily.IPv6; + DEFAULT_RESOLVE_ADDRESS_TYPES[1] = InternetProtocolFamily.IPv4; + logger.debug("-Djava.net.preferIPv6Addresses: true"); + } else { + DEFAULT_RESOLVE_ADDRESS_TYPES[0] = InternetProtocolFamily.IPv4; + DEFAULT_RESOLVE_ADDRESS_TYPES[1] = InternetProtocolFamily.IPv6; + logger.debug("-Djava.net.preferIPv6Addresses: false"); + } + } + + private static final DnsResponseDecoder DECODER = new DnsResponseDecoder(); + private static final DnsQueryEncoder ENCODER = new DnsQueryEncoder(); + + final Iterable nameServerAddresses; + final DatagramChannel ch; + + /** + * An array whose index is the ID of a DNS query and whose value is the promise of the corresponsing response. We + * don't use {@link IntObjectHashMap} or map-like data structure here because 64k elements are fairly small, which + * is only about 512KB. + */ + final AtomicReferenceArray promises = new AtomicReferenceArray(65536); + + /** + * The cache for {@link #query(DnsQuestion)} + */ + final ConcurrentMap queryCache = PlatformDependent.newConcurrentHashMap(); + + private final DnsResponseHandler responseHandler = new DnsResponseHandler(); + + 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. + private volatile int minTtl; + private volatile int maxTtl = Integer.MAX_VALUE; + private volatile int negativeTtl; + private volatile int maxTriesPerQuery = 2; + + private volatile InternetProtocolFamily[] resolveAddressTypes = DEFAULT_RESOLVE_ADDRESS_TYPES; + private volatile boolean recursionDesired = true; + private volatile int maxQueriesPerResolve = 8; + + private volatile int maxPayloadSize; + private volatile DnsClass maxPayloadSizeClass; // EDNS uses the CLASS field as the payload size field. + + /** + * Creates a new DNS-based name resolver that communicates with a single DNS server. + * + * @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 nameServerAddress the address of the DNS server + */ + public DnsNameResolver( + EventLoop eventLoop, Class channelType, + InetSocketAddress nameServerAddress) { + this(eventLoop, channelType, ANY_LOCAL_ADDR, nameServerAddress); + } + + /** + * Creates a new DNS-based name resolver that communicates with a single DNS server. + * + * @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 nameServerAddress the address of the DNS server + */ + public DnsNameResolver( + EventLoop eventLoop, Class channelType, + InetSocketAddress localAddress, InetSocketAddress nameServerAddress) { + this(eventLoop, new ReflectiveChannelFactory(channelType), localAddress, nameServerAddress); + } + + /** + * Creates a new DNS-based name resolver that communicates with a single DNS server. + * + * @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 nameServerAddress the address of the DNS server + */ + public DnsNameResolver( + EventLoop eventLoop, ChannelFactory channelFactory, + InetSocketAddress nameServerAddress) { + this(eventLoop, channelFactory, ANY_LOCAL_ADDR, nameServerAddress); + } + + /** + * Creates a new DNS-based name resolver that communicates with a single DNS server. + * + * @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 localAddress the local address of the {@link DatagramChannel} + * @param nameServerAddress the address of the DNS server + */ + public DnsNameResolver( + EventLoop eventLoop, ChannelFactory channelFactory, + InetSocketAddress localAddress, InetSocketAddress nameServerAddress) { + this(eventLoop, channelFactory, localAddress, DnsServerAddresses.singleton(nameServerAddress)); + } + + /** + * 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 {@link Iterator} is + * created from this {@link Iterable} to determine which DNS server should be contacted + * for the next retry in case of failure. + */ + public DnsNameResolver( + EventLoop eventLoop, Class channelType, + Iterable 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 {@link Iterator} is + * created from this {@link Iterable} to determine which DNS server should be contacted + * for the next retry in case of failure. + */ + public DnsNameResolver( + EventLoop eventLoop, Class channelType, + InetSocketAddress localAddress, Iterable 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 {@link Iterator} is + * created from this {@link Iterable} to determine which DNS server should be contacted + * for the next retry in case of failure. + */ + public DnsNameResolver( + EventLoop eventLoop, ChannelFactory channelFactory, + Iterable nameServerAddresses) { + this(eventLoop, channelFactory, 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 channelFactory the {@link ChannelFactory} that will create a {@link DatagramChannel} + * @param localAddress the local address of the {@link DatagramChannel} + * @param nameServerAddresses the addresses of the DNS server. For each DNS query, a new {@link Iterator} is + * created from this {@link Iterable} to determine which DNS server should be contacted + * for the next retry in case of failure. + */ + public DnsNameResolver( + EventLoop eventLoop, ChannelFactory channelFactory, + InetSocketAddress localAddress, Iterable nameServerAddresses) { + + super(eventLoop); + + if (channelFactory == null) { + throw new NullPointerException("channelFactory"); + } + if (nameServerAddresses == null) { + throw new NullPointerException("nameServerAddresses"); + } + if (!nameServerAddresses.iterator().hasNext()) { + throw new NullPointerException("nameServerAddresses is empty"); + } + if (localAddress == null) { + throw new NullPointerException("localAddress"); + } + + this.nameServerAddresses = nameServerAddresses; + ch = newChannel(channelFactory, localAddress); + + setMaxPayloadSize(4096); + } + + private DatagramChannel newChannel( + ChannelFactory channelFactory, InetSocketAddress localAddress) { + + Bootstrap b = new Bootstrap(); + b.group(executor()); + b.channelFactory(channelFactory); + b.handler(new ChannelInitializer() { + @Override + protected void initChannel(DatagramChannel ch) throws Exception { + ch.pipeline().addLast(DECODER, ENCODER, responseHandler); + } + }); + + DatagramChannel ch = (DatagramChannel) b.bind(localAddress).channel(); + ch.closeFuture().addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + clearCache(); + } + }); + + return ch; + } + + /** + * Returns the minimum TTL of the cached DNS resource records (in seconds). + * + * @see #maxTtl() + * @see #setTtl(int, int) + */ + public int minTtl() { + return minTtl; + } + + /** + * 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 maximum number of tries for each query. The default value is 2 times. + * + * @see #setMaxTriesPerQuery(int) + */ + public int maxTriesPerQuery() { + return maxTriesPerQuery; + } + + /** + * Sets the maximum number of tries for each query. + * + * @return {@code this} + * + * @see #maxTriesPerQuery() + */ + public DnsNameResolver setMaxTriesPerQuery(int maxTriesPerQuery) { + if (maxTriesPerQuery < 1) { + throw new IllegalArgumentException("maxTries: " + maxTriesPerQuery + " (expected: > 0)"); + } + + this.maxTriesPerQuery = maxTriesPerQuery; + + return this; + } + + /** + * Returns the list of the protocol families of the address resolved by {@link #resolve(SocketAddress)} + * 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); + } + + InternetProtocolFamily[] resolveAddressTypesUnsafe() { + return resolveAddressTypes; + } + + /** + * Sets the list of the protocol families of the address resolved by {@link #resolve(SocketAddress)}. + * 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) { + if (resolveAddressTypes == null) { + throw new NullPointerException("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(SocketAddress)}. + * 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) { + if (resolveAddressTypes == null) { + throw new NullPointerException("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; + } + + /** + * 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 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; + maxPayloadSizeClass = DnsClass.valueOf(maxPayloadSize); + ch.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(maxPayloadSize)); + + return this; + } + + DnsClass maxPayloadSizeClass() { + return maxPayloadSizeClass; + } + + /** + * Clears all the DNS resource records cached by this resolver. + * + * @return {@code this} + * + * @see #clearCache(DnsQuestion) + */ + public DnsNameResolver clearCache() { + for (Iterator> i = queryCache.entrySet().iterator(); i.hasNext();) { + Entry e = i.next(); + i.remove(); + e.getValue().release(); + } + + return this; + } + + /** + * Clears the DNS resource record of the specified DNS question from the cache of this resolver. + */ + public boolean clearCache(DnsQuestion question) { + DnsCacheEntry e = queryCache.remove(question); + if (e != null) { + e.release(); + return true; + } else { + return false; + } + } + + /** + * Closes the internal datagram channel used for sending and receiving DNS messages, and clears all DNS resource + * records from the cache. Attempting to send a DNS query or to resolve a domain name will fail once this method + * has been called. + */ + @Override + public void close() { + ch.close(); + } + + @Override + protected EventLoop executor() { + return (EventLoop) super.executor(); + } + + @Override + protected boolean doIsResolved(InetSocketAddress address) { + return !address.isUnresolved(); + } + + @Override + protected void doResolve(InetSocketAddress unresolvedAddress, Promise promise) throws Exception { + final String hostname = IDN.toASCII(unresolvedAddress.getHostString()); + final int port = unresolvedAddress.getPort(); + + final DnsNameResolverContext ctx = new DnsNameResolverContext(this, hostname, port, promise); + + ctx.resolve(); + } + + /** + * Sends a DNS query with the specified question. + */ + public Future query(DnsQuestion question) { + return query(nameServerAddresses, question); + } + + /** + * Sends a DNS query with the specified question. + */ + public Future query(DnsQuestion question, Promise promise) { + return query(nameServerAddresses, question, promise); + } + + /** + * Sends a DNS query with the specified question using the specified name server list. + */ + public Future query(Iterable nameServerAddresses, DnsQuestion question) { + if (nameServerAddresses == null) { + throw new NullPointerException("nameServerAddresses"); + } + if (question == null) { + throw new NullPointerException("question"); + } + + final EventLoop eventLoop = ch.eventLoop(); + final DnsCacheEntry cachedResult = queryCache.get(question); + if (cachedResult != null) { + if (cachedResult.response != null) { + return eventLoop.newSucceededFuture(cachedResult.response.retain()); + } else { + return eventLoop.newFailedFuture(cachedResult.cause); + } + } else { + return query0(nameServerAddresses, question, eventLoop.newPromise()); + } + } + + /** + * Sends a DNS query with the specified question using the specified name server list. + */ + public Future query( + Iterable nameServerAddresses, DnsQuestion question, Promise promise) { + + if (nameServerAddresses == null) { + throw new NullPointerException("nameServerAddresses"); + } + if (question == null) { + throw new NullPointerException("question"); + } + if (promise == null) { + throw new NullPointerException("promise"); + } + + final DnsCacheEntry cachedResult = queryCache.get(question); + if (cachedResult != null) { + if (cachedResult.response != null) { + return promise.setSuccess(cachedResult.response.retain()); + } else { + return promise.setFailure(cachedResult.cause); + } + } else { + return query0(nameServerAddresses, question, promise); + } + } + + private Future query0( + Iterable nameServerAddresses, DnsQuestion question, Promise promise) { + + try { + new DnsQueryContext(this, nameServerAddresses, question, promise).query(); + return promise; + } catch (Exception e) { + return promise.setFailure(e); + } + } + + void cache(final DnsQuestion question, DnsCacheEntry entry, long delaySeconds) { + queryCache.put(question, entry); + boolean scheduled = false; + try { + entry.expirationFuture = ch.eventLoop().schedule(new OneTimeTask() { + @Override + public void run() { + Object response = queryCache.remove(question); + ReferenceCountUtil.safeRelease(response); + } + }, delaySeconds, TimeUnit.SECONDS); + + scheduled = true; + } finally { + if (!scheduled) { + // If failed to schedule the expiration task, + // remove the entry from the cache so that it does not leak. + queryCache.remove(question); + entry.release(); + } + } + } + + private final class DnsResponseHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + boolean success = false; + try { + final DnsResponse res = (DnsResponse) msg; + final int queryId = res.header().id(); + + if (logger.isDebugEnabled()) { + logger.debug("{} RECEIVED: [{}: {}], {}", ch, queryId, res.sender(), res); + } + + final DnsQueryContext qCtx = promises.get(queryId); + + if (qCtx == null) { + if (logger.isWarnEnabled()) { + logger.warn("Received a DNS response with an unknown ID: {}", queryId); + } + return; + } + + final List questions = res.questions(); + if (questions.size() != 1) { + logger.warn("Received a DNS response with invalid number of questions: {}", res); + return; + } + + final DnsQuestion q = qCtx.question(); + if (!q.equals(questions.get(0))) { + logger.warn("Received a mismatching DNS response: {}", res); + return; + } + + // Cancel the timeout task. + final ScheduledFuture timeoutFuture = qCtx.timeoutFuture(); + if (timeoutFuture != null) { + timeoutFuture.cancel(false); + } + + if (res.header().responseCode() == DnsResponseCode.NOERROR) { + cache(q, res); + promises.set(queryId, null); + qCtx.promise().trySuccess(res); + success = true; + } else { + qCtx.retry(res.sender(), + "response code: " + res.header().responseCode() + + " with " + res.answers().size() + " answer(s) and " + + res.authorityResources().size() + " authority resource(s)"); + } + } finally { + if (!success) { + ReferenceCountUtil.safeRelease(msg); + } + } + } + + private void cache(DnsQuestion question, DnsResponse res) { + final int maxTtl = maxTtl(); + if (maxTtl == 0) { + return; + } + + long ttl = Long.MAX_VALUE; + // Find the smallest TTL value returned by the server. + for (DnsResource r: res.answers()) { + long rTtl = r.timeToLive(); + if (ttl > rTtl) { + ttl = rTtl; + } + } + + // Ensure that the found TTL is between minTtl and maxTtl. + ttl = Math.max(minTtl(), Math.min(maxTtl, ttl)); + + res.retain(); + + DnsNameResolver.this.cache(question, new DnsCacheEntry(res), ttl); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + logger.warn("Unexpected exception: ", cause); + } + } + + static final class DnsCacheEntry { + final DnsResponse response; + final Throwable cause; + volatile ScheduledFuture expirationFuture; + + DnsCacheEntry(DnsResponse response) { + this.response = response; + cause = null; + } + + DnsCacheEntry(Throwable cause) { + this.cause = cause; + response = null; + } + + void release() { + DnsResponse response = this.response; + if (response != null) { + ReferenceCountUtil.safeRelease(response); + } + + ScheduledFuture expirationFuture = this.expirationFuture; + if (expirationFuture != null) { + expirationFuture.cancel(false); + } + } + } +} diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java new file mode 100644 index 0000000000..538e2c9a3b --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java @@ -0,0 +1,477 @@ +/* + * Copyright 2014 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.buffer.ByteBuf; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.handler.codec.dns.DnsClass; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsResource; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.handler.codec.dns.DnsResponseDecoder; +import io.netty.handler.codec.dns.DnsType; +import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.StringUtil; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +final class DnsNameResolverContext { + + private static final int INADDRSZ4 = 4; + private static final int INADDRSZ6 = 16; + + private static final FutureListener RELEASE_RESPONSE = new FutureListener() { + @Override + public void operationComplete(Future future) { + if (future.isSuccess()) { + future.getNow().release(); + } + } + }; + + private final DnsNameResolver parent; + private final Promise promise; + private final String hostname; + private final int port; + private final int maxAllowedQueries; + private final InternetProtocolFamily[] resolveAddressTypes; + + private final Set> queriesInProgress = + Collections.newSetFromMap(new IdentityHashMap, Boolean>()); + private List resolvedAddresses; + private StringBuilder trace; + private int allowedQueries; + private boolean triedCNAME; + + DnsNameResolverContext(DnsNameResolver parent, String hostname, int port, Promise promise) { + this.parent = parent; + this.promise = promise; + this.hostname = hostname; + this.port = port; + + maxAllowedQueries = parent.maxQueriesPerResolve(); + resolveAddressTypes = parent.resolveAddressTypesUnsafe(); + allowedQueries = maxAllowedQueries; + } + + void resolve() { + for (InternetProtocolFamily f: resolveAddressTypes) { + final DnsType type; + switch (f) { + case IPv4: + type = DnsType.A; + break; + case IPv6: + type = DnsType.AAAA; + break; + default: + throw new Error(); + } + + query(parent.nameServerAddresses, new DnsQuestion(hostname, type)); + } + } + + private void query(Iterable nameServerAddresses, final DnsQuestion question) { + if (allowedQueries == 0 || promise.isCancelled()) { + return; + } + + allowedQueries --; + + final Future f = parent.query(nameServerAddresses, question); + queriesInProgress.add(f); + + f.addListener(new FutureListener() { + @Override + public void operationComplete(Future future) throws Exception { + queriesInProgress.remove(future); + + if (promise.isDone()) { + return; + } + + try { + if (future.isSuccess()) { + onResponse(question, future.getNow()); + } else { + addTrace(future.cause()); + } + } finally { + tryToFinishResolve(); + } + } + }); + } + + void onResponse(final DnsQuestion question, final DnsResponse response) { + final DnsType type = question.type(); + try { + if (type == DnsType.A || type == DnsType.AAAA) { + onResponseAorAAAA(type, question, response); + } else if (type == DnsType.CNAME) { + onResponseCNAME(question, response); + } + } finally { + ReferenceCountUtil.safeRelease(response); + } + } + + private void onResponseAorAAAA(DnsType qType, DnsQuestion question, DnsResponse response) { + // We often get a bunch of CNAMES as well when we asked for A/AAAA. + final Map cnames = buildAliasMap(response); + + boolean found = false; + for (DnsResource r: response.answers()) { + final DnsType type = r.type(); + if (type != DnsType.A && type != DnsType.AAAA) { + continue; + } + + final String qName = question.name().toLowerCase(Locale.US); + final String rName = r.name().toLowerCase(Locale.US); + + // Make sure the record is for the questioned domain. + if (!rName.equals(qName)) { + // Even if the record's name is not exactly same, it might be an alias defined in the CNAME records. + String resolved = qName; + do { + resolved = cnames.get(resolved); + if (rName.equals(resolved)) { + break; + } + } while (resolved != null); + + if (resolved == null) { + continue; + } + } + + final ByteBuf content = r.content(); + final int contentLen = content.readableBytes(); + if (contentLen != INADDRSZ4 && contentLen != INADDRSZ6) { + continue; + } + + final byte[] addrBytes = new byte[contentLen]; + content.getBytes(content.readerIndex(), addrBytes); + + try { + InetAddress resolved = InetAddress.getByAddress(hostname, addrBytes); + if (resolvedAddresses == null) { + resolvedAddresses = new ArrayList(); + } + resolvedAddresses.add(resolved); + found = true; + } catch (UnknownHostException e) { + // Should never reach here. + throw new Error(e); + } + } + + if (found) { + return; + } + + addTrace(response.sender(), "no matching " + qType + " record found"); + + // We aked for A/AAAA but we got only CNAME. + if (!cnames.isEmpty()) { + onResponseCNAME(question, response, cnames, false); + } + } + + private void onResponseCNAME(DnsQuestion question, DnsResponse response) { + onResponseCNAME(question, response, buildAliasMap(response), true); + } + + private void onResponseCNAME( + DnsQuestion question, DnsResponse response, Map cnames, boolean trace) { + + // Resolve the host name in the question into the real host name. + final String name = question.name().toLowerCase(Locale.US); + String resolved = name; + boolean found = false; + for (;;) { + String next = cnames.get(resolved); + if (next != null) { + found = true; + resolved = next; + } else { + break; + } + } + + if (found) { + followCname(response.sender(), name, resolved); + } else if (trace) { + addTrace(response.sender(), "no matching CNAME record found"); + } + } + + private static Map buildAliasMap(DnsResponse response) { + Map cnames = null; + for (DnsResource r: response.answers()) { + final DnsType type = r.type(); + if (type != DnsType.CNAME) { + continue; + } + + String content = decodeDomainName(r.content()); + if (content == null) { + continue; + } + + if (cnames == null) { + cnames = new HashMap(); + } + + cnames.put(r.name().toLowerCase(Locale.US), content.toLowerCase(Locale.US)); + } + + return cnames != null? cnames : Collections.emptyMap(); + } + + void tryToFinishResolve() { + if (!queriesInProgress.isEmpty()) { + // There are still some queries we did not receive responses for. + if (gotPreferredAddress()) { + // But it's OK to finish the resolution process if we got a resolved address of the preferred type. + finishResolve(); + } + + // We did not get any resolved address of the preferred type, so we can't finish the resolution process. + return; + } + + // There are no queries left to try. + if (resolvedAddresses == null) { + // .. and we could not find any A/AAAA records. + if (!triedCNAME) { + // As the last resort, try to query CNAME, just in case the name server has it. + triedCNAME = true; + query(parent.nameServerAddresses, new DnsQuestion(hostname, DnsType.CNAME, DnsClass.IN)); + return; + } + } + + // We have at least one resolved address or tried CNAME as the last resort.. + finishResolve(); + } + + private boolean gotPreferredAddress() { + if (resolvedAddresses == null) { + return false; + } + + final int size = resolvedAddresses.size(); + switch (resolveAddressTypes[0]) { + case IPv4: + for (int i = 0; i < size; i ++) { + if (resolvedAddresses.get(i) instanceof Inet4Address) { + return true; + } + } + break; + case IPv6: + for (int i = 0; i < size; i ++) { + if (resolvedAddresses.get(i) instanceof Inet6Address) { + return true; + } + } + break; + } + + return false; + } + + private void finishResolve() { + if (!queriesInProgress.isEmpty()) { + // If there are queries in progress, we should cancel it because we already finished the resolution. + for (Iterator> i = queriesInProgress.iterator(); i.hasNext();) { + Future f = i.next(); + i.remove(); + + if (!f.cancel(false)) { + f.addListener(RELEASE_RESPONSE); + } + } + } + + if (resolvedAddresses != null) { + // Found at least one resolved address. + for (InternetProtocolFamily f: resolveAddressTypes) { + switch (f) { + case IPv4: + if (finishResolveWithIPv4()) { + return; + } + break; + case IPv6: + if (finishResolveWithIPv6()) { + return; + } + break; + } + } + } + + // No resolved address found. + int tries = maxAllowedQueries - allowedQueries; + UnknownHostException cause; + if (tries > 1) { + cause = new UnknownHostException( + "failed to resolve " + hostname + " after " + tries + " queries:" + + trace); + } else { + cause = new UnknownHostException("failed to resolve " + hostname + ':' + trace); + } + + promise.tryFailure(cause); + } + + private boolean finishResolveWithIPv4() { + final List resolvedAddresses = this.resolvedAddresses; + final int size = resolvedAddresses.size(); + + for (int i = 0; i < size; i ++) { + InetAddress a = resolvedAddresses.get(i); + if (a instanceof Inet4Address) { + promise.trySuccess(new InetSocketAddress(a, port)); + return true; + } + } + + return false; + } + + private boolean finishResolveWithIPv6() { + final List resolvedAddresses = this.resolvedAddresses; + final int size = resolvedAddresses.size(); + + for (int i = 0; i < size; i ++) { + InetAddress a = resolvedAddresses.get(i); + if (a instanceof Inet6Address) { + promise.trySuccess(new InetSocketAddress(a, port)); + return true; + } + } + + return false; + } + + /** + * Adapted from {@link DnsResponseDecoder#readName(ByteBuf)}. + */ + static String decodeDomainName(ByteBuf buf) { + buf.markReaderIndex(); + try { + int position = -1; + int checked = 0; + int length = buf.writerIndex(); + StringBuilder name = new StringBuilder(64); + for (int len = buf.readUnsignedByte(); buf.isReadable() && len != 0; len = buf.readUnsignedByte()) { + boolean pointer = (len & 0xc0) == 0xc0; + if (pointer) { + if (position == -1) { + position = buf.readerIndex() + 1; + } + buf.readerIndex((len & 0x3f) << 8 | buf.readUnsignedByte()); + // check for loops + checked += 2; + if (checked >= length) { + // Name contains a loop; give up. + return null; + } + } else { + name.append(buf.toString(buf.readerIndex(), len, CharsetUtil.UTF_8)).append('.'); + buf.skipBytes(len); + } + } + + if (position != -1) { + buf.readerIndex(position); + } + + if (name.length() == 0) { + return null; + } + + return name.substring(0, name.length() - 1); + } finally { + buf.resetReaderIndex(); + } + } + + private void followCname( + InetSocketAddress nameServerAddr, String name, String cname) { + + if (trace == null) { + trace = new StringBuilder(128); + } + + trace.append(StringUtil.NEWLINE); + trace.append("\tfrom "); + trace.append(nameServerAddr); + trace.append(": "); + trace.append(name); + trace.append(" CNAME "); + trace.append(cname); + + query(parent.nameServerAddresses, new DnsQuestion(cname, DnsType.A, DnsClass.IN)); + query(parent.nameServerAddresses, new DnsQuestion(cname, DnsType.AAAA, DnsClass.IN)); + } + + private void addTrace(InetSocketAddress nameServerAddr, String msg) { + if (trace == null) { + trace = new StringBuilder(128); + } + + trace.append(StringUtil.NEWLINE); + trace.append("\tfrom "); + trace.append(nameServerAddr); + trace.append(": "); + trace.append(msg); + } + + private void addTrace(Throwable cause) { + if (trace == null) { + trace = new StringBuilder(128); + } + + trace.append(StringUtil.NEWLINE); + trace.append("Caused by: "); + trace.append(cause); + } +} diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverGroup.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverGroup.java new file mode 100644 index 0000000000..fcbcaeaed2 --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverGroup.java @@ -0,0 +1,101 @@ +/* + * Copyright 2014 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.resolver.NameResolver; +import io.netty.resolver.NameResolverGroup; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.internal.StringUtil; + +import java.net.InetSocketAddress; + +import static io.netty.resolver.dns.DnsNameResolver.*; + +/** + * A {@link NameResolverGroup} of {@link DnsNameResolver}s. + */ +public final class DnsNameResolverGroup extends NameResolverGroup { + + private final ChannelFactory channelFactory; + private final InetSocketAddress localAddress; + private final Iterable nameServerAddresses; + + public DnsNameResolverGroup( + Class channelType, + InetSocketAddress nameServerAddress) { + this(channelType, ANY_LOCAL_ADDR, nameServerAddress); + } + + public DnsNameResolverGroup( + Class channelType, + InetSocketAddress localAddress, InetSocketAddress nameServerAddress) { + this(new ReflectiveChannelFactory(channelType), localAddress, nameServerAddress); + } + + public DnsNameResolverGroup( + ChannelFactory channelFactory, + InetSocketAddress nameServerAddress) { + this(channelFactory, ANY_LOCAL_ADDR, nameServerAddress); + } + + public DnsNameResolverGroup( + ChannelFactory channelFactory, + InetSocketAddress localAddress, InetSocketAddress nameServerAddress) { + this(channelFactory, localAddress, DnsServerAddresses.singleton(nameServerAddress)); + } + + public DnsNameResolverGroup( + Class channelType, + Iterable nameServerAddresses) { + this(channelType, ANY_LOCAL_ADDR, nameServerAddresses); + } + + public DnsNameResolverGroup( + Class channelType, + InetSocketAddress localAddress, Iterable nameServerAddresses) { + this(new ReflectiveChannelFactory(channelType), localAddress, nameServerAddresses); + } + + public DnsNameResolverGroup( + ChannelFactory channelFactory, + Iterable nameServerAddresses) { + this(channelFactory, ANY_LOCAL_ADDR, nameServerAddresses); + } + + public DnsNameResolverGroup( + ChannelFactory channelFactory, + InetSocketAddress localAddress, Iterable nameServerAddresses) { + this.channelFactory = channelFactory; + this.localAddress = localAddress; + this.nameServerAddresses = nameServerAddresses; + } + + @Override + protected NameResolver newResolver(EventExecutor executor) throws Exception { + if (!(executor instanceof EventLoop)) { + throw new IllegalStateException( + "unsupported executor type: " + StringUtil.simpleClassName(executor) + + " (expected: " + StringUtil.simpleClassName(EventLoop.class)); + } + + return new DnsNameResolver((EventLoop) executor, channelFactory, localAddress, nameServerAddresses); + } +} diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java new file mode 100644 index 0000000000..9add6b88bc --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java @@ -0,0 +1,201 @@ +/* + * Copyright 2014 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.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.socket.DatagramChannel; +import io.netty.handler.codec.dns.DnsQuery; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsResource; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.handler.codec.dns.DnsType; +import io.netty.resolver.dns.DnsNameResolver.DnsCacheEntry; +import io.netty.util.concurrent.Promise; +import io.netty.util.concurrent.ScheduledFuture; +import io.netty.util.internal.OneTimeTask; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.ThreadLocalRandom; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Iterator; +import java.util.concurrent.TimeUnit; + +final class DnsQueryContext { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsQueryContext.class); + + private final DnsNameResolver parent; + private final Promise promise; + private final int id; + private final DnsQuestion question; + private final DnsResource optResource; + private final Iterator nameServerAddresses; + + private final boolean recursionDesired; + private final int maxTries; + private int remainingTries; + private volatile ScheduledFuture timeoutFuture; + private StringBuilder trace; + + DnsQueryContext(DnsNameResolver parent, + Iterable nameServerAddresses, + DnsQuestion question, Promise promise) { + + this.parent = parent; + this.promise = promise; + this.question = question; + + id = allocateId(); + recursionDesired = parent.isRecursionDesired(); + maxTries = parent.maxTriesPerQuery(); + remainingTries = maxTries; + optResource = new DnsResource("", DnsType.OPT, parent.maxPayloadSizeClass(), 0, Unpooled.EMPTY_BUFFER); + + this.nameServerAddresses = nameServerAddresses.iterator(); + } + + private int allocateId() { + int id = ThreadLocalRandom.current().nextInt(parent.promises.length()); + final int maxTries = parent.promises.length() << 1; + int tries = 0; + for (;;) { + if (parent.promises.compareAndSet(id, null, this)) { + return id; + } + + id = id + 1 & 0xFFFF; + + if (++ tries >= maxTries) { + throw new IllegalStateException("query ID space exhausted: " + question); + } + } + } + + Promise promise() { + return promise; + } + + DnsQuestion question() { + return question; + } + + ScheduledFuture timeoutFuture() { + return timeoutFuture; + } + + void query() { + final DnsQuestion question = this.question; + + if (remainingTries <= 0 || !nameServerAddresses.hasNext()) { + parent.promises.set(id, null); + + int tries = maxTries - remainingTries; + UnknownHostException cause; + if (tries > 1) { + cause = new UnknownHostException( + "failed to resolve " + question + " after " + tries + " attempts:" + + trace); + } else { + cause = new UnknownHostException("failed to resolve " + question + ':' + trace); + } + + cache(question, cause); + promise.tryFailure(cause); + return; + } + + remainingTries --; + + final InetSocketAddress nameServerAddr = nameServerAddresses.next(); + final DnsQuery query = new DnsQuery(id, nameServerAddr); + query.addQuestion(question); + query.header().setRecursionDesired(recursionDesired); + query.addAdditionalResource(optResource); + + final DatagramChannel ch = parent.ch; + + if (logger.isDebugEnabled()) { + logger.debug("{} WRITE: [{}: {}], {}", ch, id, nameServerAddr, question); + } + + final ChannelFuture writeFuture = ch.writeAndFlush(query); + if (writeFuture.isDone()) { + onQueryWriteCompletion(writeFuture, nameServerAddr); + } else { + writeFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + onQueryWriteCompletion(writeFuture, nameServerAddr); + } + }); + } + } + + private void onQueryWriteCompletion(ChannelFuture writeFuture, final InetSocketAddress nameServerAddr) { + if (!writeFuture.isSuccess()) { + retry(nameServerAddr, "failed to send a query: " + writeFuture.cause()); + return; + } + + // Schedule a query timeout task if necessary. + final long queryTimeoutMillis = parent.queryTimeoutMillis(); + if (queryTimeoutMillis > 0) { + timeoutFuture = parent.ch.eventLoop().schedule(new OneTimeTask() { + @Override + public void run() { + if (promise.isDone()) { + // Received a response before the query times out. + return; + } + + retry(nameServerAddr, "query timed out after " + queryTimeoutMillis + " milliseconds"); + } + }, queryTimeoutMillis, TimeUnit.MILLISECONDS); + } + } + + void retry(InetSocketAddress nameServerAddr, String message) { + if (promise.isCancelled()) { + return; + } + + if (trace == null) { + trace = new StringBuilder(128); + } + + trace.append(StringUtil.NEWLINE); + trace.append("\tfrom "); + trace.append(nameServerAddr); + trace.append(": "); + trace.append(message); + query(); + } + + private void cache(final DnsQuestion question, Throwable cause) { + final int negativeTtl = parent.negativeTtl(); + if (negativeTtl == 0) { + return; + } + + parent.cache(question, new DnsCacheEntry(cause), negativeTtl); + } +} diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java new file mode 100644 index 0000000000..c2cd6fe99a --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java @@ -0,0 +1,395 @@ +/* + * Copyright 2014 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.util.internal.PlatformDependent; +import io.netty.util.internal.ThreadLocalRandom; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Random; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +/** + * Provides a sequence of DNS server addresses to {@link DnsNameResolver}. The {@link Iterator} created by the + * {@link Iterable}s returned by the factory methods of this class is infinite, which means {@link Iterator#hasNext()} + * will never return {@code false} and {@link Iterator#next()} will never raise a {@link NoSuchElementException}. + */ +@SuppressWarnings("IteratorNextCanNotThrowNoSuchElementException") +public final class DnsServerAddresses { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsServerAddresses.class); + + private static final List DEFAULT_NAME_SERVER_LIST; + private static final InetSocketAddress[] DEFAULT_NAME_SERVER_ARRAY; + + static { + final int DNS_PORT = 53; + final List defaultNameServers = new ArrayList(2); + try { + Class configClass = Class.forName("sun.net.dns.ResolverConfiguration"); + Method open = configClass.getMethod("open"); + Method nameservers = configClass.getMethod("nameservers"); + Object instance = open.invoke(null); + + @SuppressWarnings("unchecked") + final List list = (List) nameservers.invoke(instance); + final int size = list.size(); + for (int i = 0; i < size; i ++) { + String dnsAddr = list.get(i); + if (dnsAddr != null) { + defaultNameServers.add(new InetSocketAddress(InetAddress.getByName(dnsAddr), DNS_PORT)); + } + } + } catch (Exception ignore) { + // Failed to get the system name server list. + // Will add the default name servers afterwards. + } + + if (!defaultNameServers.isEmpty()) { + if (logger.isDebugEnabled()) { + logger.debug( + "Default DNS servers: {} (sun.net.dns.ResolverConfiguration)", defaultNameServers); + } + } else { + Collections.addAll( + defaultNameServers, + new InetSocketAddress("8.8.8.8", DNS_PORT), + new InetSocketAddress("8.8.4.4", DNS_PORT)); + + if (logger.isWarnEnabled()) { + logger.warn( + "Default DNS servers: {} (Google Public DNS as a fallback)", defaultNameServers); + } + } + + DEFAULT_NAME_SERVER_LIST = Collections.unmodifiableList(defaultNameServers); + DEFAULT_NAME_SERVER_ARRAY = defaultNameServers.toArray(new InetSocketAddress[defaultNameServers.size()]); + } + + /** + * Returns the list of the system DNS server addresses. If it failed to retrieve the list of the system DNS server + * addresses from the environment, it will return {@code "8.8.8.8"} and {@code "8.8.4.4"}, the addresses of the + * Google public DNS servers. Note that the {@code Iterator} of the returned list is not infinite unlike other + * factory methods in this class. To make the returned list infinite, pass it to the other factory method. e.g. + *
+     * addresses = {@link #sequential(Iterable) sequential}({@link #defaultAddresses()});
+     * 
+ */ + public static List defaultAddresses() { + return DEFAULT_NAME_SERVER_LIST; + } + + /** + * Returns an infinite {@link Iterable} of the specified DNS server addresses, whose {@link Iterator} iterates + * the DNS server addresses in a sequential order. + */ + public static Iterable sequential(Iterable addresses) { + return sequential0(sanitize(addresses)); + } + + /** + * Returns an infinite {@link Iterable} of the specified DNS server addresses, whose {@link Iterator} iterates + * the DNS server addresses in a sequential order. + */ + public static Iterable sequential(InetSocketAddress... addresses) { + return sequential0(sanitize(addresses)); + } + + private static Iterable sequential0(final InetSocketAddress[] addresses) { + return new Iterable() { + @Override + public Iterator iterator() { + return new SequentialAddressIterator(addresses, 0); + } + }; + } + + /** + * Returns an infinite {@link Iterable} of the specified DNS server addresses, whose {@link Iterator} iterates + * the DNS server addresses in a shuffled order. + */ + public static Iterable shuffled(Iterable addresses) { + return shuffled0(sanitize(addresses)); + } + + /** + * Returns an infinite {@link Iterable} of the specified DNS server addresses, whose {@link Iterator} iterates + * the DNS server addresses in a shuffled order. + */ + public static Iterable shuffled(InetSocketAddress... addresses) { + return shuffled0(sanitize(addresses)); + } + + private static Iterable shuffled0(final InetSocketAddress[] addresses) { + if (addresses.length == 1) { + return singleton(addresses[0]); + } + + return new Iterable() { + @Override + public Iterator iterator() { + return new ShuffledAddressIterator(addresses); + } + }; + } + + /** + * Returns an infinite {@link Iterable} of the specified DNS server addresses, whose {@link Iterator} iterates + * the DNS server addresses in a rotational order. It is similar to {@link #sequential(Iterable)}, but each + * {@link Iterator} starts from a different starting point. For example, the first {@link Iterable#iterator()} + * will iterate from the first DNS server address, the second one will iterate from the second DNS server address, + * and so on. + */ + public static Iterable rotational(Iterable addresses) { + return rotational0(sanitize(addresses)); + } + + /** + * Returns an infinite {@link Iterable} of the specified DNS server addresses, whose {@link Iterator} iterates + * the DNS server addresses in a rotational order. It is similar to {@link #sequential(Iterable)}, but each + * {@link Iterator} starts from a different starting point. For example, the first {@link Iterable#iterator()} + * will iterate from the first DNS server address, the second one will iterate from the second DNS server address, + * and so on. + */ + public static Iterable rotational(InetSocketAddress... addresses) { + return rotational0(sanitize(addresses)); + } + + private static Iterable rotational0(final InetSocketAddress[] addresses) { + return new RotationalAddresses(addresses); + } + + /** + * Returns an infinite {@link Iterable} of the specified DNS server address, whose {@link Iterator} always + * return the same DNS server address. + */ + public static Iterable singleton(final InetSocketAddress address) { + if (address == null) { + throw new NullPointerException("address"); + } + if (address.isUnresolved()) { + throw new IllegalArgumentException("cannot use an unresolved DNS server address: " + address); + } + + return new Iterable() { + + private final Iterator iterator = new Iterator() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public InetSocketAddress next() { + return address; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + + @Override + public Iterator iterator() { + return iterator; + } + }; + } + + private static InetSocketAddress[] sanitize(Iterable addresses) { + if (addresses == null) { + throw new NullPointerException("addresses"); + } + + final List list; + if (addresses instanceof Collection) { + list = new ArrayList(((Collection) addresses).size()); + } else { + list = new ArrayList(4); + } + + for (InetSocketAddress a : addresses) { + if (a == null) { + break; + } + if (a.isUnresolved()) { + throw new IllegalArgumentException("cannot use an unresolved DNS server address: " + a); + } + list.add(a); + } + + if (list.isEmpty()) { + return DEFAULT_NAME_SERVER_ARRAY; + } + + return list.toArray(new InetSocketAddress[list.size()]); + } + + private static InetSocketAddress[] sanitize(InetSocketAddress[] addresses) { + if (addresses == null) { + throw new NullPointerException("addresses"); + } + + List list = new ArrayList(addresses.length); + for (InetSocketAddress a: addresses) { + if (a == null) { + break; + } + if (a.isUnresolved()) { + throw new IllegalArgumentException("cannot use an unresolved DNS server address: " + a); + } + list.add(a); + } + + if (list.isEmpty()) { + return DEFAULT_NAME_SERVER_ARRAY; + } + + return list.toArray(new InetSocketAddress[list.size()]); + } + + private DnsServerAddresses() { } + + private static final class SequentialAddressIterator implements Iterator { + + private final InetSocketAddress[] addresses; + private int i; + + SequentialAddressIterator(InetSocketAddress[] addresses, int startIdx) { + this.addresses = addresses; + i = startIdx; + } + + @Override + public boolean hasNext() { + return true; + } + + @Override + public InetSocketAddress next() { + int i = this.i; + InetSocketAddress next = addresses[i]; + if (++ i < addresses.length) { + this.i = i; + } else { + this.i = 0; + } + return next; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private static final class ShuffledAddressIterator implements Iterator { + + private final InetSocketAddress[] addresses; + private int i; + + ShuffledAddressIterator(InetSocketAddress[] addresses) { + this.addresses = addresses.clone(); + + shuffle(); + } + + private void shuffle() { + final InetSocketAddress[] addresses = this.addresses; + final Random r = ThreadLocalRandom.current(); + + for (int i = addresses.length - 1; i >= 0; i --) { + InetSocketAddress tmp = addresses[i]; + int j = r.nextInt(i + 1); + addresses[i] = addresses[j]; + addresses[j] = tmp; + } + } + + @Override + public boolean hasNext() { + return true; + } + + @Override + public InetSocketAddress next() { + int i = this.i; + InetSocketAddress next = addresses[i]; + if (++ i < addresses.length) { + this.i = i; + } else { + this.i = 0; + shuffle(); + } + return next; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private static final class RotationalAddresses implements Iterable { + + private static final AtomicIntegerFieldUpdater startIdxUpdater; + + static { + AtomicIntegerFieldUpdater updater = + PlatformDependent.newAtomicIntegerFieldUpdater(RotationalAddresses.class, "startIdx"); + + if (updater == null) { + updater = AtomicIntegerFieldUpdater.newUpdater(RotationalAddresses.class, "startIdx"); + } + + startIdxUpdater = updater; + } + + private final InetSocketAddress[] addresses; + @SuppressWarnings("UnusedDeclaration") + private volatile int startIdx; + + RotationalAddresses(InetSocketAddress[] addresses) { + this.addresses = addresses; + } + + @Override + public Iterator iterator() { + for (;;) { + int curStartIdx = startIdx; + int nextStartIdx = curStartIdx + 1; + if (nextStartIdx >= addresses.length) { + nextStartIdx = 0; + } + if (startIdxUpdater.compareAndSet(this, curStartIdx, nextStartIdx)) { + return new SequentialAddressIterator(addresses, curStartIdx); + } + } + } + } +} diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java b/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java new file mode 100644 index 0000000000..63825ba878 --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2014 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. + */ + +/** + * An alternative to Java's built-in domain name lookup mechanism that resolves a domain name asynchronously, + * which supports the queries of an arbitrary DNS record type as well. + */ +package io.netty.resolver.dns; 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 new file mode 100644 index 0000000000..eb6503743f --- /dev/null +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java @@ -0,0 +1,417 @@ +/* + * Copyright 2014 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.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsResource; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.handler.codec.dns.DnsResponseCode; +import io.netty.handler.codec.dns.DnsType; +import io.netty.util.concurrent.Future; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.ThreadLocalRandom; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +public class DnsNameResolverTest { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsNameResolver.class); + + private static final List SERVERS = Arrays.asList( + new InetSocketAddress("8.8.8.8", 53), // Google Public DNS + new InetSocketAddress("8.8.4.4", 53), + new InetSocketAddress("208.67.222.222", 53), // OpenDNS + new InetSocketAddress("208.67.220.220", 53), + new InetSocketAddress("37.235.1.174", 53), // FreeDNS + new InetSocketAddress("37.235.1.177", 53) + ); + + // Using the top-100 web sites ranked in Alexa.com (Oct 2014) + // Please use the following series of shell commands to get this up-to-date: + // $ curl -O http://s3.amazonaws.com/alexa-static/top-1m.csv.zip + // $ unzip -o top-1m.csv.zip top-1m.csv + // $ head -100 top-1m.csv | cut -d, -f2 | cut -d/ -f1 | while read L; do echo '"'"$L"'",'; done > topsites.txt + private static final String[] DOMAINS = { + "google.com", + "facebook.com", + "youtube.com", + "yahoo.com", + "baidu.com", + "wikipedia.org", + "amazon.com", + "twitter.com", + "qq.com", + "taobao.com", + "linkedin.com", + "google.co.in", + "live.com", + "hao123.com", + "sina.com.cn", + "blogspot.com", + "weibo.com", + "yahoo.co.jp", + "tmall.com", + "yandex.ru", + "sohu.com", + "bing.com", + "ebay.com", + "pinterest.com", + "vk.com", + "google.de", + "wordpress.com", + "apple.com", + "google.co.jp", + "google.co.uk", + "360.cn", + "instagram.com", + "google.fr", + "msn.com", + "ask.com", + "soso.com", + "google.com.br", + "tumblr.com", + "paypal.com", + "mail.ru", + "xvideos.com", + "microsoft.com", + "google.ru", + "reddit.com", + "google.it", + "imgur.com", + "163.com", + "google.es", + "imdb.com", + "aliexpress.com", + "t.co", + "go.com", + "adcash.com", + "craigslist.org", + "amazon.co.jp", + "alibaba.com", + "google.com.mx", + "stackoverflow.com", + "xhamster.com", + "fc2.com", + "google.ca", + "bbc.co.uk", + "espn.go.com", + "cnn.com", + "google.co.id", + "people.com.cn", + "gmw.cn", + "pornhub.com", + "blogger.com", + "huffingtonpost.com", + "flipkart.com", + "akamaihd.net", + "google.com.tr", + "amazon.de", + "netflix.com", + "onclickads.net", + "googleusercontent.com", + "kickass.to", + "google.com.au", + "google.pl", + "xinhuanet.com", + "ebay.de", + "wordpress.org", + "odnoklassniki.ru", + "google.com.hk", + "adobe.com", + "dailymotion.com", + "dailymail.co.uk", + "indiatimes.com", + "thepiratebay.se", + "amazon.co.uk", + "xnxx.com", + "rakuten.co.jp", + "dropbox.com", + "tudou.com", + "about.com", + "cnet.com", + "vimeo.com", + "redtube.com", + "blogspot.in", + }; + + /** + * The list of the domain names to exclude from {@link #testResolveAorAAAA()}. + */ + private static final Set EXCLUSIONS_RESOLVE_A = new HashSet(); + static { + Collections.addAll( + EXCLUSIONS_RESOLVE_A, + "akamaihd.net", + "googleusercontent.com", + ""); + } + + /** + * The list of the domain names to exclude from {@link #testResolveAAAA()}. + * Unfortunately, there are only handful of domain names with IPv6 addresses. + */ + private static final Set EXCLUSIONS_RESOLVE_AAAA = new HashSet(); + static { + EXCLUSIONS_RESOLVE_AAAA.addAll(EXCLUSIONS_RESOLVE_A); + Collections.addAll(EXCLUSIONS_RESOLVE_AAAA, DOMAINS); + EXCLUSIONS_RESOLVE_AAAA.removeAll(Arrays.asList( + "google.com", + "facebook.com", + "youtube.com", + "wikipedia.org", + "google.co.in", + "blogspot.com", + "vk.com", + "google.de", + "google.co.jp", + "google.co.uk", + "google.fr", + "google.com.br", + "google.ru", + "google.it", + "google.es", + "google.com.mx", + "xhamster.com", + "google.ca", + "google.co.id", + "blogger.com", + "flipkart.com", + "google.com.tr", + "google.com.au", + "google.pl", + "google.com.hk", + "blogspot.in" + )); + } + + /** + * The list of the domain names to exclude from {@link #testQueryMx()}. + */ + private static final Set EXCLUSIONS_QUERY_MX = new HashSet(); + static { + Collections.addAll( + EXCLUSIONS_QUERY_MX, + "hao123.com", + "blogspot.com", + "t.co", + "espn.go.com", + "people.com.cn", + "googleusercontent.com", + "blogspot.in", + ""); + } + + private static final EventLoopGroup group = new NioEventLoopGroup(1); + private static final DnsNameResolver resolver = new DnsNameResolver( + group.next(), NioDatagramChannel.class, DnsServerAddresses.shuffled(SERVERS)); + + static { + resolver.setMaxTriesPerQuery(SERVERS.size()); + } + + @AfterClass + public static void destroy() { + group.shutdownGracefully(); + } + + @Before + public void reset() { + resolver.clearCache(); + } + + @Test + public void testResolveAorAAAA() throws Exception { + testResolve0(EXCLUSIONS_RESOLVE_A, InternetProtocolFamily.IPv4, InternetProtocolFamily.IPv6); + } + + @Test + public void testResolveAAAAorA() throws Exception { + testResolve0(EXCLUSIONS_RESOLVE_A, InternetProtocolFamily.IPv6, InternetProtocolFamily.IPv4); + } + + @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); + + try { + final Map resultA = testResolve0(EXCLUSIONS_RESOLVE_A, InternetProtocolFamily.IPv4); + + // 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); + + assertThat(resultA, is(resultB)); + } finally { + // Restore the TTL configuration. + resolver.setTtl(oldMinTtl, oldMaxTtl); + } + } + + @Test + public void testResolveAAAA() throws Exception { + testResolve0(EXCLUSIONS_RESOLVE_AAAA, InternetProtocolFamily.IPv6); + } + + private static Map testResolve0( + Set excludedDomains, InternetProtocolFamily... famililies) throws InterruptedException { + + final List oldResolveAddressTypes = resolver.resolveAddressTypes(); + + 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>(); + + for (String name : DOMAINS) { + if (excludedDomains.contains(name)) { + continue; + } + + resolve(futures, name); + } + + for (Entry> e : futures.entrySet()) { + InetSocketAddress unresolved = e.getKey(); + InetSocketAddress resolved = e.getValue().sync().getNow(); + + logger.info("{}: {}", unresolved.getHostString(), resolved.getAddress().getHostAddress()); + + assertThat(resolved.isUnresolved(), is(false)); + assertThat(resolved.getHostString(), is(unresolved.getHostString())); + assertThat(resolved.getPort(), is(unresolved.getPort())); + + boolean typeMatches = false; + for (InternetProtocolFamily f: famililies) { + Class resolvedType = resolved.getAddress().getClass(); + switch (f) { + case IPv4: + if (Inet4Address.class.isAssignableFrom(resolvedType)) { + typeMatches = true; + } + break; + case IPv6: + if (Inet6Address.class.isAssignableFrom(resolvedType)) { + typeMatches = true; + } + break; + } + } + + assertThat(typeMatches, is(true)); + + results.put(resolved.getHostString(), resolved.getAddress()); + } + } finally { + resolver.setResolveAddressTypes(oldResolveAddressTypes); + } + + return results; + } + + @Test + public void testQueryMx() throws Exception { + 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(); + DnsResponse response = e.getValue().sync().getNow(); + + assertThat(response.header().responseCode(), is(DnsResponseCode.NOERROR)); + List mxList = new ArrayList(); + for (DnsResource r: response.answers()) { + if (r.type() == DnsType.MX) { + mxList.add(r); + } + } + + assertThat(mxList.size(), is(greaterThan(0))); + StringBuilder buf = new StringBuilder(); + for (DnsResource r: mxList) { + buf.append(StringUtil.NEWLINE); + buf.append('\t'); + buf.append(r.name()); + buf.append(' '); + buf.append(r.type()); + buf.append(' '); + buf.append(r.content().readUnsignedShort()); + buf.append(' '); + buf.append(DnsNameResolverContext.decodeDomainName(r.content())); + } + + logger.info("{} has the following MX records:{}", hostname, buf); + response.release(); + } + } + + private static void resolve(Map> futures, String hostname) { + InetSocketAddress unresolved = + InetSocketAddress.createUnresolved(hostname, ThreadLocalRandom.current().nextInt(65536)); + + futures.put(unresolved, resolver.resolve(unresolved)); + } + + private static void queryMx(Map> futures, String hostname) throws Exception { + futures.put(hostname, resolver.query(new DnsQuestion(hostname, DnsType.MX))); + } +} diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressesTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressesTest.java new file mode 100644 index 0000000000..25a6190468 --- /dev/null +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressesTest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2014 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.util.NetUtil; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Set; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +public class DnsServerAddressesTest { + + private static final InetSocketAddress ADDR1 = new InetSocketAddress(NetUtil.LOCALHOST, 1); + private static final InetSocketAddress ADDR2 = new InetSocketAddress(NetUtil.LOCALHOST, 2); + private static final InetSocketAddress ADDR3 = new InetSocketAddress(NetUtil.LOCALHOST, 3); + + @Test + public void testDefaultAddresses() { + assertThat(DnsServerAddresses.defaultAddresses().size(), is(greaterThan(0))); + } + + @Test + public void testSequential() { + Iterable seq = DnsServerAddresses.sequential(ADDR1, ADDR2, ADDR3); + assertThat(seq.iterator(), is(not(sameInstance(seq.iterator())))); + + for (int j = 0; j < 2; j ++) { + Iterator i = seq.iterator(); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + } + } + + @Test + public void testRotational() { + Iterable seq = DnsServerAddresses.rotational(ADDR1, ADDR2, ADDR3); + + Iterator i = seq.iterator(); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + + i = seq.iterator(); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + + i = seq.iterator(); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + + i = seq.iterator(); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + } + + @Test + public void testShuffled() { + Iterable seq = DnsServerAddresses.shuffled(ADDR1, ADDR2, ADDR3); + + // Ensure that all three addresses are returned by the iterator. + // In theory, this test can fail at extremely low chance, but we don't really care. + Set set = Collections.newSetFromMap(new IdentityHashMap()); + Iterator i = seq.iterator(); + for (int j = 0; j < 1048576; j ++) { + assertThat(i.hasNext(), is(true)); + set.add(i.next()); + } + + assertThat(set.size(), is(3)); + assertThat(seq.iterator(), is(not(sameInstance(seq.iterator())))); + } + + @Test + public void testSingleton() { + Iterable seq = DnsServerAddresses.singleton(ADDR1); + + // Should return the same iterator instance for least possible footprint. + assertThat(seq.iterator(), is(sameInstance(seq.iterator()))); + + Iterator i = seq.iterator(); + assertNext(i, ADDR1); + assertNext(i, ADDR1); + assertNext(i, ADDR1); + } + + private static void assertNext(Iterator i, InetSocketAddress addr) { + assertThat(i.hasNext(), is(true)); + assertThat(i.next(), is(sameInstance(addr))); + } +} diff --git a/resolver/pom.xml b/resolver/pom.xml new file mode 100644 index 0000000000..13f67c363f --- /dev/null +++ b/resolver/pom.xml @@ -0,0 +1,39 @@ + + + + + 4.0.0 + + io.netty + netty-parent + 4.1.0.Beta4-SNAPSHOT + + + netty-resolver + jar + + Netty/Resolver + + + + ${project.groupId} + netty-common + ${project.version} + + + + diff --git a/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java b/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java new file mode 100644 index 0000000000..bdfb3b7cb4 --- /dev/null +++ b/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java @@ -0,0 +1,52 @@ +/* + * Copyright 2014 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.Promise; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +/** + * A {@link NameResolver} that resolves an {@link InetSocketAddress} using JDK's built-in domain name lookup mechanism. + * Note that this resolver performs a blocking name lookup from the caller thread. + */ +public class DefaultNameResolver extends SimpleNameResolver { + + public DefaultNameResolver(EventExecutor executor) { + super(executor); + } + + @Override + protected boolean doIsResolved(InetSocketAddress address) { + return !address.isUnresolved(); + } + + @Override + protected void doResolve(InetSocketAddress unresolvedAddress, Promise promise) throws Exception { + try { + promise.setSuccess( + new InetSocketAddress( + InetAddress.getByName(unresolvedAddress.getHostString()), + unresolvedAddress.getPort())); + } catch (UnknownHostException e) { + promise.setFailure(e); + } + } +} diff --git a/resolver/src/main/java/io/netty/resolver/DefaultNameResolverGroup.java b/resolver/src/main/java/io/netty/resolver/DefaultNameResolverGroup.java new file mode 100644 index 0000000000..f5b76ae1e5 --- /dev/null +++ b/resolver/src/main/java/io/netty/resolver/DefaultNameResolverGroup.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 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 java.net.InetSocketAddress; + +/** + * A {@link NameResolverGroup} of {@link DefaultNameResolver}s. + */ +public final class DefaultNameResolverGroup extends NameResolverGroup { + + public static final DefaultNameResolverGroup INSTANCE = new DefaultNameResolverGroup(); + + private DefaultNameResolverGroup() { } + + @Override + protected NameResolver newResolver(EventExecutor executor) throws Exception { + return new DefaultNameResolver(executor); + } +} diff --git a/resolver/src/main/java/io/netty/resolver/NameResolver.java b/resolver/src/main/java/io/netty/resolver/NameResolver.java new file mode 100644 index 0000000000..7760d1f460 --- /dev/null +++ b/resolver/src/main/java/io/netty/resolver/NameResolver.java @@ -0,0 +1,90 @@ +/* + * Copyright 2014 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.Future; +import io.netty.util.concurrent.Promise; + +import java.io.Closeable; +import java.net.SocketAddress; +import java.nio.channels.UnsupportedAddressTypeException; + +/** + * Resolves an arbitrary string that represents the name of an endpoint into a {@link SocketAddress}. + */ +public interface NameResolver extends Closeable { + + /** + * Returns {@code true} if and only if the specified address is supported by this resolved. + */ + boolean isSupported(SocketAddress address); + + /** + * Returns {@code true} if and only if the specified address has been resolved. + * + * @throws UnsupportedAddressTypeException if the specified address is not supported by this resolver + */ + boolean isResolved(SocketAddress address); + + /** + * Resolves the specified name into a {@link SocketAddress}. + * + * @param inetHost the name to resolve + * @param inetPort the port number + * + * @return the {@link SocketAddress} as the result of the resolution + */ + Future resolve(String inetHost, int inetPort); + + /** + * Resolves the specified name into a {@link SocketAddress}. + * + * @param inetHost the name to resolve + * @param inetPort the port number + * @param promise the {@link Promise} which will be fulfilled when the name resolution is finished + * + * @return the {@link SocketAddress} as the result of the resolution + */ + Future resolve(String inetHost, int inetPort, Promise promise); + + /** + * Resolves the specified address. If the specified address is resolved already, this method does nothing + * but returning the original address. + * + * @param address the address to resolve + * + * @return the {@link SocketAddress} as the result of the resolution + */ + Future resolve(SocketAddress address); + + /** + * Resolves the specified address. If the specified address is resolved already, this method does nothing + * but returning the original address. + * + * @param address the address to resolve + * @param promise the {@link Promise} which will be fulfilled when the name resolution is finished + * + * @return the {@link SocketAddress} as the result of the resolution + */ + Future resolve(SocketAddress address, Promise promise); + + /** + * Closes all the resources allocated and used by this resolver. + */ + @Override + void close(); +} diff --git a/resolver/src/main/java/io/netty/resolver/NameResolverGroup.java b/resolver/src/main/java/io/netty/resolver/NameResolverGroup.java new file mode 100644 index 0000000000..58662a4ec5 --- /dev/null +++ b/resolver/src/main/java/io/netty/resolver/NameResolverGroup.java @@ -0,0 +1,113 @@ +/* + * Copyright 2014 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.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.io.Closeable; +import java.net.SocketAddress; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; + +/** + * Creates and manages {@link NameResolver}s so that each {@link EventExecutor} has its own resolver instance. + */ +public abstract class NameResolverGroup implements Closeable { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(NameResolverGroup.class); + + /** + * Note that we do not use a {@link ConcurrentMap} here because it is usually expensive to instantiate a resolver. + */ + private final Map> resolvers = + new IdentityHashMap>(); + + protected NameResolverGroup() { } + + /** + * Returns the {@link NameResolver} associated with the specified {@link EventExecutor}. If there's no associated + * resolved found, this method creates and returns a new resolver instance created by + * {@link #newResolver(EventExecutor)} so that the new resolver is reused on another + * {@link #getResolver(EventExecutor)} call with the same {@link EventExecutor}. + */ + public NameResolver getResolver(final EventExecutor executor) { + if (executor == null) { + throw new NullPointerException("executor"); + } + + if (executor.isShuttingDown()) { + throw new IllegalStateException("executor not accepting a task"); + } + + NameResolver r; + synchronized (resolvers) { + r = resolvers.get(executor); + if (r == null) { + final NameResolver newResolver; + try { + newResolver = newResolver(executor); + } catch (Exception e) { + throw new IllegalStateException("failed to create a new resolver", e); + } + + resolvers.put(executor, newResolver); + executor.terminationFuture().addListener(new FutureListener() { + @Override + public void operationComplete(Future future) throws Exception { + resolvers.remove(executor); + newResolver.close(); + } + }); + + r = newResolver; + } + } + + return r; + } + + /** + * Invoked by {@link #getResolver(EventExecutor)} to create a new {@link NameResolver}. + */ + protected abstract NameResolver newResolver(EventExecutor executor) throws Exception; + + /** + * Closes all {@link NameResolver}s created by this group. + */ + @Override + @SuppressWarnings({ "unchecked", "SuspiciousToArrayCall" }) + public void close() { + final NameResolver[] rArray; + synchronized (resolvers) { + rArray = (NameResolver[]) resolvers.values().toArray(new NameResolver[resolvers.size()]); + resolvers.clear(); + } + + for (NameResolver r: rArray) { + try { + r.close(); + } catch (Throwable t) { + logger.warn("Failed to close a resolver:", t); + } + } + } +} diff --git a/resolver/src/main/java/io/netty/resolver/NoopNameResolver.java b/resolver/src/main/java/io/netty/resolver/NoopNameResolver.java new file mode 100644 index 0000000000..8605aae894 --- /dev/null +++ b/resolver/src/main/java/io/netty/resolver/NoopNameResolver.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014 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.Promise; + +import java.net.SocketAddress; + +/** + * A {@link NameResolver} that does not perform any resolution but always reports successful resolution. + * This resolver is useful when name resolution is performed by a handler in a pipeline, such as a proxy handler. + */ +public class NoopNameResolver extends SimpleNameResolver { + + public NoopNameResolver(EventExecutor executor) { + super(executor); + } + + @Override + protected boolean doIsResolved(SocketAddress address) { + return true; + } + + @Override + protected void doResolve(SocketAddress unresolvedAddress, Promise promise) throws Exception { + promise.setSuccess(unresolvedAddress); + } +} diff --git a/resolver/src/main/java/io/netty/resolver/NoopNameResolverGroup.java b/resolver/src/main/java/io/netty/resolver/NoopNameResolverGroup.java new file mode 100644 index 0000000000..7c6d601afd --- /dev/null +++ b/resolver/src/main/java/io/netty/resolver/NoopNameResolverGroup.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 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 java.net.SocketAddress; + +/** + * A {@link NameResolverGroup} of {@link NoopNameResolver}s. + */ +public final class NoopNameResolverGroup extends NameResolverGroup { + + public static final NoopNameResolverGroup INSTANCE = new NoopNameResolverGroup(); + + private NoopNameResolverGroup() { } + + @Override + protected NameResolver newResolver(EventExecutor executor) throws Exception { + return new NoopNameResolver(executor); + } +} diff --git a/resolver/src/main/java/io/netty/resolver/SimpleNameResolver.java b/resolver/src/main/java/io/netty/resolver/SimpleNameResolver.java new file mode 100644 index 0000000000..9bc5329bc6 --- /dev/null +++ b/resolver/src/main/java/io/netty/resolver/SimpleNameResolver.java @@ -0,0 +1,179 @@ +/* + * Copyright 2014 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.Promise; +import io.netty.util.internal.TypeParameterMatcher; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.channels.UnsupportedAddressTypeException; + +/** + * A skeletal {@link NameResolver} implementation. + */ +public abstract class SimpleNameResolver implements NameResolver { + + private final EventExecutor executor; + private final TypeParameterMatcher matcher; + + /** + * @param executor the {@link EventExecutor} which is used to notify the listeners of the {@link Future} returned + * by {@link #resolve(SocketAddress)} + */ + protected SimpleNameResolver(EventExecutor executor) { + if (executor == null) { + throw new NullPointerException("executor"); + } + + this.executor = executor; + matcher = TypeParameterMatcher.find(this, SimpleNameResolver.class, "T"); + } + + /** + * @param executor the {@link EventExecutor} which is used to notify the listeners of the {@link Future} returned + * by {@link #resolve(SocketAddress)} + * @param addressType the type of the {@link SocketAddress} supported by this resolver + */ + protected SimpleNameResolver(EventExecutor executor, Class addressType) { + if (executor == null) { + throw new NullPointerException("executor"); + } + + this.executor = executor; + matcher = TypeParameterMatcher.get(addressType); + } + + /** + * Returns the {@link EventExecutor} which is used to notify the listeners of the {@link Future} returned + * by {@link #resolve(SocketAddress)}. + */ + protected EventExecutor executor() { + return executor; + } + + @Override + public boolean isSupported(SocketAddress address) { + return matcher.match(address); + } + + @Override + public final boolean isResolved(SocketAddress address) { + if (!isSupported(address)) { + throw new UnsupportedAddressTypeException(); + } + + @SuppressWarnings("unchecked") + final T castAddress = (T) address; + return doIsResolved(castAddress); + } + + /** + * Invoked by {@link #isResolved(SocketAddress)} to check if the specified {@code address} has been resolved + * already. + */ + protected abstract boolean doIsResolved(T address); + + @Override + public final Future resolve(String inetHost, int inetPort) { + if (inetHost == null) { + throw new NullPointerException("inetHost"); + } + + return resolve(InetSocketAddress.createUnresolved(inetHost, inetPort)); + } + + @Override + public Future resolve(String inetHost, int inetPort, Promise promise) { + if (inetHost == null) { + throw new NullPointerException("inetHost"); + } + + return resolve(InetSocketAddress.createUnresolved(inetHost, inetPort), promise); + } + + @Override + public final Future resolve(SocketAddress address) { + if (address == null) { + throw new NullPointerException("unresolvedAddress"); + } + + if (!isSupported(address)) { + // Address type not supported by the resolver + return executor().newFailedFuture(new UnsupportedAddressTypeException()); + } + + if (isResolved(address)) { + // Resolved already; no need to perform a lookup + @SuppressWarnings("unchecked") + final T cast = (T) address; + return executor.newSucceededFuture(cast); + } + + try { + @SuppressWarnings("unchecked") + final T cast = (T) address; + final Promise promise = executor().newPromise(); + doResolve(cast, promise); + return promise; + } catch (Exception e) { + return executor().newFailedFuture(e); + } + } + + @Override + public final Future resolve(SocketAddress address, Promise promise) { + if (address == null) { + throw new NullPointerException("unresolvedAddress"); + } + if (promise == null) { + throw new NullPointerException("promise"); + } + + if (!isSupported(address)) { + // Address type not supported by the resolver + return promise.setFailure(new UnsupportedAddressTypeException()); + } + + if (isResolved(address)) { + // Resolved already; no need to perform a lookup + @SuppressWarnings("unchecked") + final T cast = (T) address; + return promise.setSuccess(cast); + } + + try { + @SuppressWarnings("unchecked") + final T cast = (T) address; + doResolve(cast, promise); + return promise; + } catch (Exception e) { + return promise.setFailure(e); + } + } + + /** + * Invoked by {@link #resolve(SocketAddress)} and {@link #resolve(String, int)} to perform the actual name + * resolution. + */ + protected abstract void doResolve(T unresolvedAddress, Promise promise) throws Exception; + + @Override + public void close() { } +} diff --git a/resolver/src/main/java/io/netty/resolver/package-info.java b/resolver/src/main/java/io/netty/resolver/package-info.java new file mode 100644 index 0000000000..33c69307b5 --- /dev/null +++ b/resolver/src/main/java/io/netty/resolver/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014 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. + */ + +/** + * Resolves an arbitrary string that represents the name of an endpoint into a {@link java.net.SocketAddress}. + */ +package io.netty.resolver; diff --git a/testsuite/src/test/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java b/testsuite/src/test/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java index 740c69753d..730f1787de 100644 --- a/testsuite/src/test/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java +++ b/testsuite/src/test/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java @@ -17,9 +17,9 @@ package io.netty.testsuite.transport.socket; import io.netty.bootstrap.AbstractBootstrap; import io.netty.bootstrap.Bootstrap; -import io.netty.bootstrap.ChannelFactory; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; +import io.netty.channel.ChannelFactory; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTestPermutation.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTestPermutation.java index 6d56c6b891..00aaf4ffe1 100644 --- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTestPermutation.java +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTestPermutation.java @@ -16,9 +16,9 @@ package io.netty.channel.epoll; import io.netty.bootstrap.Bootstrap; -import io.netty.bootstrap.ChannelFactory; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; +import io.netty.channel.ChannelFactory; import io.netty.channel.EventLoopGroup; import io.netty.channel.socket.InternetProtocolFamily; import io.netty.channel.socket.nio.NioDatagramChannel; diff --git a/transport-udt/src/main/java/io/netty/channel/udt/nio/NioUdtProvider.java b/transport-udt/src/main/java/io/netty/channel/udt/nio/NioUdtProvider.java index 095258f9c7..08c81d0ca9 100644 --- a/transport-udt/src/main/java/io/netty/channel/udt/nio/NioUdtProvider.java +++ b/transport-udt/src/main/java/io/netty/channel/udt/nio/NioUdtProvider.java @@ -23,11 +23,11 @@ import com.barchart.udt.nio.RendezvousChannelUDT; import com.barchart.udt.nio.SelectorProviderUDT; import com.barchart.udt.nio.ServerSocketChannelUDT; import com.barchart.udt.nio.SocketChannelUDT; -import io.netty.bootstrap.ChannelFactory; import io.netty.channel.Channel; import io.netty.channel.ChannelException; -import io.netty.channel.udt.UdtServerChannel; +import io.netty.channel.ChannelFactory; import io.netty.channel.udt.UdtChannel; +import io.netty.channel.udt.UdtServerChannel; import java.io.IOException; import java.nio.channels.spi.SelectorProvider; diff --git a/transport/pom.xml b/transport/pom.xml index e0bfe5bb50..6f30d3546d 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -34,6 +34,11 @@ netty-buffer ${project.version} + + ${project.groupId} + netty-resolver + ${project.version} + diff --git a/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java b/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java index 48ad5da3d4..e6ece5a79f 100644 --- a/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java +++ b/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java @@ -17,7 +17,6 @@ package io.netty.bootstrap; import io.netty.channel.Channel; -import io.netty.channel.ChannelException; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; @@ -26,6 +25,7 @@ import io.netty.channel.ChannelPromise; import io.netty.channel.DefaultChannelPromise; import io.netty.channel.EventLoop; import io.netty.channel.EventLoopGroup; +import io.netty.channel.ReflectiveChannelFactory; import io.netty.util.AttributeKey; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.GlobalEventExecutor; @@ -47,6 +47,7 @@ import java.util.Map; public abstract class AbstractBootstrap, C extends Channel> implements Cloneable { private volatile EventLoopGroup group; + @SuppressWarnings("deprecation") private volatile ChannelFactory channelFactory; private volatile SocketAddress localAddress; private final Map, Object> options = new LinkedHashMap, Object>(); @@ -88,23 +89,20 @@ public abstract class AbstractBootstrap, C ext /** * The {@link Class} which is used to create {@link Channel} instances from. - * You either use this or {@link #channelFactory(ChannelFactory)} if your + * You either use this or {@link #channelFactory(io.netty.channel.ChannelFactory)} if your * {@link Channel} implementation has no no-args constructor. */ public B channel(Class channelClass) { if (channelClass == null) { throw new NullPointerException("channelClass"); } - return channelFactory(new BootstrapChannelFactory(channelClass)); + return channelFactory(new ReflectiveChannelFactory(channelClass)); } /** - * {@link ChannelFactory} which is used to create {@link Channel} instances from - * when calling {@link #bind()}. This method is usually only used if {@link #channel(Class)} - * is not working for you because of some more complex needs. If your {@link Channel} implementation - * has a no-args constructor, its highly recommend to just use {@link #channel(Class)} for - * simplify your code. + * @deprecated Use {@link #channelFactory(io.netty.channel.ChannelFactory)} instead. */ + @Deprecated @SuppressWarnings("unchecked") public B channelFactory(ChannelFactory channelFactory) { if (channelFactory == null) { @@ -118,9 +116,20 @@ public abstract class AbstractBootstrap, C ext return (B) this; } + /** + * {@link io.netty.channel.ChannelFactory} which is used to create {@link Channel} instances from + * when calling {@link #bind()}. This method is usually only used if {@link #channel(Class)} + * is not working for you because of some more complex needs. If your {@link Channel} implementation + * has a no-args constructor, its highly recommend to just use {@link #channel(Class)} for + * simplify your code. + */ + @SuppressWarnings({ "unchecked", "deprecation" }) + public B channelFactory(io.netty.channel.ChannelFactory channelFactory) { + return channelFactory((ChannelFactory) channelFactory); + } + /** * The {@link SocketAddress} which is used to bind the local "end" to. - * */ @SuppressWarnings("unchecked") public B localAddress(SocketAddress localAddress) { @@ -371,6 +380,7 @@ public abstract class AbstractBootstrap, C ext return localAddress; } + @SuppressWarnings("deprecation") final ChannelFactory channelFactory() { return channelFactory; } @@ -442,28 +452,6 @@ public abstract class AbstractBootstrap, C ext return buf.toString(); } - private static final class BootstrapChannelFactory implements ChannelFactory { - private final Class clazz; - - BootstrapChannelFactory(Class clazz) { - this.clazz = clazz; - } - - @Override - public T newChannel() { - try { - return clazz.newInstance(); - } catch (Throwable t) { - throw new ChannelException("Unable to create Channel from class " + clazz, t); - } - } - - @Override - public String toString() { - return StringUtil.simpleClassName(clazz) + ".class"; - } - } - private static final class PendingRegistrationPromise extends DefaultChannelPromise { // Is set to the correct EventExecutor once the registration was successful. Otherwise it will // stay null and so the GlobalEventExecutor.INSTANCE will be used for notifications. diff --git a/transport/src/main/java/io/netty/bootstrap/Bootstrap.java b/transport/src/main/java/io/netty/bootstrap/Bootstrap.java index 687d717fc9..2cd4c3227f 100644 --- a/transport/src/main/java/io/netty/bootstrap/Bootstrap.java +++ b/transport/src/main/java/io/netty/bootstrap/Bootstrap.java @@ -21,7 +21,13 @@ import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoop; +import io.netty.resolver.DefaultNameResolverGroup; +import io.netty.resolver.NameResolver; +import io.netty.resolver.NameResolverGroup; import io.netty.util.AttributeKey; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -42,6 +48,10 @@ public final class Bootstrap extends AbstractBootstrap { private static final InternalLogger logger = InternalLoggerFactory.getInstance(Bootstrap.class); + private static final NameResolverGroup DEFAULT_RESOLVER = DefaultNameResolverGroup.INSTANCE; + + @SuppressWarnings("unchecked") + private volatile NameResolverGroup resolver = (NameResolverGroup) DEFAULT_RESOLVER; private volatile SocketAddress remoteAddress; public Bootstrap() { } @@ -51,6 +61,18 @@ public final class Bootstrap extends AbstractBootstrap { remoteAddress = bootstrap.remoteAddress; } + /** + * Sets the {@link NameResolver} which will resolve the address of the unresolved named address. + */ + @SuppressWarnings("unchecked") + public Bootstrap resolver(NameResolverGroup resolver) { + if (resolver == null) { + throw new NullPointerException("resolver"); + } + this.resolver = (NameResolverGroup) resolver; + return this; + } + /** * The {@link SocketAddress} to connect to once the {@link #connect()} method * is called. @@ -64,7 +86,7 @@ public final class Bootstrap extends AbstractBootstrap { * @see {@link #remoteAddress(SocketAddress)} */ public Bootstrap remoteAddress(String inetHost, int inetPort) { - remoteAddress = new InetSocketAddress(inetHost, inetPort); + remoteAddress = InetSocketAddress.createUnresolved(inetHost, inetPort); return this; } @@ -86,14 +108,14 @@ public final class Bootstrap extends AbstractBootstrap { throw new IllegalStateException("remoteAddress not set"); } - return doConnect(remoteAddress, localAddress()); + return doResolveAndConnect(remoteAddress, localAddress()); } /** * Connect a {@link Channel} to the remote peer. */ public ChannelFuture connect(String inetHost, int inetPort) { - return connect(new InetSocketAddress(inetHost, inetPort)); + return connect(InetSocketAddress.createUnresolved(inetHost, inetPort)); } /** @@ -112,7 +134,7 @@ public final class Bootstrap extends AbstractBootstrap { } validate(); - return doConnect(remoteAddress, localAddress()); + return doResolveAndConnect(remoteAddress, localAddress()); } /** @@ -123,52 +145,94 @@ public final class Bootstrap extends AbstractBootstrap { throw new NullPointerException("remoteAddress"); } validate(); - return doConnect(remoteAddress, localAddress); + return doResolveAndConnect(remoteAddress, localAddress); } /** * @see {@link #connect()} */ - private ChannelFuture doConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) { + private ChannelFuture doResolveAndConnect(SocketAddress remoteAddress, final SocketAddress localAddress) { final ChannelFuture regFuture = initAndRegister(); - final Channel channel = regFuture.channel(); if (regFuture.cause() != null) { return regFuture; } - final ChannelPromise promise = channel.newPromise(); + final Channel channel = regFuture.channel(); + final EventLoop eventLoop = channel.eventLoop(); + final NameResolver resolver = this.resolver.getResolver(eventLoop); + + if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) { + // Resolver has no idea about what to do with the specified remote address or it's resolved already. + return doConnect(remoteAddress, localAddress, regFuture, channel.newPromise()); + } + + final Future resolveFuture = resolver.resolve(remoteAddress); + final Throwable resolveFailureCause = resolveFuture.cause(); + + if (resolveFailureCause != null) { + // Failed to resolve immediately + channel.close(); + return channel.newFailedFuture(resolveFailureCause); + } + + if (resolveFuture.isDone()) { + // Succeeded to resolve immediately; cached? (or did a blocking lookup) + return doConnect(resolveFuture.getNow(), localAddress, regFuture, channel.newPromise()); + } + + // Wait until the name resolution is finished. + final ChannelPromise connectPromise = channel.newPromise(); + resolveFuture.addListener(new FutureListener() { + @Override + public void operationComplete(Future future) throws Exception { + if (future.cause() != null) { + channel.close(); + connectPromise.setFailure(future.cause()); + } else { + doConnect(future.getNow(), localAddress, regFuture, connectPromise); + } + } + }); + + return connectPromise; + } + + private static ChannelFuture doConnect( + final SocketAddress remoteAddress, final SocketAddress localAddress, + final ChannelFuture regFuture, final ChannelPromise connectPromise) { if (regFuture.isDone()) { - doConnect0(regFuture, channel, remoteAddress, localAddress, promise); + doConnect0(remoteAddress, localAddress, regFuture, connectPromise); } else { regFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { - doConnect0(regFuture, channel, remoteAddress, localAddress, promise); + doConnect0(remoteAddress, localAddress, regFuture, connectPromise); } }); } - return promise; + return connectPromise; } private static void doConnect0( - final ChannelFuture regFuture, final Channel channel, - final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) { + final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelFuture regFuture, + final ChannelPromise connectPromise) { // This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up // the pipeline in its channelRegistered() implementation. + final Channel channel = connectPromise.channel(); channel.eventLoop().execute(new Runnable() { @Override public void run() { if (regFuture.isSuccess()) { if (localAddress == null) { - channel.connect(remoteAddress, promise); + channel.connect(remoteAddress, connectPromise); } else { - channel.connect(remoteAddress, localAddress, promise); + channel.connect(remoteAddress, localAddress, connectPromise); } - promise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE); } else { - promise.setFailure(regFuture.cause()); + connectPromise.setFailure(regFuture.cause()); } } }); diff --git a/transport/src/main/java/io/netty/bootstrap/ChannelFactory.java b/transport/src/main/java/io/netty/bootstrap/ChannelFactory.java index 292eb6efa6..6d12b99864 100644 --- a/transport/src/main/java/io/netty/bootstrap/ChannelFactory.java +++ b/transport/src/main/java/io/netty/bootstrap/ChannelFactory.java @@ -18,9 +18,9 @@ package io.netty.bootstrap; import io.netty.channel.Channel; /** - * Factory that creates a new {@link Channel} on {@link Bootstrap#bind()}, {@link Bootstrap#connect()}, and - * {@link ServerBootstrap#bind()}. + * @deprecated Use {@link io.netty.channel.ChannelFactory} instead. */ +@Deprecated public interface ChannelFactory { /** * Creates a new channel. diff --git a/transport/src/main/java/io/netty/channel/ChannelFactory.java b/transport/src/main/java/io/netty/channel/ChannelFactory.java new file mode 100644 index 0000000000..dddea4eb45 --- /dev/null +++ b/transport/src/main/java/io/netty/channel/ChannelFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright 2014 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.channel; + +/** + * Creates a new {@link Channel}. + */ +@SuppressWarnings({ "ClassNameSameAsAncestorName", "deprecation" }) +public interface ChannelFactory extends io.netty.bootstrap.ChannelFactory { + /** + * Creates a new channel. + */ + @Override + T newChannel(); +} diff --git a/transport/src/main/java/io/netty/channel/ReflectiveChannelFactory.java b/transport/src/main/java/io/netty/channel/ReflectiveChannelFactory.java new file mode 100644 index 0000000000..0db431bbb0 --- /dev/null +++ b/transport/src/main/java/io/netty/channel/ReflectiveChannelFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014 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.channel; + +import io.netty.util.internal.StringUtil; + +/** + * A {@link ChannelFactory} that instantiates a new {@link Channel} by invoking its default constructor reflectively. + */ +public class ReflectiveChannelFactory implements ChannelFactory { + + private final Class clazz; + + public ReflectiveChannelFactory(Class clazz) { + if (clazz == null) { + throw new NullPointerException("clazz"); + } + this.clazz = clazz; + } + + @Override + public T newChannel() { + try { + return clazz.newInstance(); + } catch (Throwable t) { + throw new ChannelException("Unable to create Channel from class " + clazz, t); + } + } + + @Override + public String toString() { + return StringUtil.simpleClassName(clazz) + ".class"; + } +} diff --git a/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java b/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java index 12964e3ae5..39d5864221 100644 --- a/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java +++ b/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java @@ -17,124 +17,124 @@ package io.netty.bootstrap; import io.netty.channel.Channel; +import io.netty.channel.ChannelFactory; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelInboundHandler; import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.DefaultEventLoopGroup; import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultEventLoopGroup; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; import io.netty.channel.local.LocalAddress; import io.netty.channel.local.LocalChannel; import io.netty.channel.local.LocalServerChannel; +import io.netty.resolver.NameResolver; +import io.netty.resolver.NameResolverGroup; +import io.netty.resolver.SimpleNameResolver; +import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; -import org.junit.Assert; +import io.netty.util.concurrent.Promise; +import org.junit.AfterClass; import org.junit.Test; import java.net.SocketAddress; import java.net.SocketException; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + public class BootstrapTest { + private static final EventLoopGroup groupA = new DefaultEventLoopGroup(1); + private static final EventLoopGroup groupB = new DefaultEventLoopGroup(1); + private static final ChannelInboundHandler dummyHandler = new DummyHandler(); + + @AfterClass + public static void destroy() { + groupA.shutdownGracefully(); + groupB.shutdownGracefully(); + groupA.terminationFuture().syncUninterruptibly(); + groupB.terminationFuture().syncUninterruptibly(); + } + @Test(timeout = 10000) public void testBindDeadLock() throws Exception { - EventLoopGroup groupA = new DefaultEventLoopGroup(1); - EventLoopGroup groupB = new DefaultEventLoopGroup(1); - try { - ChannelInboundHandler dummyHandler = new DummyHandler(); + final Bootstrap bootstrapA = new Bootstrap(); + bootstrapA.group(groupA); + bootstrapA.channel(LocalChannel.class); + bootstrapA.handler(dummyHandler); - final Bootstrap bootstrapA = new Bootstrap(); - bootstrapA.group(groupA); - bootstrapA.channel(LocalChannel.class); - bootstrapA.handler(dummyHandler); + final Bootstrap bootstrapB = new Bootstrap(); + bootstrapB.group(groupB); + bootstrapB.channel(LocalChannel.class); + bootstrapB.handler(dummyHandler); - final Bootstrap bootstrapB = new Bootstrap(); - bootstrapB.group(groupB); - bootstrapB.channel(LocalChannel.class); - bootstrapB.handler(dummyHandler); + List> bindFutures = new ArrayList>(); - List> bindFutures = new ArrayList>(); + // Try to bind from each other. + for (int i = 0; i < 1024; i ++) { + bindFutures.add(groupA.next().submit(new Runnable() { + @Override + public void run() { + bootstrapB.bind(LocalAddress.ANY); + } + })); - // Try to bind from each other. - for (int i = 0; i < 1024; i ++) { - bindFutures.add(groupA.next().submit(new Runnable() { - @Override - public void run() { - bootstrapB.bind(LocalAddress.ANY); - } - })); + bindFutures.add(groupB.next().submit(new Runnable() { + @Override + public void run() { + bootstrapA.bind(LocalAddress.ANY); + } + })); + } - bindFutures.add(groupB.next().submit(new Runnable() { - @Override - public void run() { - bootstrapA.bind(LocalAddress.ANY); - } - })); - } - - for (Future f: bindFutures) { - f.sync(); - } - } finally { - groupA.shutdownGracefully(); - groupB.shutdownGracefully(); - groupA.terminationFuture().sync(); - groupB.terminationFuture().sync(); + for (Future f: bindFutures) { + f.sync(); } } @Test(timeout = 10000) public void testConnectDeadLock() throws Exception { - EventLoopGroup groupA = new DefaultEventLoopGroup(1); - EventLoopGroup groupB = new DefaultEventLoopGroup(1); - try { - ChannelInboundHandler dummyHandler = new DummyHandler(); + final Bootstrap bootstrapA = new Bootstrap(); + bootstrapA.group(groupA); + bootstrapA.channel(LocalChannel.class); + bootstrapA.handler(dummyHandler); - final Bootstrap bootstrapA = new Bootstrap(); - bootstrapA.group(groupA); - bootstrapA.channel(LocalChannel.class); - bootstrapA.handler(dummyHandler); + final Bootstrap bootstrapB = new Bootstrap(); + bootstrapB.group(groupB); + bootstrapB.channel(LocalChannel.class); + bootstrapB.handler(dummyHandler); - final Bootstrap bootstrapB = new Bootstrap(); - bootstrapB.group(groupB); - bootstrapB.channel(LocalChannel.class); - bootstrapB.handler(dummyHandler); + List> bindFutures = new ArrayList>(); - List> bindFutures = new ArrayList>(); + // Try to connect from each other. + for (int i = 0; i < 1024; i ++) { + bindFutures.add(groupA.next().submit(new Runnable() { + @Override + public void run() { + bootstrapB.connect(LocalAddress.ANY); + } + })); - // Try to connect from each other. - for (int i = 0; i < 1024; i ++) { - bindFutures.add(groupA.next().submit(new Runnable() { - @Override - public void run() { - bootstrapB.connect(LocalAddress.ANY); - } - })); + bindFutures.add(groupB.next().submit(new Runnable() { + @Override + public void run() { + bootstrapA.connect(LocalAddress.ANY); + } + })); + } - bindFutures.add(groupB.next().submit(new Runnable() { - @Override - public void run() { - bootstrapA.connect(LocalAddress.ANY); - } - })); - } - - for (Future f: bindFutures) { - f.sync(); - } - } finally { - groupA.shutdownGracefully(); - groupB.shutdownGracefully(); - groupA.terminationFuture().sync(); - groupB.terminationFuture().sync(); + for (Future f: bindFutures) { + f.sync(); } } @@ -148,7 +148,7 @@ public class BootstrapTest { bootstrap.childHandler(new DummyHandler()); bootstrap.localAddress(new LocalAddress("1")); ChannelFuture future = bootstrap.bind(); - Assert.assertFalse(future.isDone()); + assertFalse(future.isDone()); group.promise.setSuccess(); final BlockingQueue queue = new LinkedBlockingQueue(); future.addListener(new ChannelFutureListener() { @@ -158,8 +158,8 @@ public class BootstrapTest { queue.add(future.isSuccess()); } }); - Assert.assertTrue(queue.take()); - Assert.assertTrue(queue.take()); + assertTrue(queue.take()); + assertTrue(queue.take()); } finally { group.shutdownGracefully(); group.terminationFuture().sync(); @@ -197,7 +197,7 @@ public class BootstrapTest { bootstrap.childHandler(new DummyHandler()); bootstrap.localAddress(new LocalAddress("1")); ChannelFuture future = bootstrap.bind(); - Assert.assertFalse(future.isDone()); + assertFalse(future.isDone()); group.promise.setSuccess(); final BlockingQueue queue = new LinkedBlockingQueue(); future.addListener(new ChannelFutureListener() { @@ -207,16 +207,61 @@ public class BootstrapTest { queue.add(future.isSuccess()); } }); - Assert.assertTrue(queue.take()); - Assert.assertFalse(queue.take()); + assertTrue(queue.take()); + assertFalse(queue.take()); } finally { group.shutdownGracefully(); group.terminationFuture().sync(); } } + @Test + public void testAsyncResolutionSuccess() throws Exception { + + final Bootstrap bootstrapA = new Bootstrap(); + bootstrapA.group(groupA); + bootstrapA.channel(LocalChannel.class); + bootstrapA.resolver(new TestNameResolverGroup(true)); + bootstrapA.handler(dummyHandler); + + final ServerBootstrap bootstrapB = new ServerBootstrap(); + bootstrapB.group(groupB); + bootstrapB.channel(LocalServerChannel.class); + bootstrapB.childHandler(dummyHandler); + SocketAddress localAddress = bootstrapB.bind(LocalAddress.ANY).sync().channel().localAddress(); + + // Connect to the server using the asynchronous resolver. + bootstrapA.connect(localAddress).sync(); + } + + @Test + public void testAsyncResolutionFailure() throws Exception { + + final Bootstrap bootstrapA = new Bootstrap(); + bootstrapA.group(groupA); + bootstrapA.channel(LocalChannel.class); + bootstrapA.resolver(new TestNameResolverGroup(false)); + bootstrapA.handler(dummyHandler); + + final ServerBootstrap bootstrapB = new ServerBootstrap(); + bootstrapB.group(groupB); + bootstrapB.channel(LocalServerChannel.class); + bootstrapB.childHandler(dummyHandler); + SocketAddress localAddress = bootstrapB.bind(LocalAddress.ANY).sync().channel().localAddress(); + + // Connect to the server using the asynchronous resolver. + ChannelFuture connectFuture = bootstrapA.connect(localAddress); + + // Should fail with the UnknownHostException. + assertThat(connectFuture.await(10000), is(true)); + assertThat(connectFuture.cause(), is(instanceOf(UnknownHostException.class))); + assertThat(connectFuture.channel().isOpen(), is(false)); + } + private static final class TestEventLoopGroup extends DefaultEventLoopGroup { + ChannelPromise promise; + TestEventLoopGroup() { super(1); } @@ -236,4 +281,39 @@ public class BootstrapTest { @Sharable private static final class DummyHandler extends ChannelInboundHandlerAdapter { } + + private static final class TestNameResolverGroup extends NameResolverGroup { + + private final boolean success; + + TestNameResolverGroup(boolean success) { + this.success = success; + } + + @Override + protected NameResolver newResolver(EventExecutor executor) throws Exception { + return new SimpleNameResolver(executor) { + + @Override + protected boolean doIsResolved(SocketAddress address) { + return false; + } + + @Override + protected void doResolve( + final SocketAddress unresolvedAddress, final Promise promise) { + executor().execute(new Runnable() { + @Override + public void run() { + if (success) { + promise.setSuccess(unresolvedAddress); + } else { + promise.setFailure(new UnknownHostException()); + } + } + }); + } + }; + } + } }