diff --git a/src/dorkbox/network/DnsClient.java b/src/dorkbox/network/DnsClient.java index ca2a827d..714f035f 100644 --- a/src/dorkbox/network/DnsClient.java +++ b/src/dorkbox/network/DnsClient.java @@ -15,6 +15,10 @@ */ package dorkbox.network; +import static io.netty.resolver.dns.DnsServerAddressStreamProviders.platformDefault; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.intValue; + import java.net.IDN; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -46,6 +50,7 @@ import dorkbox.util.OS; import dorkbox.util.Property; import io.netty.buffer.ByteBuf; import io.netty.channel.AddressedEnvelope; +import io.netty.channel.ChannelFactory; import io.netty.channel.EventLoopGroup; import io.netty.channel.ReflectiveChannelFactory; import io.netty.channel.epoll.EpollDatagramChannel; @@ -53,6 +58,7 @@ import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.oio.OioEventLoopGroup; import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.InternetProtocolFamily; import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.oio.OioDatagramChannel; import io.netty.handler.codec.dns.DefaultDnsQuestion; @@ -63,19 +69,24 @@ import io.netty.handler.codec.dns.DnsRecordType; import io.netty.handler.codec.dns.DnsResponse; import io.netty.handler.codec.dns.DnsResponseCode; import io.netty.handler.codec.dns.DnsSection; +import io.netty.resolver.HostsFileEntriesResolver; import io.netty.resolver.ResolvedAddressTypes; +import io.netty.resolver.dns.DefaultDnsCache; import io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider; +import io.netty.resolver.dns.DnsCache; import io.netty.resolver.dns.DnsNameResolver; -import io.netty.resolver.dns.DnsNameResolverBuilder; +import io.netty.resolver.dns.DnsNameResolverAccess; +import io.netty.resolver.dns.DnsQueryLifecycleObserverFactory; import io.netty.resolver.dns.DnsServerAddressStreamProvider; +import io.netty.resolver.dns.NoopDnsQueryLifecycleObserverFactory; import io.netty.resolver.dns.SequentialDnsServerAddressStreamProvider; import io.netty.util.concurrent.Future; import io.netty.util.internal.PlatformDependent; /** - * for now, we only support ipv4 + * A DnsClient for resolving DNS name, with reasonably good defaults. */ -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "WeakerAccess"}) public class DnsClient { @@ -83,15 +94,18 @@ class DnsClient { static { //noinspection Duplicates try { - // doesn't work when running from inside eclipse. - // Needed for NIO selectors on Android 2.2, and to force IPv4. - System.setProperty("java.net.preferIPv4Stack", Boolean.TRUE.toString()); - System.setProperty("java.net.preferIPv6Addresses", Boolean.FALSE.toString()); + if (PlatformDependent.isAndroid()) { + // doesn't work when running from inside eclipse. + // Needed for NIO selectors on Android 2.2, and to force IPv4. + System.setProperty("java.net.preferIPv4Stack", Boolean.TRUE.toString()); + System.setProperty("java.net.preferIPv6Addresses", Boolean.FALSE.toString()); + } // java6 has stack overflow problems when loading certain classes in it's classloader. The result is a StackOverflow when - // loading them normally + // loading them normally. This calls AND FIXES this issue. if (OS.javaVersion == 6) { if (PlatformDependent.hasUnsafe()) { + //noinspection ResultOfMethodCallIgnored PlatformDependent.newFixedMpscQueue(8); } } @@ -104,7 +118,8 @@ class DnsClient { */ // @formatter:off @Property - public static List DNS_SERVER_LIST = Arrays.asList( + public static + List DNS_SERVER_LIST = 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 @@ -151,13 +166,51 @@ class DnsClient { } private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); - private final DnsNameResolver resolver; + private Class channelType; + private DnsNameResolver resolver; private final Map> customDecoders = new HashMap>(); + private ThreadGroup threadGroup; - public static final String THREAD_NAME = "DnsClient"; + private static final String THREAD_NAME = "DnsClient"; + private EventLoopGroup eventLoopGroup; + private ChannelFactory channelFactory; + + private DnsCache resolveCache; + private DnsCache authoritativeDnsServerCache; + + private Integer minTtl; + private Integer maxTtl; + private Integer negativeTtl; + private long queryTimeoutMillis = 5000; + + private ResolvedAddressTypes resolvedAddressTypes = DnsNameResolverAccess.getDefaultResolvedAddressTypes(); + private boolean recursionDesired = true; + private int maxQueriesPerResolve = 16; + + private boolean traceEnabled; + private int maxPayloadSize = 4096; + + private boolean optResourceEnabled = true; + + private HostsFileEntriesResolver hostsFileEntriesResolver = HostsFileEntriesResolver.DEFAULT; + private DnsServerAddressStreamProvider dnsServerAddressStreamProvider = platformDefault(); + private DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory = NoopDnsQueryLifecycleObserverFactory.INSTANCE; + + private String[] searchDomains; + private int ndots = -1; + private boolean decodeIdn = true; + + /** + * Creates a new DNS client, with default name server addresses. + */ + public + DnsClient() { + this(DnsClient.DNS_SERVER_LIST); + } + /** * Creates a new DNS client, using the provided server (default port 53) for DNS query resolution, with a cache that will obey the TTL of the response * @@ -178,16 +231,6 @@ class DnsClient { this(Collections.singletonList(nameServerAddresses)); } - /** - * Creates a new DNS client, with a cache that will obey the TTL of the response - * - * @param nameServerAddresses the list of servers to receive your DNS questions, until it succeeds - */ - public - DnsClient(Collection nameServerAddresses) { - this(nameServerAddresses, 0, Integer.MAX_VALUE); - } - /** * Creates a new DNS client. * @@ -195,13 +238,9 @@ class DnsClient { * respect the TTL from the DNS server. * * @param nameServerAddresses the list of servers to receive your DNS questions, until it succeeds - * @param minTtl the minimum TTL - * @param maxTtl the maximum TTL */ public - DnsClient(Collection nameServerAddresses, int minTtl, int maxTtl) { - Class channelType; - + DnsClient(Collection nameServerAddresses) { // setup the thread group to easily ID what the following threads belong to (and their spawned threads...) SecurityManager s = System.getSecurityManager(); threadGroup = new ThreadGroup(s != null @@ -225,20 +264,7 @@ class DnsClient { channelType = NioDatagramChannel.class; } - - List list = new ArrayList(nameServerAddresses.size()); - - DnsNameResolverBuilder builder = new DnsNameResolverBuilder(eventLoopGroup.next()); - builder.channelFactory(new ReflectiveChannelFactory(channelType)) - .channelType(channelType) - .ttl(minTtl, maxTtl) - .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) // for now, we only support ipv4 - .nameServerProvider(new SequentialDnsServerAddressStreamProvider(nameServerAddresses)); - - resolver = builder.build(); - - - // A/AAAA use the built-in decoder + // NOTE: A/AAAA use the built-in decoder customDecoders.put(DnsRecordType.MX, new MailExchangerDecoder()); customDecoders.put(DnsRecordType.TXT, new TextDecoder()); @@ -249,6 +275,330 @@ class DnsClient { customDecoders.put(DnsRecordType.CNAME, decoder); customDecoders.put(DnsRecordType.PTR, decoder); customDecoders.put(DnsRecordType.SOA, new StartOfAuthorityDecoder()); + + if (nameServerAddresses != null) { + this.dnsServerAddressStreamProvider = new SequentialDnsServerAddressStreamProvider(nameServerAddresses); + } + } + + /** + * Sets the cache for resolution results. + * + * @param resolveCache the DNS resolution results cache + * + * @return {@code this} + */ + public + DnsClient resolveCache(DnsCache resolveCache) { + this.resolveCache = resolveCache; + return this; + } + + /** + * Set the factory used to generate objects which can observe individual DNS queries. + * + * @param lifecycleObserverFactory the factory used to generate objects which can observe individual DNS queries. + * + * @return {@code this} + */ + public + DnsClient dnsQueryLifecycleObserverFactory(DnsQueryLifecycleObserverFactory lifecycleObserverFactory) { + this.dnsQueryLifecycleObserverFactory = checkNotNull(lifecycleObserverFactory, "lifecycleObserverFactory"); + return this; + } + + /** + * Sets the cache for authoritative NS servers + * + * @param authoritativeDnsServerCache the authoritative NS servers cache + * + * @return {@code this} + */ + public + DnsClient authoritativeDnsServerCache(DnsCache authoritativeDnsServerCache) { + this.authoritativeDnsServerCache = authoritativeDnsServerCache; + return this; + } + + /** + * Sets the minimum and maximum TTL of the cached DNS resource records (in seconds). If the TTL of the DNS + * resource record returned by the DNS server is less than the minimum TTL or greater than the maximum TTL, + * this resolver will ignore the TTL from the DNS server and use the minimum TTL or the maximum TTL instead + * respectively. + * The default value is {@code 0} and {@link Integer#MAX_VALUE}, which practically tells this resolver to + * respect the TTL from the DNS server. + * + * @param minTtl the minimum TTL + * @param maxTtl the maximum TTL + * + * @return {@code this} + */ + public + DnsClient ttl(int minTtl, int maxTtl) { + this.maxTtl = maxTtl; + this.minTtl = minTtl; + return this; + } + + /** + * Sets the TTL of the cache for the failed DNS queries (in seconds). + * + * @param negativeTtl the TTL for failed cached queries + * + * @return {@code this} + */ + public + DnsClient negativeTtl(int negativeTtl) { + this.negativeTtl = negativeTtl; + return this; + } + + /** + * Sets the timeout of each DNS query performed by this resolver (in milliseconds). + * + * @param queryTimeoutMillis the query timeout + * + * @return {@code this} + */ + public + DnsClient queryTimeoutMillis(long queryTimeoutMillis) { + this.queryTimeoutMillis = queryTimeoutMillis; + return this; + } + + /** + * Sets the list of the protocol families of the address resolved. + * You can use {@link DnsClient#computeResolvedAddressTypes(InternetProtocolFamily...)} + * to get a {@link ResolvedAddressTypes} out of some {@link InternetProtocolFamily}s. + * + * @param resolvedAddressTypes the address types + * + * @return {@code this} + */ + public + DnsClient resolvedAddressTypes(ResolvedAddressTypes resolvedAddressTypes) { + this.resolvedAddressTypes = resolvedAddressTypes; + return this; + } + + /** + * Sets if this resolver has to send a DNS query with the RD (recursion desired) flag set. + * + * @param recursionDesired true if recursion is desired + * + * @return {@code this} + */ + public + DnsClient recursionDesired(boolean recursionDesired) { + this.recursionDesired = recursionDesired; + return this; + } + + /** + * Sets the maximum allowed number of DNS queries to send when resolving a host name. + * + * @param maxQueriesPerResolve the max number of queries + * + * @return {@code this} + */ + public + DnsClient maxQueriesPerResolve(int maxQueriesPerResolve) { + this.maxQueriesPerResolve = maxQueriesPerResolve; + return this; + } + + /** + * Sets if this resolver should generate the detailed trace information in an exception message so that + * it is easier to understand the cause of resolution failure. + * + * @param traceEnabled true if trace is enabled + * + * @return {@code this} + */ + public + DnsClient traceEnabled(boolean traceEnabled) { + this.traceEnabled = traceEnabled; + return this; + } + + /** + * Sets the capacity of the datagram packet buffer (in bytes). The default value is {@code 4096} bytes. + * + * @param maxPayloadSize the capacity of the datagram packet buffer + * + * @return {@code this} + */ + public + DnsClient maxPayloadSize(int maxPayloadSize) { + this.maxPayloadSize = maxPayloadSize; + return this; + } + + /** + * Enable the automatic inclusion of a optional records that tries to give the remote DNS server a hint about + * how much data the resolver can read per response. Some DNSServer may not support this and so fail to answer + * queries. If you find problems you may want to disable this. + * + * @param optResourceEnabled if optional records inclusion is enabled + * + * @return {@code this} + */ + public + DnsClient optResourceEnabled(boolean optResourceEnabled) { + this.optResourceEnabled = optResourceEnabled; + return this; + } + + /** + * @param hostsFileEntriesResolver the {@link HostsFileEntriesResolver} used to first check + * if the hostname is locally aliased. + * + * @return {@code this} + */ + public + DnsClient hostsFileEntriesResolver(HostsFileEntriesResolver hostsFileEntriesResolver) { + this.hostsFileEntriesResolver = hostsFileEntriesResolver; + return this; + } + + /** + * Set the {@link DnsServerAddressStreamProvider} which is used to determine which DNS server is used to resolve + * each hostname. + * + * @return {@code this} + */ + public + DnsClient nameServerProvider(DnsServerAddressStreamProvider dnsServerAddressStreamProvider) { + this.dnsServerAddressStreamProvider = checkNotNull(dnsServerAddressStreamProvider, "dnsServerAddressStreamProvider"); + return this; + } + + /** + * Set the list of search domains of the resolver. + * + * @param searchDomains the search domains + * + * @return {@code this} + */ + public + DnsClient searchDomains(Iterable searchDomains) { + checkNotNull(searchDomains, "searchDomains"); + + final List list = new ArrayList(4); + + for (String f : searchDomains) { + if (f == null) { + break; + } + + // Avoid duplicate entries. + if (list.contains(f)) { + continue; + } + + list.add(f); + } + + this.searchDomains = list.toArray(new String[list.size()]); + return this; + } + + /** + * Set the number of dots which must appear in a name before an initial absolute query is made. + * The default value is {@code 1}. + * + * @param ndots the ndots value + * + * @return {@code this} + */ + public + DnsClient ndots(int ndots) { + this.ndots = ndots; + return this; + } + + private + DnsCache newCache() { + return new DefaultDnsCache(intValue(minTtl, 0), intValue(maxTtl, Integer.MAX_VALUE), intValue(negativeTtl, 0)); + } + + /** + * Set if domain / host names should be decoded to unicode when received. + * See rfc3492. + * + * @param decodeIdn if should get decoded + * + * @return {@code this} + */ + public + DnsClient decodeToUnicode(boolean decodeIdn) { + this.decodeIdn = decodeIdn; + return this; + } + + + /** + * Compute a {@link ResolvedAddressTypes} from some {@link InternetProtocolFamily}s. + * An empty input will return the default value, based on "java.net" System properties. + * Valid inputs are (), (IPv4), (IPv6), (Ipv4, IPv6) and (IPv6, IPv4). + * + * @param internetProtocolFamilies a valid sequence of {@link InternetProtocolFamily}s + * + * @return a {@link ResolvedAddressTypes} + */ + public static + ResolvedAddressTypes computeResolvedAddressTypes(InternetProtocolFamily... internetProtocolFamilies) { + if (internetProtocolFamilies == null || internetProtocolFamilies.length == 0) { + return DnsNameResolverAccess.getDefaultResolvedAddressTypes(); + } + if (internetProtocolFamilies.length > 2) { + throw new IllegalArgumentException("No more than 2 InternetProtocolFamilies"); + } + + switch (internetProtocolFamilies[0]) { + case IPv4: + return (internetProtocolFamilies.length >= 2 && internetProtocolFamilies[1] == InternetProtocolFamily.IPv6) + ? ResolvedAddressTypes.IPV4_PREFERRED + : ResolvedAddressTypes.IPV4_ONLY; + case IPv6: + return (internetProtocolFamilies.length >= 2 && internetProtocolFamilies[1] == InternetProtocolFamily.IPv4) + ? ResolvedAddressTypes.IPV6_PREFERRED + : ResolvedAddressTypes.IPV6_ONLY; + default: + throw new IllegalArgumentException("Couldn't resolve ResolvedAddressTypes from InternetProtocolFamily array"); + } + } + + + /** + * Starts the DNS Name Resolver for the client, which will resolve DNS queries. + */ + public + DnsClient start() { + ReflectiveChannelFactory channelFactory = new ReflectiveChannelFactory(channelType); + + // default support is IPV4 + if (this.resolvedAddressTypes == null) { + this.resolvedAddressTypes = ResolvedAddressTypes.IPV4_ONLY; + } + + if (resolveCache != null && (minTtl != null || maxTtl != null || negativeTtl != null)) { + throw new IllegalStateException("resolveCache and TTLs are mutually exclusive"); + } + + if (authoritativeDnsServerCache != null && (minTtl != null || maxTtl != null || negativeTtl != null)) { + throw new IllegalStateException("authoritativeDnsServerCache and TTLs are mutually exclusive"); + } + + DnsCache resolveCache = this.resolveCache != null ? this.resolveCache : newCache(); + DnsCache authoritativeDnsServerCache = this.authoritativeDnsServerCache != null ? this.authoritativeDnsServerCache : newCache(); + + resolver = new DnsNameResolver(eventLoopGroup.next(), channelFactory, resolveCache, authoritativeDnsServerCache, + dnsQueryLifecycleObserverFactory, queryTimeoutMillis, resolvedAddressTypes, recursionDesired, + maxQueriesPerResolve, traceEnabled, maxPayloadSize, optResourceEnabled, hostsFileEntriesResolver, + dnsServerAddressStreamProvider, searchDomains, ndots, decodeIdn); + + return this; } /** @@ -258,6 +608,10 @@ class DnsClient { */ public String resolve(String hostname) { + if (resolver == null) { + start(); + } + if (resolver.resolvedAddressTypes() == ResolvedAddressTypes.IPV4_ONLY) { return resolve(hostname, DnsRecordType.A); } @@ -279,6 +633,10 @@ class DnsClient { @SuppressWarnings("unchecked") private T resolve(String hostname, DnsRecordType type) { + if (resolver == null) { + start(); + } + hostname = IDN.toASCII(hostname); final int value = type.intValue(); @@ -368,6 +726,15 @@ class DnsClient { */ public void reset() { + if (resolver == null) { + start(); + } + + clearResolver(); + } + + private + void clearResolver() { resolver.resolveCache() .clear(); } @@ -403,8 +770,11 @@ class DnsClient { private void stopInThread() { - reset(); - resolver.close(); // also closes the UDP channel that DNS client uses + clearResolver(); + + if (resolver != null) { + resolver.close(); // also closes the UDP channel that DNS client uses + } // Sometimes there might be "lingering" connections (ie, halfway though registration) that need to be closed. long maxShutdownWaitTimeInMilliSeconds = EndPoint.maxShutdownWaitTimeInMilliSeconds; diff --git a/src/io/netty/resolver/dns/DnsNameResolver.java b/src/io/netty/resolver/dns/DnsNameResolver.java new file mode 100644 index 00000000..f6ba4171 --- /dev/null +++ b/src/io/netty/resolver/dns/DnsNameResolver.java @@ -0,0 +1,981 @@ +/* + * 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 static io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.DNS_PORT; +import static io.netty.resolver.dns.UnixResolverDnsServerAddressStreamProvider.parseEtcResolverFirstNdots; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositive; + +import java.lang.reflect.Method; +import java.net.IDN; +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 io.netty.bootstrap.Bootstrap; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.Channel; +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.ChannelOption; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoop; +import io.netty.channel.FixedRecvByteBufAllocator; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.handler.codec.dns.DatagramDnsQueryEncoder; +import io.netty.handler.codec.dns.DatagramDnsResponse; +import io.netty.handler.codec.dns.DatagramDnsResponseDecoder; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsRawRecord; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.handler.codec.dns.DnsRecordType; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.resolver.HostsFileEntriesResolver; +import io.netty.resolver.InetNameResolver; +import io.netty.resolver.ResolvedAddressTypes; +import io.netty.util.NetUtil; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.FastThreadLocal; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.UnstableApi; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +/** + * A DNS-based {@link InetNameResolver} + */ +@SuppressWarnings("unused") +@UnstableApi +public class DnsNameResolver extends InetNameResolver { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsNameResolver.class); + private static final String LOCALHOST = "localhost"; + private static final InetAddress LOCALHOST_ADDRESS; + private static final DnsRecord[] EMPTY_ADDITIONALS = new DnsRecord[0]; + + private static final DnsRecordType[] IPV4_ONLY_RESOLVED_RECORD_TYPES = {DnsRecordType.A}; + private static final InternetProtocolFamily[] IPV4_ONLY_RESOLVED_PROTOCOL_FAMILIES = {InternetProtocolFamily.IPv4}; + private static final DnsRecordType[] IPV4_PREFERRED_RESOLVED_RECORD_TYPES = {DnsRecordType.A, DnsRecordType.AAAA}; + private static final InternetProtocolFamily[] IPV4_PREFERRED_RESOLVED_PROTOCOL_FAMILIES = {InternetProtocolFamily.IPv4, + InternetProtocolFamily.IPv6}; + + private static final DnsRecordType[] IPV6_ONLY_RESOLVED_RECORD_TYPES = {DnsRecordType.AAAA}; + private static final InternetProtocolFamily[] IPV6_ONLY_RESOLVED_PROTOCOL_FAMILIES = {InternetProtocolFamily.IPv6}; + private static final DnsRecordType[] IPV6_PREFERRED_RESOLVED_RECORD_TYPES = {DnsRecordType.AAAA, DnsRecordType.A}; + private static final InternetProtocolFamily[] IPV6_PREFERRED_RESOLVED_PROTOCOL_FAMILIES = {InternetProtocolFamily.IPv6, + InternetProtocolFamily.IPv4}; + + static final ResolvedAddressTypes DEFAULT_RESOLVE_ADDRESS_TYPES; + static final String[] DEFAULT_SEARCH_DOMAINS; + private static final int DEFAULT_NDOTS; + + static { + if (NetUtil.isIpV4StackPreferred()) { + DEFAULT_RESOLVE_ADDRESS_TYPES = ResolvedAddressTypes.IPV4_ONLY; + LOCALHOST_ADDRESS = NetUtil.LOCALHOST4; + } + else { + if (NetUtil.isIpV6AddressesPreferred()) { + DEFAULT_RESOLVE_ADDRESS_TYPES = ResolvedAddressTypes.IPV6_PREFERRED; + LOCALHOST_ADDRESS = NetUtil.LOCALHOST6; + } + else { + DEFAULT_RESOLVE_ADDRESS_TYPES = ResolvedAddressTypes.IPV4_PREFERRED; + LOCALHOST_ADDRESS = NetUtil.LOCALHOST4; + } + } + } + + static { + String[] searchDomains; + try { + Class configClass = Class.forName("sun.net.dns.ResolverConfiguration"); + Method open = configClass.getMethod("open"); + Method nameservers = configClass.getMethod("searchlist"); + Object instance = open.invoke(null); + + @SuppressWarnings("unchecked") + List list = (List) nameservers.invoke(instance); + searchDomains = list.toArray(new String[list.size()]); + } catch (Exception ignore) { + // Failed to get the system name search domain list. + searchDomains = EmptyArrays.EMPTY_STRINGS; + } + DEFAULT_SEARCH_DOMAINS = searchDomains; + + int ndots; + try { + ndots = parseEtcResolverFirstNdots(); + } catch (Exception ignore) { + ndots = UnixResolverDnsServerAddressStreamProvider.DEFAULT_NDOTS; + } + DEFAULT_NDOTS = ndots; + } + + private static final DatagramDnsResponseDecoder DECODER = new DatagramDnsResponseDecoder(); + private static final DatagramDnsQueryEncoder ENCODER = new DatagramDnsQueryEncoder(); + + final Future channelFuture; + final DatagramChannel ch; + + /** + * Manages the {@link DnsQueryContext}s in progress and their query IDs. + */ + final DnsQueryContextManager queryContextManager = new DnsQueryContextManager(); + + /** + * Cache for {@link #doResolve(String, Promise)} and {@link #doResolveAll(String, Promise)}. + */ + private final DnsCache resolveCache; + private final DnsCache authoritativeDnsServerCache; + + private final FastThreadLocal nameServerAddrStream = new FastThreadLocal() { + @Override + protected + DnsServerAddressStream initialValue() throws Exception { + return dnsServerAddressStreamProvider.nameServerAddressStream(""); + } + }; + + + private final long queryTimeoutMillis; + private final int maxQueriesPerResolve; + + private final ResolvedAddressTypes resolvedAddressTypes; + private final InternetProtocolFamily[] resolvedInternetProtocolFamilies; + + private final boolean recursionDesired; + private final int maxPayloadSize; + private final boolean optResourceEnabled; + + private final HostsFileEntriesResolver hostsFileEntriesResolver; + private final DnsServerAddressStreamProvider dnsServerAddressStreamProvider; + private final String[] searchDomains; + private final int ndots; + + private final boolean supportsAAAARecords; + + private final boolean supportsARecords; + private final InternetProtocolFamily preferredAddressType; + private final DnsRecordType[] resolveRecordTypes; + + private final boolean decodeIdn; + private final DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory; + + /** + * 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 resolveCache the DNS resolved entries cache + * @param authoritativeDnsServerCache the cache used to find the authoritative DNS server for a domain + * @param dnsQueryLifecycleObserverFactory used to generate new instances of {@link DnsQueryLifecycleObserver} which + * can be used to track metrics for DNS servers. + * @param queryTimeoutMillis timeout of each DNS query in millis + * @param resolvedAddressTypes the preferred address types + * @param recursionDesired if recursion desired flag must be set + * @param maxQueriesPerResolve the maximum allowed number of DNS queries for a given name resolution + * @param traceEnabled if trace is enabled + * @param maxPayloadSize the capacity of the datagram packet buffer + * @param optResourceEnabled if automatic inclusion of a optional records is enabled + * @param hostsFileEntriesResolver the {@link HostsFileEntriesResolver} used to check for local aliases + * @param dnsServerAddressStreamProvider The {@link DnsServerAddressStreamProvider} used to determine the name + * servers for each hostname lookup. + * @param searchDomains the list of search domain + * (can be null, if so, will try to default to the underlying platform ones) + * @param ndots the ndots value + * @param decodeIdn {@code true} if domain / host names should be decoded to unicode when received. + * See rfc3492. + */ + public + DnsNameResolver(EventLoop eventLoop, + ChannelFactory channelFactory, + final DnsCache resolveCache, + DnsCache authoritativeDnsServerCache, + DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory, + long queryTimeoutMillis, + ResolvedAddressTypes resolvedAddressTypes, + boolean recursionDesired, + int maxQueriesPerResolve, + boolean traceEnabled, + int maxPayloadSize, + boolean optResourceEnabled, + HostsFileEntriesResolver hostsFileEntriesResolver, + DnsServerAddressStreamProvider dnsServerAddressStreamProvider, + String[] searchDomains, + int ndots, + boolean decodeIdn) { + super(eventLoop); + + this.queryTimeoutMillis = checkPositive(queryTimeoutMillis, "queryTimeoutMillis"); + this.resolvedAddressTypes = resolvedAddressTypes != null ? resolvedAddressTypes : DEFAULT_RESOLVE_ADDRESS_TYPES; + this.recursionDesired = recursionDesired; + this.maxQueriesPerResolve = checkPositive(maxQueriesPerResolve, "maxQueriesPerResolve"); + this.maxPayloadSize = checkPositive(maxPayloadSize, "maxPayloadSize"); + this.optResourceEnabled = optResourceEnabled; + this.hostsFileEntriesResolver = checkNotNull(hostsFileEntriesResolver, "hostsFileEntriesResolver"); + this.dnsServerAddressStreamProvider = checkNotNull(dnsServerAddressStreamProvider, "dnsServerAddressStreamProvider"); + this.resolveCache = checkNotNull(resolveCache, "resolveCache"); + this.authoritativeDnsServerCache = checkNotNull(authoritativeDnsServerCache, "authoritativeDnsServerCache"); + + if (traceEnabled) { + if (dnsQueryLifecycleObserverFactory instanceof NoopDnsQueryLifecycleObserverFactory) { + this.dnsQueryLifecycleObserverFactory = new TraceDnsQueryLifeCycleObserverFactory(); + } + else { + this.dnsQueryLifecycleObserverFactory = new BiDnsQueryLifecycleObserverFactory(new TraceDnsQueryLifeCycleObserverFactory(), + dnsQueryLifecycleObserverFactory); + } + } + else { + this.dnsQueryLifecycleObserverFactory = checkNotNull(dnsQueryLifecycleObserverFactory, "dnsQueryLifecycleObserverFactory"); + } + + + this.searchDomains = searchDomains != null ? searchDomains.clone() : DEFAULT_SEARCH_DOMAINS; + this.ndots = ndots >= 0 ? ndots : DEFAULT_NDOTS; + this.decodeIdn = decodeIdn; + + switch (this.resolvedAddressTypes) { + case IPV4_ONLY: + supportsAAAARecords = false; + supportsARecords = true; + resolveRecordTypes = IPV4_ONLY_RESOLVED_RECORD_TYPES; + resolvedInternetProtocolFamilies = IPV4_ONLY_RESOLVED_PROTOCOL_FAMILIES; + preferredAddressType = InternetProtocolFamily.IPv4; + break; + case IPV4_PREFERRED: + supportsAAAARecords = true; + supportsARecords = true; + resolveRecordTypes = IPV4_PREFERRED_RESOLVED_RECORD_TYPES; + resolvedInternetProtocolFamilies = IPV4_PREFERRED_RESOLVED_PROTOCOL_FAMILIES; + preferredAddressType = InternetProtocolFamily.IPv4; + break; + case IPV6_ONLY: + supportsAAAARecords = true; + supportsARecords = false; + resolveRecordTypes = IPV6_ONLY_RESOLVED_RECORD_TYPES; + resolvedInternetProtocolFamilies = IPV6_ONLY_RESOLVED_PROTOCOL_FAMILIES; + preferredAddressType = InternetProtocolFamily.IPv6; + break; + case IPV6_PREFERRED: + supportsAAAARecords = true; + supportsARecords = true; + resolveRecordTypes = IPV6_PREFERRED_RESOLVED_RECORD_TYPES; + resolvedInternetProtocolFamilies = IPV6_PREFERRED_RESOLVED_PROTOCOL_FAMILIES; + preferredAddressType = InternetProtocolFamily.IPv6; + break; + default: + throw new IllegalArgumentException("Unknown ResolvedAddressTypes " + resolvedAddressTypes); + } + + Bootstrap b = new Bootstrap(); + b.group(executor()); + + b.channelFactory(channelFactory); + b.option(ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION, true); + + final DnsResponseHandler responseHandler = new DnsResponseHandler(executor().newPromise()); + b.handler(new ChannelInitializer() { + @Override + protected void initChannel(DatagramChannel ch) throws Exception { + ch.pipeline().addLast(DECODER, ENCODER, responseHandler); + } + }); + + channelFuture = responseHandler.channelActivePromise; + ch = (DatagramChannel) b.register().channel(); + ch.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(maxPayloadSize)); + + ch.closeFuture().addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + resolveCache.clear(); + } + }); + } + + // Only here to override in unit tests. + int dnsRedirectPort(@SuppressWarnings("unused") InetAddress server) { + return DNS_PORT; + } + + final DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory() { + return dnsQueryLifecycleObserverFactory; + } + + /** + * Provides the opportunity to sort the name servers before following a redirected DNS query. + * @param nameServers The addresses of the DNS servers which are used in the event of a redirect. + * @return A {@link DnsServerAddressStream} which will be used to follow the DNS redirect. + */ + protected DnsServerAddressStream uncachedRedirectDnsServerStream(List nameServers) { + return DnsServerAddresses.sequential(nameServers).stream(); + } + + /** + * Returns the resolution cache. + */ + public DnsCache resolveCache() { + return resolveCache; + } + + /** + * Returns the cache used for authoritative DNS servers for a domain. + */ + public DnsCache authoritativeDnsServerCache() { + return authoritativeDnsServerCache; + } + + /** + * Returns the timeout of each DNS query performed by this resolver (in milliseconds). + * The default value is 5 seconds. + */ + public long queryTimeoutMillis() { + return queryTimeoutMillis; + } + + /** + * Returns the {@link ResolvedAddressTypes} resolved by {@link #resolve(String)}. + * The default value depends on the value of the system property {@code "java.net.preferIPv6Addresses"}. + */ + public ResolvedAddressTypes resolvedAddressTypes() { + return resolvedAddressTypes; + } + + InternetProtocolFamily[] resolvedInternetProtocolFamiliesUnsafe() { + return resolvedInternetProtocolFamilies; + } + + final String[] searchDomains() { + return searchDomains; + } + + final int ndots() { + return ndots; + } + + final boolean supportsAAAARecords() { + return supportsAAAARecords; + } + + final boolean supportsARecords() { + return supportsARecords; + } + + final InternetProtocolFamily preferredAddressType() { + return preferredAddressType; + } + + final DnsRecordType[] resolveRecordTypes() { + return resolveRecordTypes; + } + + final boolean isDecodeIdn() { + return decodeIdn; + } + + /** + * 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}. + */ + public boolean isRecursionDesired() { + return recursionDesired; + } + + /** + * Returns the maximum allowed number of DNS queries to send when resolving a host name. + * The default value is {@code 8}. + */ + public int maxQueriesPerResolve() { + return maxQueriesPerResolve; + } + + /** + * Returns the capacity of the datagram packet buffer (in bytes). The default value is {@code 4096} bytes. + */ + public int maxPayloadSize() { + return maxPayloadSize; + } + + /** + * Returns the automatic inclusion of a optional records that tries to give the remote DNS server a hint about how + * much data the resolver can read per response is enabled. + */ + public boolean isOptResourceEnabled() { + return optResourceEnabled; + } + + /** + * Returns the component that tries to resolve hostnames against the hosts file prior to asking to + * remotes DNS servers. + */ + public HostsFileEntriesResolver hostsFileEntriesResolver() { + return hostsFileEntriesResolver; + } + + /** + * 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() { + if (ch.isOpen()) { + ch.close(); + } + } + + @Override + protected EventLoop executor() { + return (EventLoop) super.executor(); + } + + private InetAddress resolveHostsFileEntry(String hostname) { + if (hostsFileEntriesResolver == null) { + return null; + } else { + InetAddress address = hostsFileEntriesResolver.address(hostname, resolvedAddressTypes); + if (address == null && PlatformDependent.isWindows() && LOCALHOST.equalsIgnoreCase(hostname)) { + // If we tried to resolve localhost we need workaround that windows removed localhost from its + // hostfile in later versions. + // See https://github.com/netty/netty/issues/5386 + return LOCALHOST_ADDRESS; + } + return address; + } + } + + /** + * Resolves the specified name into an address. + * + * @param inetHost the name to resolve + * @param additionals additional records ({@code OPT}) + * + * @return the address as the result of the resolution + */ + public final Future resolve(String inetHost, Iterable additionals) { + return resolve(inetHost, additionals, executor().newPromise()); + } + + /** + * Resolves the specified name into an address. + * + * @param inetHost the name to resolve + * @param additionals additional records ({@code OPT}) + * @param promise the {@link Promise} which will be fulfilled when the name resolution is finished + * + * @return the address as the result of the resolution + */ + public final Future resolve(String inetHost, Iterable additionals, + Promise promise) { + checkNotNull(promise, "promise"); + DnsRecord[] additionalsArray = toArray(additionals, true); + try { + doResolve(inetHost, additionalsArray, promise, resolveCache); + return promise; + } catch (Exception e) { + return promise.setFailure(e); + } + } + + /** + * Resolves the specified host name and port into a list of address. + * + * @param inetHost the name to resolve + * @param additionals additional records ({@code OPT}) + * + * @return the list of the address as the result of the resolution + */ + public final Future> resolveAll(String inetHost, Iterable additionals) { + return resolveAll(inetHost, additionals, executor().>newPromise()); + } + + /** + * Resolves the specified host name and port into a list of address. + * + * @param inetHost the name to resolve + * @param additionals additional records ({@code OPT}) + * @param promise the {@link Promise} which will be fulfilled when the name resolution is finished + * + * @return the list of the address as the result of the resolution + */ + public final Future> resolveAll(String inetHost, Iterable additionals, + Promise> promise) { + checkNotNull(promise, "promise"); + DnsRecord[] additionalsArray = toArray(additionals, true); + try { + doResolveAll(inetHost, additionalsArray, promise, resolveCache); + return promise; + } catch (Exception e) { + return promise.setFailure(e); + } + } + + @Override + protected void doResolve(String inetHost, Promise promise) throws Exception { + doResolve(inetHost, EMPTY_ADDITIONALS, promise, resolveCache); + } + + private static DnsRecord[] toArray(Iterable additionals, boolean validateType) { + checkNotNull(additionals, "additionals"); + if (additionals instanceof Collection) { + Collection records = (Collection) additionals; + for (DnsRecord r: additionals) { + validateAdditional(r, validateType); + } + return records.toArray(new DnsRecord[records.size()]); + } + + Iterator additionalsIt = additionals.iterator(); + if (!additionalsIt.hasNext()) { + return EMPTY_ADDITIONALS; + } + List records = new ArrayList(); + do { + DnsRecord r = additionalsIt.next(); + validateAdditional(r, validateType); + records.add(r); + } while (additionalsIt.hasNext()); + + return records.toArray(new DnsRecord[records.size()]); + } + + private static void validateAdditional(DnsRecord record, boolean validateType) { + checkNotNull(record, "record"); + if (validateType && record instanceof DnsRawRecord) { + throw new IllegalArgumentException("DnsRawRecord implementations not allowed: " + record); + } + } + + private InetAddress loopbackAddress() { + return preferredAddressType().localhost(); + } + + /** + * Hook designed for extensibility so one can pass a different cache on each resolution attempt + * instead of using the global one. + */ + protected void doResolve(String inetHost, + DnsRecord[] additionals, + Promise promise, + DnsCache resolveCache) throws Exception { + if (inetHost == null || inetHost.isEmpty()) { + // If an empty hostname is used we should use "localhost", just like InetAddress.getByName(...) does. + promise.setSuccess(loopbackAddress()); + return; + } + final byte[] bytes = NetUtil.createByteArrayFromIpAddressString(inetHost); + if (bytes != null) { + // The inetHost is actually an ipaddress. + promise.setSuccess(InetAddress.getByAddress(bytes)); + return; + } + + final String hostname = hostname(inetHost); + + InetAddress hostsFileEntry = resolveHostsFileEntry(hostname); + if (hostsFileEntry != null) { + promise.setSuccess(hostsFileEntry); + return; + } + + if (!doResolveCached(hostname, additionals, promise, resolveCache)) { + doResolveUncached(hostname, additionals, promise, resolveCache); + } + } + + private boolean doResolveCached(String hostname, + DnsRecord[] additionals, + Promise promise, + DnsCache resolveCache) { + final List cachedEntries = resolveCache.get(hostname, additionals); + if (cachedEntries == null || cachedEntries.isEmpty()) { + return false; + } + + InetAddress address = null; + Throwable cause = null; + synchronized (cachedEntries) { + final int numEntries = cachedEntries.size(); + assert numEntries > 0; + + if (cachedEntries.get(0).cause() != null) { + cause = cachedEntries.get(0).cause(); + } else { + // Find the first entry with the preferred address type. + for (InternetProtocolFamily f : resolvedInternetProtocolFamilies) { + for (int i = 0; i < numEntries; i++) { + final DnsCacheEntry e = cachedEntries.get(i); + if (f.addressType().isInstance(e.address())) { + address = e.address(); + break; + } + } + } + } + } + + if (address != null) { + trySuccess(promise, address); + return true; + } + if (cause != null) { + tryFailure(promise, cause); + return true; + } + return false; + } + + private static void trySuccess(Promise promise, T result) { + if (!promise.trySuccess(result)) { + logger.warn("Failed to notify success ({}) to a promise: {}", result, promise); + } + } + + private static void tryFailure(Promise promise, Throwable cause) { + if (!promise.tryFailure(cause)) { + logger.warn("Failed to notify failure to a promise: {}", promise, cause); + } + } + + private void doResolveUncached(String hostname, + DnsRecord[] additionals, + Promise promise, + DnsCache resolveCache) { + new SingleResolverContext(this, hostname, additionals, resolveCache, + dnsServerAddressStreamProvider.nameServerAddressStream(hostname)).resolve(promise); + } + + static final class SingleResolverContext extends DnsNameResolverContext { + SingleResolverContext(DnsNameResolver parent, String hostname, + DnsRecord[] additionals, DnsCache resolveCache, DnsServerAddressStream nameServerAddrs) { + super(parent, hostname, additionals, resolveCache, nameServerAddrs); + } + + @Override + DnsNameResolverContext newResolverContext(DnsNameResolver parent, String hostname, + DnsRecord[] additionals, DnsCache resolveCache, + DnsServerAddressStream nameServerAddrs) { + return new SingleResolverContext(parent, hostname, additionals, resolveCache, nameServerAddrs); + } + + @Override + boolean finishResolve( + Class addressType, List resolvedEntries, + Promise promise) { + + final int numEntries = resolvedEntries.size(); + for (int i = 0; i < numEntries; i++) { + final InetAddress a = resolvedEntries.get(i).address(); + if (addressType.isInstance(a)) { + trySuccess(promise, a); + return true; + } + } + return false; + } + } + + @Override + protected void doResolveAll(String inetHost, Promise> promise) throws Exception { + doResolveAll(inetHost, EMPTY_ADDITIONALS, promise, resolveCache); + } + + /** + * Hook designed for extensibility so one can pass a different cache on each resolution attempt + * instead of using the global one. + */ + protected void doResolveAll(String inetHost, + DnsRecord[] additionals, + Promise> promise, + DnsCache resolveCache) throws Exception { + if (inetHost == null || inetHost.isEmpty()) { + // If an empty hostname is used we should use "localhost", just like InetAddress.getAllByName(...) does. + promise.setSuccess(Collections.singletonList(loopbackAddress())); + return; + } + final byte[] bytes = NetUtil.createByteArrayFromIpAddressString(inetHost); + if (bytes != null) { + // The unresolvedAddress was created via a String that contains an ipaddress. + promise.setSuccess(Collections.singletonList(InetAddress.getByAddress(bytes))); + return; + } + + final String hostname = hostname(inetHost); + + InetAddress hostsFileEntry = resolveHostsFileEntry(hostname); + if (hostsFileEntry != null) { + promise.setSuccess(Collections.singletonList(hostsFileEntry)); + return; + } + + if (!doResolveAllCached(hostname, additionals, promise, resolveCache)) { + doResolveAllUncached(hostname, additionals, promise, resolveCache); + } + } + + private boolean doResolveAllCached(String hostname, + DnsRecord[] additionals, + Promise> promise, + DnsCache resolveCache) { + final List cachedEntries = resolveCache.get(hostname, additionals); + if (cachedEntries == null || cachedEntries.isEmpty()) { + return false; + } + + List result = null; + Throwable cause = null; + synchronized (cachedEntries) { + final int numEntries = cachedEntries.size(); + assert numEntries > 0; + + if (cachedEntries.get(0).cause() != null) { + cause = cachedEntries.get(0).cause(); + } else { + for (InternetProtocolFamily f : resolvedInternetProtocolFamilies) { + for (int i = 0; i < numEntries; i++) { + final DnsCacheEntry e = cachedEntries.get(i); + if (f.addressType().isInstance(e.address())) { + if (result == null) { + result = new ArrayList(numEntries); + } + result.add(e.address()); + } + } + } + } + } + + if (result != null) { + trySuccess(promise, result); + return true; + } + if (cause != null) { + tryFailure(promise, cause); + return true; + } + return false; + } + + static final class ListResolverContext extends DnsNameResolverContext> { + ListResolverContext(DnsNameResolver parent, String hostname, + DnsRecord[] additionals, DnsCache resolveCache, DnsServerAddressStream nameServerAddrs) { + super(parent, hostname, additionals, resolveCache, nameServerAddrs); + } + + @Override + DnsNameResolverContext> newResolverContext( + DnsNameResolver parent, String hostname, DnsRecord[] additionals, DnsCache resolveCache, + DnsServerAddressStream nameServerAddrs) { + return new ListResolverContext(parent, hostname, additionals, resolveCache, nameServerAddrs); + } + + @Override + boolean finishResolve( + Class addressType, List resolvedEntries, + Promise> promise) { + + List result = null; + final int numEntries = resolvedEntries.size(); + for (int i = 0; i < numEntries; i++) { + final InetAddress a = resolvedEntries.get(i).address(); + if (addressType.isInstance(a)) { + if (result == null) { + result = new ArrayList(numEntries); + } + result.add(a); + } + } + + if (result != null) { + promise.trySuccess(result); + return true; + } + return false; + } + } + + private void doResolveAllUncached(String hostname, + DnsRecord[] additionals, + Promise> promise, + DnsCache resolveCache) { + new ListResolverContext(this, hostname, additionals, resolveCache, + dnsServerAddressStreamProvider.nameServerAddressStream(hostname)).resolve(promise); + } + + private static String hostname(String inetHost) { + String hostname = IDN.toASCII(inetHost); + // Check for http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6894622 + if (StringUtil.endsWith(inetHost, '.') && !StringUtil.endsWith(hostname, '.')) { + hostname += "."; + } + return hostname; + } + + /** + * Sends a DNS query with the specified question. + */ + public Future> query(DnsQuestion question) { + return query(nextNameServerAddress(), question); + } + + /** + * Sends a DNS query with the specified question with additional records. + */ + public Future> query( + DnsQuestion question, Iterable additionals) { + return query(nextNameServerAddress(), question, additionals); + } + + /** + * Sends a DNS query with the specified question. + */ + public Future> query( + DnsQuestion question, Promise> promise) { + return query(nextNameServerAddress(), question, Collections.emptyList(), promise); + } + + private InetSocketAddress nextNameServerAddress() { + return nameServerAddrStream.get().next(); + } + + /** + * Sends a DNS query with the specified question using the specified name server list. + */ + public Future> query( + InetSocketAddress nameServerAddr, DnsQuestion question) { + + return query0(nameServerAddr, question, EMPTY_ADDITIONALS, + ch.eventLoop().>newPromise()); + } + + /** + * Sends a DNS query with the specified question with additional records using the specified name server list. + */ + public Future> query( + InetSocketAddress nameServerAddr, DnsQuestion question, Iterable additionals) { + + return query0(nameServerAddr, question, toArray(additionals, false), + ch.eventLoop().>newPromise()); + } + + /** + * Sends a DNS query with the specified question using the specified name server list. + */ + public Future> query( + InetSocketAddress nameServerAddr, DnsQuestion question, + Promise> promise) { + + return query0(nameServerAddr, question, EMPTY_ADDITIONALS, promise); + } + + /** + * Sends a DNS query with the specified question with additional records using the specified name server list. + */ + public Future> query( + InetSocketAddress nameServerAddr, DnsQuestion question, + Iterable additionals, + Promise> promise) { + + return query0(nameServerAddr, question, toArray(additionals, false), promise); + } + + final Future> query0( + InetSocketAddress nameServerAddr, DnsQuestion question, + DnsRecord[] additionals, + Promise> promise) { + return query0(nameServerAddr, question, additionals, ch.newPromise(), promise); + } + + final Future> query0( + InetSocketAddress nameServerAddr, DnsQuestion question, + DnsRecord[] additionals, + ChannelPromise writePromise, + Promise> promise) { + assert !writePromise.isVoid(); + + final Promise> castPromise = cast( + checkNotNull(promise, "promise")); + try { + new DnsQueryContext(this, nameServerAddr, question, additionals, castPromise).query(writePromise); + return castPromise; + } catch (Exception e) { + return castPromise.setFailure(e); + } + } + + @SuppressWarnings("unchecked") + private static Promise> cast(Promise promise) { + return (Promise>) promise; + } + + private final class DnsResponseHandler extends ChannelInboundHandlerAdapter { + + private final Promise channelActivePromise; + + DnsResponseHandler(Promise channelActivePromise) { + this.channelActivePromise = channelActivePromise; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + try { + final DatagramDnsResponse res = (DatagramDnsResponse) msg; + final int queryId = res.id(); + + if (logger.isDebugEnabled()) { + logger.debug("{} RECEIVED: [{}: {}], {}", ch, queryId, res.sender(), res); + } + + final DnsQueryContext qCtx = queryContextManager.get(res.sender(), queryId); + if (qCtx == null) { + logger.warn("{} Received a DNS response with an unknown ID: {}", ch, queryId); + return; + } + + qCtx.finish(res); + } finally { + ReferenceCountUtil.safeRelease(msg); + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + channelActivePromise.setSuccess(ctx.channel()); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + logger.warn("{} Unexpected exception: ", ch, cause); + } + } +} diff --git a/src/io/netty/resolver/dns/DnsNameResolverAccess.java b/src/io/netty/resolver/dns/DnsNameResolverAccess.java index 5a39f3cc..bd05b552 100644 --- a/src/io/netty/resolver/dns/DnsNameResolverAccess.java +++ b/src/io/netty/resolver/dns/DnsNameResolverAccess.java @@ -16,6 +16,7 @@ package io.netty.resolver.dns; import io.netty.buffer.ByteBuf; +import io.netty.resolver.ResolvedAddressTypes; public final class DnsNameResolverAccess { @@ -25,6 +26,11 @@ class DnsNameResolverAccess { return DnsNameResolverContext.decodeDomainName(byteBuff); } + public static + ResolvedAddressTypes getDefaultResolvedAddressTypes() { + return DnsNameResolver.DEFAULT_RESOLVE_ADDRESS_TYPES; + } + private DnsNameResolverAccess() { } diff --git a/test/dorkbox/network/DnsTests.java b/test/dorkbox/network/DnsTests.java index d0cf2992..719e5318 100644 --- a/test/dorkbox/network/DnsTests.java +++ b/test/dorkbox/network/DnsTests.java @@ -15,15 +15,16 @@ */ package dorkbox.network; -import dorkbox.network.dns.record.MailExchangerRecord; -import dorkbox.network.dns.record.ServiceRecord; -import dorkbox.network.dns.record.StartOfAuthorityRecord; -import org.junit.Test; +import static org.junit.Assert.fail; import java.net.UnknownHostException; import java.util.List; -import static org.junit.Assert.fail; +import org.junit.Test; + +import dorkbox.network.dns.record.MailExchangerRecord; +import dorkbox.network.dns.record.ServiceRecord; +import dorkbox.network.dns.record.StartOfAuthorityRecord; public class DnsTests { @@ -31,7 +32,7 @@ public class DnsTests { public void decode_A_Record() throws UnknownHostException { //DnsClient dnsClient = new DnsClient("127.0.1.1"); - DnsClient dnsClient = new DnsClient(DnsClient.DNS_SERVER_LIST); + DnsClient dnsClient = new DnsClient(); String answer = dnsClient.resolveA("google.com"); dnsClient.stop(); @@ -45,7 +46,7 @@ public class DnsTests { // PTR absolutely MUST end in '.in-addr.arpa' in order for the DNS server to understand it. // our DNS client will FIX THIS, so that end-users do NOT have to know this! - DnsClient dnsClient = new DnsClient(DnsClient.DNS_SERVER_LIST); + DnsClient dnsClient = new DnsClient(); String answer = dnsClient.resolvePTR("204.228.150.3"); dnsClient.stop(); @@ -56,7 +57,7 @@ public class DnsTests { @Test public void decode_CNAME_Record() { - DnsClient dnsClient = new DnsClient(DnsClient.DNS_SERVER_LIST); + DnsClient dnsClient = new DnsClient(); String answer = dnsClient.resolveCNAME("www.atmos.org"); dnsClient.stop(); @@ -67,7 +68,7 @@ public class DnsTests { @Test public void decode_MX_Record() { - DnsClient dnsClient = new DnsClient(DnsClient.DNS_SERVER_LIST); + DnsClient dnsClient = new DnsClient(); MailExchangerRecord answer = dnsClient.resolveMX("bbc.co.uk"); final String name = answer.name(); dnsClient.stop(); @@ -79,7 +80,7 @@ public class DnsTests { @Test public void decode_SRV_Record() { - DnsClient dnsClient = new DnsClient(DnsClient.DNS_SERVER_LIST); + DnsClient dnsClient = new DnsClient(); ServiceRecord answer = dnsClient.resolveSRV("_pop3._tcp.fudo.org"); final String name = answer.name(); dnsClient.stop(); @@ -91,7 +92,7 @@ public class DnsTests { @Test public void decode_SOA_Record() { - DnsClient dnsClient = new DnsClient(DnsClient.DNS_SERVER_LIST); + DnsClient dnsClient = new DnsClient(); StartOfAuthorityRecord answer = dnsClient.resolveSOA("google.com"); final String nameServer = answer.primaryNameServer(); dnsClient.stop(); @@ -104,7 +105,7 @@ public class DnsTests { @Test public void decode_TXT_Record() { - DnsClient dnsClient = new DnsClient(DnsClient.DNS_SERVER_LIST); + DnsClient dnsClient = new DnsClient(); List answer = dnsClient.resolveTXT("real-world-systems.com"); final String name = answer.get(0); dnsClient.stop();