diff --git a/src/dorkbox/network/dns/resolver/BiDnsQueryLifecycleObserver.java b/src/dorkbox/network/dns/resolver/BiDnsQueryLifecycleObserver.java new file mode 100644 index 00000000..0e2d9d16 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/BiDnsQueryLifecycleObserver.java @@ -0,0 +1,120 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import java.net.InetSocketAddress; +import java.util.List; + +import dorkbox.network.dns.records.DnsMessage; +import io.netty.channel.ChannelFuture; +import io.netty.util.internal.UnstableApi; + +/** + * Combines two {@link DnsQueryLifecycleObserver} into a single {@link DnsQueryLifecycleObserver}. + */ +@UnstableApi +public final +class BiDnsQueryLifecycleObserver implements DnsQueryLifecycleObserver { + private final DnsQueryLifecycleObserver a; + private final DnsQueryLifecycleObserver b; + + /** + * Create a new instance. + * + * @param a The {@link DnsQueryLifecycleObserver} that will receive events first. + * @param b The {@link DnsQueryLifecycleObserver} that will receive events second. + */ + public + BiDnsQueryLifecycleObserver(DnsQueryLifecycleObserver a, DnsQueryLifecycleObserver b) { + this.a = checkNotNull(a, "a"); + this.b = checkNotNull(b, "b"); + } + + @Override + public + void queryWritten(InetSocketAddress dnsServerAddress, ChannelFuture future) { + try { + a.queryWritten(dnsServerAddress, future); + } finally { + b.queryWritten(dnsServerAddress, future); + } + } + + @Override + public + void queryCancelled(int queriesRemaining) { + try { + a.queryCancelled(queriesRemaining); + } finally { + b.queryCancelled(queriesRemaining); + } + } + + @Override + public + DnsQueryLifecycleObserver queryRedirected(List nameServers) { + try { + a.queryRedirected(nameServers); + } finally { + b.queryRedirected(nameServers); + } + return this; + } + + @Override + public + DnsQueryLifecycleObserver queryCNAMEd(DnsMessage cnameQuestion) { + try { + a.queryCNAMEd(cnameQuestion); + } finally { + b.queryCNAMEd(cnameQuestion); + } + return this; + } + + @Override + public + DnsQueryLifecycleObserver queryNoAnswer(int code) { + try { + a.queryNoAnswer(code); + } finally { + b.queryNoAnswer(code); + } + return this; + } + + @Override + public + void queryFailed(Throwable cause) { + try { + a.queryFailed(cause); + } finally { + b.queryFailed(cause); + } + } + + @Override + public + void querySucceed() { + try { + a.querySucceed(); + } finally { + b.querySucceed(); + } + } +} diff --git a/src/dorkbox/network/dns/resolver/BiDnsQueryLifecycleObserverFactory.java b/src/dorkbox/network/dns/resolver/BiDnsQueryLifecycleObserverFactory.java new file mode 100644 index 00000000..a9462893 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/BiDnsQueryLifecycleObserverFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import dorkbox.network.dns.records.DnsMessage; +import io.netty.util.internal.UnstableApi; + +/** + * Combines two {@link DnsQueryLifecycleObserverFactory} into a single {@link DnsQueryLifecycleObserverFactory}. + */ +@UnstableApi +public final +class BiDnsQueryLifecycleObserverFactory implements DnsQueryLifecycleObserverFactory { + private final DnsQueryLifecycleObserverFactory a; + private final DnsQueryLifecycleObserverFactory b; + + /** + * Create a new instance. + * + * @param a The {@link DnsQueryLifecycleObserverFactory} that will receive events first. + * @param b The {@link DnsQueryLifecycleObserverFactory} that will receive events second. + */ + public + BiDnsQueryLifecycleObserverFactory(DnsQueryLifecycleObserverFactory a, DnsQueryLifecycleObserverFactory b) { + this.a = checkNotNull(a, "a"); + this.b = checkNotNull(b, "b"); + } + + @Override + public + DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsMessage question) { + return new BiDnsQueryLifecycleObserver(a.newDnsQueryLifecycleObserver(question), b.newDnsQueryLifecycleObserver(question)); + } +} diff --git a/src/io/netty/resolver/dns/DnsNameResolver.java b/src/dorkbox/network/dns/resolver/DnsNameResolver.java similarity index 53% rename from src/io/netty/resolver/dns/DnsNameResolver.java rename to src/dorkbox/network/dns/resolver/DnsNameResolver.java index f6ba4171..de2d09d8 100644 --- a/src/io/netty/resolver/dns/DnsNameResolver.java +++ b/src/dorkbox/network/dns/resolver/DnsNameResolver.java @@ -13,31 +13,39 @@ * License for the specific language governing permissions and limitations * under the License. */ -package io.netty.resolver.dns; +package dorkbox.network.dns.resolver; -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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dorkbox.network.dns.DatagramDnsQueryEncoder; +import dorkbox.network.dns.DatagramDnsResponseDecoder; +import dorkbox.network.dns.DnsQuestion; +import dorkbox.network.dns.DnsResponse; +import dorkbox.network.dns.constants.DnsRecordType; +import dorkbox.network.dns.resolver.addressProvider.DefaultDnsServerAddressStreamProvider; +import dorkbox.network.dns.resolver.addressProvider.DnsServerAddressStream; +import dorkbox.network.dns.resolver.addressProvider.DnsServerAddressStreamProvider; +import dorkbox.network.dns.resolver.addressProvider.DnsServerAddresses; +import dorkbox.network.dns.resolver.addressProvider.UnixResolverDnsServerAddressStreamProvider; +import dorkbox.network.dns.resolver.cache.DnsCache; +import dorkbox.network.dns.resolver.cache.DnsCacheEntry; 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; @@ -45,57 +53,98 @@ 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 { +public +class DnsNameResolver extends InetNameResolver { + + static final Logger logger = LoggerFactory.getLogger(DnsNameResolver.class); - 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 int[] 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 int[] 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 int[] 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 int[] 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; + public static final ResolvedAddressTypes DEFAULT_RESOLVE_ADDRESS_TYPES; static final String[] DEFAULT_SEARCH_DOMAINS; private static final int DEFAULT_NDOTS; + private static final DatagramDnsResponseDecoder DNS_DECODER = new DatagramDnsResponseDecoder(); + private final DatagramDnsQueryEncoder DNS_ENCODER; + + 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 long queryTimeoutMillis; + private final int maxQueriesPerResolve; + + private final ResolvedAddressTypes resolvedAddressTypes; + private final InternetProtocolFamily[] resolvedInternetProtocolFamilies; + + private final boolean recursionDesired; + private final int maxPayloadSize; + + private final HostsFileEntriesResolver hostsFileEntriesResolver; + private final DnsServerAddressStreamProvider dnsServerAddressStreamProvider; + + private final FastThreadLocal nameServerAddrStream = new FastThreadLocal() { + @Override + protected + DnsServerAddressStream initialValue() throws Exception { + return dnsServerAddressStreamProvider.nameServerAddressStream(""); + } + }; + + private final String[] searchDomains; + private final int ndots; + + private final boolean supportsAAAARecords; + private final boolean supportsARecords; + + private final InternetProtocolFamily preferredAddressType; + private final int[] resolveRecordTypes; + + private final boolean decodeIdn; + + private final DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory; + static { if (NetUtil.isIpV4StackPreferred()) { DEFAULT_RESOLVE_ADDRESS_TYPES = ResolvedAddressTypes.IPV4_ONLY; @@ -132,63 +181,13 @@ public class DnsNameResolver extends InetNameResolver { int ndots; try { - ndots = parseEtcResolverFirstNdots(); + ndots = UnixResolverDnsServerAddressStreamProvider.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. * @@ -197,22 +196,21 @@ public class DnsNameResolver extends InetNameResolver { * @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. + * 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. + * 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) + * (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. + * See rfc3492. */ public DnsNameResolver(EventLoop eventLoop, @@ -226,7 +224,6 @@ public class DnsNameResolver extends InetNameResolver { int maxQueriesPerResolve, boolean traceEnabled, int maxPayloadSize, - boolean optResourceEnabled, HostsFileEntriesResolver hostsFileEntriesResolver, DnsServerAddressStreamProvider dnsServerAddressStreamProvider, String[] searchDomains, @@ -239,7 +236,6 @@ public class DnsNameResolver extends InetNameResolver { 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"); @@ -302,143 +298,53 @@ public class DnsNameResolver extends InetNameResolver { b.channelFactory(channelFactory); b.option(ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION, true); - final DnsResponseHandler responseHandler = new DnsResponseHandler(executor().newPromise()); + DNS_ENCODER = new DatagramDnsQueryEncoder(maxPayloadSize); + + + Promise channelActivePromise = executor().newPromise(); + final DnsNameResolverResponseHandler responseHandler = new DnsNameResolverResponseHandler(this, channelActivePromise); b.handler(new ChannelInitializer() { @Override - protected void initChannel(DatagramChannel ch) throws Exception { - ch.pipeline().addLast(DECODER, ENCODER, responseHandler); + protected + void initChannel(DatagramChannel ch) throws Exception { + ch.pipeline() + .addLast(DNS_DECODER, DNS_ENCODER, responseHandler); } }); - channelFuture = responseHandler.channelActivePromise; - ch = (DatagramChannel) b.register().channel(); - ch.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(maxPayloadSize)); + channelFuture = channelActivePromise; + ch = (DatagramChannel) b.register() + .channel(); - ch.closeFuture().addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - resolveCache.clear(); - } - }); + 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; + @Override + protected + EventLoop executor() { + return (EventLoop) super.executor(); } - final DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory() { - return dnsQueryLifecycleObserverFactory; + @Override + protected + void doResolve(String inetHost, Promise promise) throws Exception { + doResolve(inetHost, promise, resolveCache); } - /** - * 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; + @Override + protected + void doResolveAll(String inetHost, Promise> promise) throws Exception { + doResolveAll(inetHost, promise, resolveCache); } /** @@ -447,276 +353,21 @@ public class DnsNameResolver extends InetNameResolver { * has been called. */ @Override - public void close() { + 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; - } + protected + void doResolveAll(String inetHost, Promise> promise, DnsCache resolveCache) + throws Exception { - 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())); @@ -729,7 +380,7 @@ public class DnsNameResolver extends InetNameResolver { return; } - final String hostname = hostname(inetHost); + final String hostname = DnsQuestion.hostname(inetHost); InetAddress hostsFileEntry = resolveHostsFileEntry(hostname); if (hostsFileEntry != null) { @@ -737,33 +388,38 @@ public class DnsNameResolver extends InetNameResolver { return; } - if (!doResolveAllCached(hostname, additionals, promise, resolveCache)) { - doResolveAllUncached(hostname, additionals, promise, resolveCache); + if (!doResolveAllCached(hostname, promise, resolveCache)) { + doResolveAllUncached(hostname, promise, resolveCache); } } - private boolean doResolveAllCached(String hostname, - DnsRecord[] additionals, - Promise> promise, - DnsCache resolveCache) { - final List cachedEntries = resolveCache.get(hostname, additionals); + private + boolean doResolveAllCached(String hostname, Promise> promise, DnsCache resolveCache) { + + final List cachedEntries = resolveCache.get(hostname); if (cachedEntries == null || cachedEntries.isEmpty()) { return false; } List result = null; Throwable cause = null; + + //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (cachedEntries) { final int numEntries = cachedEntries.size(); assert numEntries > 0; - if (cachedEntries.get(0).cause() != null) { - cause = cachedEntries.get(0).cause(); - } else { + 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 (f.addressType() + .isInstance(e.address())) { if (result == null) { result = new ArrayList(numEntries); } @@ -785,147 +441,352 @@ public class DnsNameResolver extends InetNameResolver { 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); + private + void doResolveAllUncached(String hostname, Promise> promise, DnsCache resolveCache) { + new DnsNameResolverListResolverContext(this, + hostname, + resolveCache, + dnsServerAddressStreamProvider.nameServerAddressStream(hostname)).resolve(promise); + } + + /** + * 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, 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; } - @Override - DnsNameResolverContext> newResolverContext( - DnsNameResolver parent, String hostname, DnsRecord[] additionals, DnsCache resolveCache, - DnsServerAddressStream nameServerAddrs) { - return new ListResolverContext(parent, hostname, additionals, resolveCache, nameServerAddrs); + final byte[] bytes = NetUtil.createByteArrayFromIpAddressString(inetHost); + if (bytes != null) { + // The inetHost is actually an ipaddress. + promise.setSuccess(InetAddress.getByAddress(bytes)); + return; } - @Override - boolean finishResolve( - Class addressType, List resolvedEntries, - Promise> promise) { + final String hostname = DnsQuestion.hostname(inetHost); - 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); - } - } + InetAddress hostsFileEntry = resolveHostsFileEntry(hostname); + if (hostsFileEntry != null) { + promise.setSuccess(hostsFileEntry); + return; + } - if (result != null) { - promise.trySuccess(result); - return true; + if (!doResolveCached(hostname, promise, resolveCache)) { + doResolveUncached(hostname, promise, resolveCache); + } + } + + 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; + } + } + + private + InetAddress loopbackAddress() { + return preferredAddressType().localhost(); + } + + final + InternetProtocolFamily preferredAddressType() { + return preferredAddressType; + } + + private + boolean doResolveCached(String hostname, Promise promise, DnsCache resolveCache) { + final List cachedEntries = resolveCache.get(hostname); + if (cachedEntries == null || cachedEntries.isEmpty()) { return false; } - } - private void doResolveAllUncached(String hostname, - DnsRecord[] additionals, - Promise> promise, - DnsCache resolveCache) { - new ListResolverContext(this, hostname, additionals, resolveCache, - dnsServerAddressStreamProvider.nameServerAddressStream(hostname)).resolve(promise); - } + InetAddress address = null; + Throwable cause = null; + ArrayList arrayList; - 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 += "."; + //noinspection SynchronizationOnLocalVariableOrMethodParameter + 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; + } + + 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, Promise promise, DnsCache resolveCache) { + new DnsNameResolverSingleResolverContext(this, + hostname, + resolveCache, + dnsServerAddressStreamProvider.nameServerAddressStream(hostname)).resolve(promise); + } + + // Only here to override in unit tests. + int dnsRedirectPort(@SuppressWarnings("unused") InetAddress server) { + return DefaultDnsServerAddressStreamProvider.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 + int[] resolveRecordTypes() { + return resolveRecordTypes; + } + + final + boolean isDecodeIdn() { + return decodeIdn; + } + + /** + * 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 component that tries to resolve hostnames against the hosts file prior to asking to + * remotes DNS servers. + */ + public + HostsFileEntriesResolver hostsFileEntriesResolver() { + return hostsFileEntriesResolver; + } + + + /** + * 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; + } + + + + /** + * Resolves the specified name into an address. + * + * @param inetHost the name to resolve + * @param promise the {@link Promise} which will be fulfilled when the name resolution is finished + * + * @return the address as the result of the resolution + */ + @Override + public final + Future resolve(String inetHost, Promise promise) { + checkNotNull(promise, "promise"); + try { + doResolve(inetHost, 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 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 + */ + @Override + public final + Future> resolveAll(String inetHost, Promise> promise) { + checkNotNull(promise, "promise"); + try { + doResolveAll(inetHost, promise, resolveCache); + return promise; + } catch (Exception e) { + return promise.setFailure(e); } - return hostname; } /** * Sends a DNS query with the specified question. */ - public Future> query(DnsQuestion 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(); + 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()); + public + Future> query(InetSocketAddress nameServerAddr, DnsQuestion question) { + return query0(nameServerAddr, + question, + 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()); + final + Future> query0(InetSocketAddress nameServerAddr, + DnsQuestion question, + Promise> promise) { + return query0(nameServerAddr, question, ch.newPromise(), promise); } - /** - * 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) { + final + Future> query0(InetSocketAddress nameServerAddr, + DnsQuestion question, + ChannelPromise writePromise, + Promise> promise) { assert !writePromise.isVoid(); - final Promise> castPromise = cast( - checkNotNull(promise, "promise")); + final Promise> castPromise = cast(checkNotNull(promise, "promise")); try { - new DnsQueryContext(this, nameServerAddr, question, additionals, castPromise).query(writePromise); + new DnsQueryContext(this, nameServerAddr, question, castPromise).query(writePromise); return castPromise; } catch (Exception e) { return castPromise.setFailure(e); @@ -933,49 +794,29 @@ public class DnsNameResolver extends InetNameResolver { } @SuppressWarnings("unchecked") - private static Promise> cast(Promise promise) { + private static + Promise> cast(Promise promise) { return (Promise>) promise; } - private final class DnsResponseHandler extends ChannelInboundHandlerAdapter { - private final Promise channelActivePromise; + /** + * Sends a DNS query with the specified question. + */ + public + Future> query(DnsQuestion question, + Promise> promise) { + return query(nextNameServerAddress(), question, promise); + } - DnsResponseHandler(Promise channelActivePromise) { - this.channelActivePromise = channelActivePromise; - } + /** + * Sends a DNS query with the specified question using the specified name server list. + */ + public + Future> query(InetSocketAddress nameServerAddr, + DnsQuestion question, + Promise> promise) { - @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); - } + return query0(nameServerAddr, question, null, promise); } } diff --git a/src/dorkbox/network/dns/resolver/DnsNameResolverBuilder.java b/src/dorkbox/network/dns/resolver/DnsNameResolverBuilder.java new file mode 100644 index 00000000..2248d338 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/DnsNameResolverBuilder.java @@ -0,0 +1,406 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package dorkbox.network.dns.resolver; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.intValue; + +import java.util.ArrayList; +import java.util.List; + +import dorkbox.network.dns.resolver.addressProvider.DnsServerAddressStreamProvider; +import dorkbox.network.dns.resolver.addressProvider.DnsServerAddressStreamProviders; +import dorkbox.network.dns.resolver.cache.DefaultDnsCache; +import dorkbox.network.dns.resolver.cache.DnsCache; +import io.netty.channel.ChannelFactory; +import io.netty.channel.EventLoop; +import io.netty.channel.ReflectiveChannelFactory; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.resolver.HostsFileEntriesResolver; +import io.netty.resolver.ResolvedAddressTypes; +import io.netty.util.internal.UnstableApi; + +/** + * A {@link DnsNameResolver} builder. + */ +@UnstableApi +public final +class DnsNameResolverBuilder { + private final EventLoop eventLoop; + 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 = DnsNameResolver.DEFAULT_RESOLVE_ADDRESS_TYPES; + private boolean recursionDesired = true; + private int maxQueriesPerResolve = 16; + private boolean traceEnabled; + private int maxPayloadSize = 4096; + private HostsFileEntriesResolver hostsFileEntriesResolver = HostsFileEntriesResolver.DEFAULT; + private DnsServerAddressStreamProvider dnsServerAddressStreamProvider = DnsServerAddressStreamProviders.platformDefault(); + private DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory = NoopDnsQueryLifecycleObserverFactory.INSTANCE; + private String[] searchDomains; + private int ndots = -1; + private boolean decodeIdn = true; + + /** + * Creates a new builder. + * + * @param eventLoop the {@link EventLoop} the {@link EventLoop} which will perform the communication with the DNS + * servers. + */ + public + DnsNameResolverBuilder(EventLoop eventLoop) { + this.eventLoop = eventLoop; + } + + /** + * Sets the {@link ChannelFactory} as a {@link ReflectiveChannelFactory} of this type. + * Use as an alternative to {@link #channelFactory(ChannelFactory)}. + * + * @param channelType the type + * + * @return {@code this} + */ + public + DnsNameResolverBuilder channelType(Class channelType) { + return channelFactory(new ReflectiveChannelFactory(channelType)); + } + + /** + * Sets the {@link ChannelFactory} that will create a {@link DatagramChannel}. + * + * @param channelFactory the {@link ChannelFactory} + * + * @return {@code this} + */ + public + DnsNameResolverBuilder channelFactory(ChannelFactory channelFactory) { + this.channelFactory = channelFactory; + return this; + } + + /** + * Sets the cache for resolution results. + * + * @param resolveCache the DNS resolution results cache + * + * @return {@code this} + */ + public + DnsNameResolverBuilder 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 + DnsNameResolverBuilder 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 + DnsNameResolverBuilder 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 + DnsNameResolverBuilder ttl(int minTtl, int maxTtl) { + this.maxTtl = maxTtl; + this.minTtl = minTtl; + return this; + } + + /** + * Sets the TTL of the cache for the failed DNS queries (in seconds). + * + * @param negativeTtl the TTL for failed cached queries + * + * @return {@code this} + */ + public + DnsNameResolverBuilder negativeTtl(int negativeTtl) { + this.negativeTtl = negativeTtl; + return this; + } + + /** + * Sets the timeout of each DNS query performed by this resolver (in milliseconds). + * + * @param queryTimeoutMillis the query timeout + * + * @return {@code this} + */ + public + DnsNameResolverBuilder queryTimeoutMillis(long queryTimeoutMillis) { + this.queryTimeoutMillis = queryTimeoutMillis; + return this; + } + + /** + * 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 DnsNameResolver.DEFAULT_RESOLVE_ADDRESS_TYPES; + } + 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"); + } + } + + /** + * Sets the list of the protocol families of the address resolved. + * You can use {@link DnsNameResolverBuilder#computeResolvedAddressTypes(InternetProtocolFamily...)} + * to get a {@link ResolvedAddressTypes} out of some {@link InternetProtocolFamily}s. + * + * @param resolvedAddressTypes the address types + * + * @return {@code this} + */ + public + DnsNameResolverBuilder 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 + DnsNameResolverBuilder recursionDesired(boolean recursionDesired) { + this.recursionDesired = recursionDesired; + return this; + } + + /** + * Sets the maximum allowed number of DNS queries to send when resolving a host name. + * + * @param maxQueriesPerResolve the max number of queries + * + * @return {@code this} + */ + public + DnsNameResolverBuilder maxQueriesPerResolve(int maxQueriesPerResolve) { + this.maxQueriesPerResolve = maxQueriesPerResolve; + return this; + } + + /** + * Sets if this resolver should generate the detailed trace information in an exception message so that + * it is easier to understand the cause of resolution failure. + * + * @param traceEnabled true if trace is enabled + * + * @return {@code this} + */ + public + DnsNameResolverBuilder traceEnabled(boolean traceEnabled) { + this.traceEnabled = traceEnabled; + return this; + } + + /** + * Sets the capacity of the datagram packet buffer (in bytes). The default value is {@code 4096} bytes. + * + * @param maxPayloadSize the capacity of the datagram packet buffer + * + * @return {@code this} + */ + public + DnsNameResolverBuilder maxPayloadSize(int maxPayloadSize) { + this.maxPayloadSize = maxPayloadSize; + return this; + } + + /** + * @param hostsFileEntriesResolver the {@link HostsFileEntriesResolver} used to first check + * if the hostname is locally aliased. + * + * @return {@code this} + */ + public + DnsNameResolverBuilder 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 + DnsNameResolverBuilder 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 + DnsNameResolverBuilder 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 + DnsNameResolverBuilder ndots(int ndots) { + this.ndots = ndots; + return this; + } + + /** + * Set if domain / host names should be decoded to unicode when received. + * See rfc3492. + * + * @param decodeIdn if should get decoded + * + * @return {@code this} + */ + public + DnsNameResolverBuilder decodeIdn(boolean decodeIdn) { + this.decodeIdn = decodeIdn; + return this; + } + + /** + * Returns a new {@link DnsNameResolver} instance. + * + * @return a {@link DnsNameResolver} + */ + public + DnsNameResolver build() { + 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(); + return new DnsNameResolver(eventLoop, + channelFactory, + resolveCache, + authoritativeDnsServerCache, + dnsQueryLifecycleObserverFactory, + queryTimeoutMillis, + resolvedAddressTypes, + recursionDesired, + maxQueriesPerResolve, + traceEnabled, + maxPayloadSize, + hostsFileEntriesResolver, + dnsServerAddressStreamProvider, + searchDomains, + ndots, + decodeIdn); + } + + private + DnsCache newCache() { + return new DefaultDnsCache(intValue(minTtl, 0), intValue(maxTtl, Integer.MAX_VALUE), intValue(negativeTtl, 0)); + } +} diff --git a/src/dorkbox/network/dns/resolver/DnsNameResolverContext.java b/src/dorkbox/network/dns/resolver/DnsNameResolverContext.java new file mode 100644 index 00000000..c5e612fa --- /dev/null +++ b/src/dorkbox/network/dns/resolver/DnsNameResolverContext.java @@ -0,0 +1,964 @@ +/* + * 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 dorkbox.network.dns.resolver; + +import static java.lang.Math.min; +import static java.util.Collections.unmodifiableList; + +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; + +import dorkbox.network.dns.DnsQuestion; +import dorkbox.network.dns.DnsResponse; +import dorkbox.network.dns.constants.DnsRecordType; +import dorkbox.network.dns.constants.DnsResponseCode; +import dorkbox.network.dns.constants.DnsSection; +import dorkbox.network.dns.records.AAAARecord; +import dorkbox.network.dns.records.ARecord; +import dorkbox.network.dns.records.CNAMERecord; +import dorkbox.network.dns.records.DnsMessage; +import dorkbox.network.dns.records.DnsRecord; +import dorkbox.network.dns.records.NSRecord; +import dorkbox.network.dns.resolver.addressProvider.DnsServerAddressStream; +import dorkbox.network.dns.resolver.addressProvider.DnsServerAddresses; +import dorkbox.network.dns.resolver.cache.DnsCache; +import dorkbox.network.dns.resolver.cache.DnsCacheEntry; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.ChannelPromise; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.ThrowableUtil; + +abstract +class DnsNameResolverContext { + + private static final FutureListener> RELEASE_RESPONSE = new FutureListener>() { + @Override + public + void operationComplete(Future> future) { + if (future.isSuccess()) { + future.getNow() + .release(); + } + } + }; + + private static final RuntimeException NXDOMAIN_QUERY_FAILED_EXCEPTION = + ThrowableUtil.unknownStackTrace(new RuntimeException("No answer found and NXDOMAIN response code returned"), + DnsNameResolverContext.class, + "onResponse(..)"); + + private static final RuntimeException CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION = + ThrowableUtil.unknownStackTrace(new RuntimeException("No matching CNAME record found"), + DnsNameResolverContext.class, + "onResponseCNAME(..)"); + + private static final RuntimeException NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION = + ThrowableUtil.unknownStackTrace(new RuntimeException("No matching record type found"), + DnsNameResolverContext.class, + "onResponseAorAAAA(..)"); + + private static final RuntimeException UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION = + ThrowableUtil.unknownStackTrace(new RuntimeException("Response type was unrecognized"), + DnsNameResolverContext.class, + "onResponse(..)"); + + private static final RuntimeException NAME_SERVERS_EXHAUSTED_EXCEPTION = + ThrowableUtil.unknownStackTrace(new RuntimeException("No name servers returned an answer"), + DnsNameResolverContext.class, + "tryToFinishResolve(..)"); + + private final DnsNameResolver parent; + private final DnsServerAddressStream nameServerAddrs; + private final String hostname; + private final DnsCache resolveCache; + private final int maxAllowedQueries; + private final InternetProtocolFamily[] resolvedInternetProtocolFamilies; + + private final Set>> queriesInProgress = Collections.newSetFromMap(new IdentityHashMap>, Boolean>()); + + private List resolvedEntries; + private int allowedQueries; + private boolean triedCNAME; + + DnsNameResolverContext(DnsNameResolver parent, + String hostname, + DnsCache resolveCache, + DnsServerAddressStream nameServerAddrs) { + this.parent = parent; + this.hostname = hostname; + this.resolveCache = resolveCache; + + this.nameServerAddrs = ObjectUtil.checkNotNull(nameServerAddrs, "nameServerAddrs"); + + maxAllowedQueries = parent.maxQueriesPerResolve(); + resolvedInternetProtocolFamilies = parent.resolvedInternetProtocolFamiliesUnsafe(); + allowedQueries = maxAllowedQueries; + } + + void resolve(final Promise promise) { + if (parent.searchDomains().length == 0 || parent.ndots() == 0 || StringUtil.endsWith(hostname, '.')) { + internalResolve(promise); + } + else { + int dots = 0; + for (int idx = hostname.length() - 1; idx >= 0; idx--) { + if (hostname.charAt(idx) == '.' && ++dots >= parent.ndots()) { + internalResolve(promise); + return; + } + } + + doSearchDomainQuery(0, new FutureListener() { + private int count = 1; + + @Override + public + void operationComplete(Future future) throws Exception { + if (future.isSuccess()) { + promise.trySuccess(future.getNow()); + } + else if (count < parent.searchDomains().length) { + doSearchDomainQuery(count++, this); + } + else { + promise.tryFailure(new SearchDomainUnknownHostException(future.cause(), hostname)); + } + } + }); + } + } + + private static final + class SearchDomainUnknownHostException extends UnknownHostException { + SearchDomainUnknownHostException(Throwable cause, String originalHostname) { + super("Search domain query failed. Original hostname: '" + originalHostname + "' " + cause.getMessage()); + setStackTrace(cause.getStackTrace()); + } + + @Override + public + Throwable fillInStackTrace() { + return this; + } + } + + private + void doSearchDomainQuery(int count, FutureListener listener) { + DnsNameResolverContext nextContext = newResolverContext(parent, + hostname + '.' + parent.searchDomains()[count], + resolveCache, + nameServerAddrs); + Promise nextPromise = parent.executor() + .newPromise(); + nextContext.internalResolve(nextPromise); + nextPromise.addListener(listener); + } + + private + void internalResolve(Promise promise) { + DnsServerAddressStream nameServerAddressStream = getNameServers(hostname); + + int[] recordTypes = parent.resolveRecordTypes(); + assert recordTypes.length > 0; + final int end = recordTypes.length - 1; + + for (int i = 0; i < end; ++i) { + if (!resolveQuery(hostname, recordTypes[i], nameServerAddressStream.duplicate(), promise)) { + return; + } + } + + resolveQuery(hostname, recordTypes[end], nameServerAddressStream, promise); + } + + /** + * Add an authoritative nameserver to the cache if its not a root server. + */ + private + void addNameServerToCache(AuthoritativeNameServer name, InetAddress resolved, long ttl) { + if (!name.isRootServer()) { + // Cache NS record if not for a root server as we should never cache for root servers. + parent.authoritativeDnsServerCache() + .cache(name.domainName(), resolved, ttl, parent.ch.eventLoop()); + } + } + + /** + * Returns the {@link DnsServerAddressStream} that was cached for the given hostname or {@code null} if non + * could be found. + */ + private + DnsServerAddressStream getNameServersFromCache(String hostname) { + int len = hostname.length(); + + if (len == 0) { + // We never cache for root servers. + return null; + } + + // We always store in the cache with a trailing '.'. + if (hostname.charAt(len - 1) != '.') { + hostname += "."; + } + + int idx = hostname.indexOf('.'); + if (idx == hostname.length() - 1) { + // We are not interested in handling '.' as we should never serve the root servers from cache. + return null; + } + + // We start from the closed match and then move down. + for (; ; ) { + // Skip '.' as well. + hostname = hostname.substring(idx + 1); + + int idx2 = hostname.indexOf('.'); + if (idx2 <= 0 || idx2 == hostname.length() - 1) { + // We are not interested in handling '.TLD.' as we should never serve the root servers from cache. + return null; + } + idx = idx2; + + List entries = parent.authoritativeDnsServerCache().get(hostname); + if (entries != null && !entries.isEmpty()) { + return DnsServerAddresses.sequential(new DnsCacheIterable(entries)) + .stream(); + } + } + } + + private final + class DnsCacheIterable implements Iterable { + private final List entries; + + DnsCacheIterable(List entries) { + this.entries = entries; + } + + @Override + public + Iterator iterator() { + return new Iterator() { + Iterator entryIterator = entries.iterator(); + + @Override + public + boolean hasNext() { + return entryIterator.hasNext(); + } + + @Override + public + InetSocketAddress next() { + InetAddress address = entryIterator.next() + .address(); + return new InetSocketAddress(address, parent.dnsRedirectPort(address)); + } + + @Override + public + void remove() { + entryIterator.remove(); + } + }; + } + } + + private + void resolveQuery(final DnsServerAddressStream nameServerAddrStream, + final int nameServerAddrStreamIndex, + final DnsQuestion question, + final Promise promise) { + + resolveQuery(nameServerAddrStream, + nameServerAddrStreamIndex, + question, + parent.dnsQueryLifecycleObserverFactory() + .newDnsQueryLifecycleObserver(question), + promise); + } + + private + void resolveQuery(final DnsServerAddressStream nameServerAddrStream, + final int nameServerAddrStreamIndex, + final DnsQuestion question, + final DnsQueryLifecycleObserver queryLifecycleObserver, + final Promise promise) { + // question should have refCnt=2 + if (nameServerAddrStreamIndex >= nameServerAddrStream.size() || allowedQueries == 0 || promise.isCancelled()) { + tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, queryLifecycleObserver, promise); + return; + } + + --allowedQueries; + + final InetSocketAddress nameServerAddr = nameServerAddrStream.next(); + final ChannelPromise writePromise = parent.ch.newPromise(); + final Future> f = + parent.query0(nameServerAddr, + question, + writePromise, + parent.ch.eventLoop().>newPromise()); + + queriesInProgress.add(f); + + queryLifecycleObserver.queryWritten(nameServerAddr, writePromise); + + f.addListener(new FutureListener>() { + @Override + public + void operationComplete(Future> future) { + // future.result() should have refCnt=2 + // question should have refCnt=1 + queriesInProgress.remove(future); + + if (promise.isDone() || future.isCancelled()) { + queryLifecycleObserver.queryCancelled(allowedQueries); + return; + } + + AddressedEnvelope envelope = future.getNow(); + try { + if (future.isSuccess()) { + onResponse(nameServerAddrStream, + nameServerAddrStreamIndex, + question, + envelope, + queryLifecycleObserver, + promise); + } + else { + // Server did not respond or I/O error occurred; try again. + queryLifecycleObserver.queryFailed(future.cause()); + + // query uses the question again... + question.retain(); + resolveQuery(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, promise); + } + } finally { + // future.result() should have refCnt=2 + // question should have refCnt=1 + tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, + // queryLifecycleObserver has already been terminated at this point so we must + // not allow it to be terminated again by tryToFinishResolve. + NoopDnsQueryLifecycleObserver.INSTANCE, promise); + } + } + }); + } + + void onResponse(final DnsServerAddressStream nameServerAddrStream, + final int nameServerAddrStreamIndex, + final DnsQuestion question, + AddressedEnvelope envelope, + final DnsQueryLifecycleObserver queryLifecycleObserver, + Promise promise) { + + final DnsResponse res = envelope.content(); + final int code = res.getHeader() + .getRcode(); + if (code == DnsResponseCode.NOERROR) { + if (handleRedirect(question, envelope, queryLifecycleObserver, promise)) { + // Was a redirect so return here as everything else is handled in handleRedirect(...) + return; + } + final int type = question.getQuestion() + .getType(); + + if (type == DnsRecordType.A || type == DnsRecordType.AAAA) { + onResponseAorAAAA(type, question, envelope, queryLifecycleObserver, promise); + } + else if (type == DnsRecordType.CNAME) { + onResponseCNAME(question, envelope, queryLifecycleObserver, promise); + } + else { + queryLifecycleObserver.queryFailed(UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION); + } + return; + } + + // Retry with the next server if the server did not tell us that the domain does not exist. + if (code != DnsResponseCode.NXDOMAIN) { + resolveQuery(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, queryLifecycleObserver.queryNoAnswer(code), promise); + } + else { + queryLifecycleObserver.queryFailed(NXDOMAIN_QUERY_FAILED_EXCEPTION); + } + } + + /** + * Handles a redirect answer if needed and returns {@code true} if a redirect query has been made. + */ + private + boolean handleRedirect(DnsQuestion question, + AddressedEnvelope envelope, + final DnsQueryLifecycleObserver queryLifecycleObserver, + Promise promise) { + + final DnsResponse res = envelope.content(); + + // Check if we have answers, if not this may be an non authority NS and so redirects must be handled. + DnsRecord[] answerArray = res.getSectionArray(DnsSection.ANSWER); + if (answerArray.length == 0) { + AuthoritativeNameServerList serverNames = extractAuthoritativeNameServers(question.getQuestion() + .getName() + .toString(), res); + + if (serverNames != null) { + List nameServers = new ArrayList(serverNames.size()); + DnsRecord[] additionalArray = res.getSectionArray(DnsSection.ADDITIONAL); + + for (int i = 0; i < additionalArray.length; i++) { + final DnsRecord r = additionalArray[i]; + + if (r.getType() == DnsRecordType.A && !parent.supportsARecords() || + r.getType() == DnsRecordType.AAAA && !parent.supportsAAAARecords()) { + continue; + } + + final String recordName = r.getName() + .toString(); + AuthoritativeNameServer authoritativeNameServer = serverNames.remove(recordName); + + if (authoritativeNameServer == null) { + // Not a server we are interested in. + continue; + } + + InetAddress resolved = parseAddress(r, recordName); + if (resolved == null) { + // Could not parse it, move to the next. + continue; + } + + nameServers.add(new InetSocketAddress(resolved, parent.dnsRedirectPort(resolved))); + addNameServerToCache(authoritativeNameServer, resolved, r.getTTL()); + } + + if (!nameServers.isEmpty()) { + resolveQuery(parent.uncachedRedirectDnsServerStream(nameServers), + 0, + question, + queryLifecycleObserver.queryRedirected(unmodifiableList(nameServers)), + promise); + return true; + } + } + } + return false; + } + + /** + * Returns the {@code {@link AuthoritativeNameServerList} which were included in {@link DnsSection#AUTHORITY} + * or {@code null} if non are found. + */ + private static + AuthoritativeNameServerList extractAuthoritativeNameServers(String questionName, DnsResponse res) { + DnsRecord[] authority = res.getSectionArray(DnsSection.AUTHORITY); + if (authority.length == 0) { + return null; + } + + System.err.println("TYODO"); + AuthoritativeNameServerList serverNames = new AuthoritativeNameServerList(questionName); + for (int i = 0; i < authority.length; i++) { + final DnsRecord dnsRecord = authority[i]; + serverNames.add(dnsRecord); + } + + return serverNames; + } + + private + void onResponseAorAAAA(int qType, + DnsMessage question, + AddressedEnvelope envelope, + final DnsQueryLifecycleObserver queryLifecycleObserver, + Promise promise) { + + // We often get a bunch of CNAMES as well when we asked for A/AAAA. + final DnsResponse response = envelope.content(); + final Map cnames = buildAliasMap(response); + + DnsRecord[] answerArray = response.getSectionArray(DnsSection.ANSWER); + + boolean found = false; + for (int i = 0; i < answerArray.length; i++) { + final DnsRecord r = answerArray[i]; + final int type = r.getType(); + if (type != DnsRecordType.A && type != DnsRecordType.AAAA) { + continue; + } + + final String questionName = question.getQuestion() + .getName() + .toString(); + final String recordName = r.getName() + .toString(); + + // Make sure the record is for the questioned domain. + if (!recordName.equals(questionName)) { + // Even if the record's name is not exactly same, it might be an alias defined in the CNAME records. + String resolved = questionName; + do { + resolved = cnames.get(resolved); + if (recordName.equals(resolved)) { + break; + } + } while (resolved != null); + + if (resolved == null) { + continue; + } + } + + InetAddress resolved = parseAddress(r, hostname); + if (resolved == null) { + continue; + } + + if (resolvedEntries == null) { + resolvedEntries = new ArrayList(8); + } + + final DnsCacheEntry e = new DnsCacheEntry(hostname, resolved); + resolveCache.cache(hostname, resolved, r.getTTL(), parent.ch.eventLoop()); + resolvedEntries.add(e); + found = true; + + // Note that we do not break from the loop here, so we decode/cache all A/AAAA records. + } + + if (found) { + queryLifecycleObserver.querySucceed(); + return; + } + + if (cnames.isEmpty()) { + queryLifecycleObserver.queryFailed(NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION); + } + else { + // We asked for A/AAAA but we got only CNAME. + onResponseCNAME(question, envelope, cnames, queryLifecycleObserver, promise); + } + } + + private + InetAddress parseAddress(DnsRecord record, String name) { + int type = record.getType(); + + if (type == DnsRecordType.A) { + ARecord aRecord = (ARecord) record; + return aRecord.getAddress(); + } + else if (type == DnsRecordType.AAAA) { + AAAARecord aaaaRecord = (AAAARecord) record; + return aaaaRecord.getAddress(); + } + else { + return null; + } + } + + private + void onResponseCNAME(DnsMessage question, + AddressedEnvelope envelope, + final DnsQueryLifecycleObserver queryLifecycleObserver, + Promise promise) { + onResponseCNAME(question, envelope, buildAliasMap(envelope.content()), queryLifecycleObserver, promise); + } + + private + void onResponseCNAME(DnsMessage question, + AddressedEnvelope response, + Map cnames, + final DnsQueryLifecycleObserver queryLifecycleObserver, + Promise promise) { + + // Resolve the host name in the question into the real host name. + String resolved = question.getQuestion() + .getName() + .toString(); + boolean found = false; + while (!cnames.isEmpty()) { // Do not attempt to call Map.remove() when the Map is empty + // because it can be Collections.emptyMap() + // whose remove() throws a UnsupportedOperationException. + final String next = cnames.remove(resolved); + if (next != null) { + found = true; + resolved = next; + } + else { + break; + } + } + + if (found) { + followCname(resolved, queryLifecycleObserver, promise); + } + else { + queryLifecycleObserver.queryFailed(CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION); + } + } + + private static + Map buildAliasMap(DnsMessage response) { + DnsRecord[] answerArray = response.getSectionArray(DnsSection.ANSWER); + Map cnames = null; + int length = answerArray.length; + for (int i = 0; i < length; i++) { + final DnsRecord record = answerArray[i]; + final int type = record.getType(); + if (type != DnsRecordType.CNAME) { + continue; + } + + System.err.println("CHECK ME ME! we don't have bytebuf content in this fashion anymore"); + CNAMERecord re = (CNAMERecord) record; + final String domainName = re.getAlias() + .toString(); + + if (domainName == null) { + continue; + } + + if (cnames == null) { + cnames = new HashMap(min(8, length)); + } + + cnames.put(record.getName() + .toString() + .toLowerCase(Locale.US), domainName.toLowerCase(Locale.US)); + } + + return cnames != null ? cnames : Collections.emptyMap(); + } + + void tryToFinishResolve(final DnsServerAddressStream nameServerAddrStream, + final int nameServerAddrStreamIndex, + final DnsQuestion question, + final DnsQueryLifecycleObserver queryLifecycleObserver, + final Promise promise) { + // There are no queries left to try. + if (!queriesInProgress.isEmpty()) { + queryLifecycleObserver.queryCancelled(allowedQueries); + + // 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(promise, question); + } + + // 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 (resolvedEntries == null) { + if (nameServerAddrStreamIndex < nameServerAddrStream.size()) { + // the query is going to use the question again... + question.retain(); + if (queryLifecycleObserver == NoopDnsQueryLifecycleObserver.INSTANCE) { + // If the queryLifecycleObserver has already been terminated we should create a new one for this + // fresh query. + resolveQuery(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, promise); + } + else { + resolveQuery(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, queryLifecycleObserver, promise); + } + return; + } + + queryLifecycleObserver.queryFailed(NAME_SERVERS_EXHAUSTED_EXCEPTION); + + // .. 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; + + resolveQuery(hostname, DnsRecordType.CNAME, getNameServers(hostname), promise); + return; + } + } + else { + queryLifecycleObserver.queryCancelled(allowedQueries); + } + + // We have at least one resolved address or tried CNAME as the last resort.. + finishResolve(promise, question); + } + + private + boolean gotPreferredAddress() { + if (resolvedEntries == null) { + return false; + } + + final int size = resolvedEntries.size(); + final Class inetAddressType = parent.preferredAddressType() + .addressType(); + for (int i = 0; i < size; i++) { + InetAddress address = resolvedEntries.get(i) + .address(); + if (inetAddressType.isInstance(address)) { + return true; + } + } + return false; + } + + private + void finishResolve(Promise promise, final DnsQuestion question) { + // now we are done with the question. + question.release(); + + 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 (resolvedEntries != null) { + // Found at least one resolved address. + for (InternetProtocolFamily f : resolvedInternetProtocolFamilies) { + if (finishResolve(f.addressType(), resolvedEntries, promise)) { + return; + } + } + } + + // No resolved address found. + final int tries = maxAllowedQueries - allowedQueries; + final StringBuilder buf = new StringBuilder(64); + + buf.append("failed to resolve '") + .append(hostname) + .append('\''); + if (tries > 1) { + if (tries < maxAllowedQueries) { + buf.append(" after ") + .append(tries) + .append(" queries "); + } + else { + buf.append(". Exceeded max queries per resolve ") + .append(maxAllowedQueries) + .append(' '); + } + } + + final UnknownHostException cause = new UnknownHostException(buf.toString()); + cause.setStackTrace(new StackTraceElement[0]); + + resolveCache.cache(hostname, cause, parent.ch.eventLoop()); + promise.tryFailure(cause); + } + + abstract + boolean finishResolve(Class addressType, List resolvedEntries, Promise promise); + + abstract + DnsNameResolverContext newResolverContext(DnsNameResolver parent, + String hostname, + DnsCache resolveCache, + DnsServerAddressStream nameServerAddrs); + + private + DnsServerAddressStream getNameServers(String hostname) { + DnsServerAddressStream stream = getNameServersFromCache(hostname); + return stream == null ? nameServerAddrs : stream; + } + + private + void followCname(String cname, final DnsQueryLifecycleObserver queryLifecycleObserver, Promise promise) { + + // Use the same server for both CNAME queries + DnsServerAddressStream stream = DnsServerAddresses.singleton(getNameServers(cname).next()) + .stream(); + DnsQuestion cnameQuestion = null; + try { + if (parent.supportsARecords()) { + cnameQuestion = DnsQuestion.newResolveQuestion(hostname, DnsRecordType.A, parent.isRecursionDesired()); + } + if (parent.supportsAAAARecords()) { + cnameQuestion = DnsQuestion.newResolveQuestion(hostname, DnsRecordType.AAAA, parent.isRecursionDesired()); + } + + } catch (Throwable cause) { + queryLifecycleObserver.queryFailed(cause); + PlatformDependent.throwException(cause); + } + + if (cnameQuestion != null) { + resolveQuery(stream, 0, cnameQuestion, queryLifecycleObserver.queryCNAMEd(cnameQuestion), promise); + } + } + + private + boolean resolveQuery(String hostname, int type, DnsServerAddressStream dnsServerAddressStream, Promise promise) { + + DnsQuestion message = DnsQuestion.newResolveQuestion(hostname, type, parent.isRecursionDesired()); + if (message == null) { + return false; + } + + resolveQuery(dnsServerAddressStream, 0, message, promise); + return true; + } + + /** + * Holds the closed DNS Servers for a domain. + */ + private static final + class AuthoritativeNameServerList { + + private final String questionName; + + // We not expect the linked-list to be very long so a double-linked-list is overkill. + private AuthoritativeNameServer head; + private int count; + + AuthoritativeNameServerList(String questionName) { + this.questionName = questionName.toLowerCase(Locale.US); + } + + void add(DnsRecord record) { + if (record.getType() != DnsRecordType.NS) { + return; + } + + // Only include servers that serve the correct domain. + String recordName = record.getName() + .toString(); + if (questionName.length() < recordName.length()) { + return; + } + + int dots = 0; + for (int a = recordName.length() - 1, b = questionName.length() - 1; a >= 0; a--, b--) { + char c = recordName.charAt(a); + if (questionName.charAt(b) != c) { + return; + } + if (c == '.') { + dots++; + } + } + + if (head != null && head.dots > dots) { + // We already have a closer match so ignore this one, no need to parse the domainName etc. + return; + } + + System.err.println("DOUBLE CHECK me! we do things differently now!"); + NSRecord re = (NSRecord) record; + final String domainName = re.getAdditionalName() + .toString(); + if (domainName == null) { + // Could not be parsed, ignore. + return; + } + + // We are only interested in preserving the nameservers which are the closest to our qName, so ensure + // we drop servers that have a smaller dots count. + if (head == null || head.dots < dots) { + count = 1; + head = new AuthoritativeNameServer(dots, recordName, domainName); + } + else if (head.dots == dots) { + AuthoritativeNameServer serverName = head; + while (serverName.next != null) { + serverName = serverName.next; + } + serverName.next = new AuthoritativeNameServer(dots, recordName, domainName); + count++; + } + } + + // Just walk the linked-list and mark the entry as removed when matched, so next lookup will need to process + // one node less. + AuthoritativeNameServer remove(String nsName) { + AuthoritativeNameServer serverName = head; + + while (serverName != null) { + if (!serverName.removed && serverName.nsName.equalsIgnoreCase(nsName)) { + serverName.removed = true; + return serverName; + } + serverName = serverName.next; + } + return null; + } + + int size() { + return count; + } + } + + + static final + class AuthoritativeNameServer { + final int dots; + final String nsName; + final String domainName; + + AuthoritativeNameServer next; + boolean removed; + + AuthoritativeNameServer(int dots, String domainName, String nsName) { + this.dots = dots; + this.nsName = nsName; + this.domainName = domainName; + } + + /** + * Returns {@code true} if its a root server. + */ + boolean isRootServer() { + return dots == 1; + } + + /** + * The domain for which the {@link AuthoritativeNameServer} is responsible. + */ + String domainName() { + return domainName; + } + } +} diff --git a/src/dorkbox/network/dns/resolver/DnsNameResolverException.java b/src/dorkbox/network/dns/resolver/DnsNameResolverException.java new file mode 100644 index 00000000..21b320f1 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/DnsNameResolverException.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package dorkbox.network.dns.resolver; + +import java.net.InetSocketAddress; + +import dorkbox.network.dns.DnsQuestion; +import dorkbox.network.dns.records.DnsMessage; +import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.UnstableApi; + +/** + * A {@link RuntimeException} raised when {@link DnsResolver} failed to perform a successful query. + */ +@UnstableApi +public final +class DnsNameResolverException extends RuntimeException { + + private static final long serialVersionUID = -8826717909627131850L; + + private final InetSocketAddress remoteAddress; + private final DnsQuestion question; + + public + DnsNameResolverException(InetSocketAddress remoteAddress, DnsQuestion question, String message) { + super(message); + this.remoteAddress = validateRemoteAddress(remoteAddress); + this.question = validateQuestion(question); + } + + private static + InetSocketAddress validateRemoteAddress(InetSocketAddress remoteAddress) { + return ObjectUtil.checkNotNull(remoteAddress, "remoteAddress"); + } + + private static + DnsQuestion validateQuestion(DnsQuestion question) { + return ObjectUtil.checkNotNull(question, "question"); + } + + public + DnsNameResolverException(InetSocketAddress remoteAddress, DnsQuestion question, String message, Throwable cause) { + super(message, cause); + this.remoteAddress = validateRemoteAddress(remoteAddress); + this.question = validateQuestion(question); + } + + /** + * Returns the {@link InetSocketAddress} of the DNS query that has failed. + */ + public + InetSocketAddress remoteAddress() { + return remoteAddress; + } + + /** + * Returns the {@link DnsQuestion} of the DNS query that has failed. + */ + public + DnsMessage question() { + return question; + } + + @Override + public + Throwable fillInStackTrace() { + setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + return this; + } +} diff --git a/src/dorkbox/network/dns/resolver/DnsNameResolverListResolverContext.java b/src/dorkbox/network/dns/resolver/DnsNameResolverListResolverContext.java new file mode 100644 index 00000000..2d572c5e --- /dev/null +++ b/src/dorkbox/network/dns/resolver/DnsNameResolverListResolverContext.java @@ -0,0 +1,70 @@ +/* + * 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 dorkbox.network.dns.resolver; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; + +import dorkbox.network.dns.resolver.addressProvider.DnsServerAddressStream; +import dorkbox.network.dns.resolver.cache.DnsCache; +import dorkbox.network.dns.resolver.cache.DnsCacheEntry; +import io.netty.util.concurrent.Promise; + +/** + * + */ +final +class DnsNameResolverListResolverContext extends DnsNameResolverContext> { + DnsNameResolverListResolverContext(DnsNameResolver parent, + String hostname, + DnsCache resolveCache, + DnsServerAddressStream nameServerAddrs) { + super(parent, hostname, resolveCache, nameServerAddrs); + } + + @Override + DnsNameResolverContext> newResolverContext(DnsNameResolver parent, + String hostname, + DnsCache resolveCache, + DnsServerAddressStream nameServerAddrs) { + return new DnsNameResolverListResolverContext(parent, hostname, 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; + } +} diff --git a/src/dorkbox/network/dns/resolver/DnsNameResolverResponseHandler.java b/src/dorkbox/network/dns/resolver/DnsNameResolverResponseHandler.java new file mode 100644 index 00000000..9114ca8d --- /dev/null +++ b/src/dorkbox/network/dns/resolver/DnsNameResolverResponseHandler.java @@ -0,0 +1,67 @@ +/* + * 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 dorkbox.network.dns.resolver; + +import dorkbox.network.dns.DnsResponse; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.concurrent.Promise; + +final +class DnsNameResolverResponseHandler extends ChannelInboundHandlerAdapter { + + private DnsNameResolver dnsNameResolver; + private final Promise channelActivePromise; + + DnsNameResolverResponseHandler(final DnsNameResolver dnsNameResolver, Promise channelActivePromise) { + this.dnsNameResolver = dnsNameResolver; + this.channelActivePromise = channelActivePromise; + } + + @Override + public + void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + channelActivePromise.setSuccess(ctx.channel()); + } + + @Override + public + void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + final DnsResponse response = (DnsResponse) msg; + + final int queryId = response.getHeader().getID(); + + if (DnsNameResolver.logger.isDebugEnabled()) { + DnsNameResolver.logger.debug("{} RECEIVED: [{}: {}], {}", dnsNameResolver.ch, queryId, response.sender(), response); + } + + final DnsQueryContext qCtx = dnsNameResolver.queryContextManager.get(response.sender(), queryId); + if (qCtx == null) { + DnsNameResolver.logger.warn("{} Received a DNS response with an unknown ID: {}", dnsNameResolver.ch, queryId); + return; + } + + qCtx.finish(response); + } + + @Override + public + void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + DnsNameResolver.logger.warn("{} Unexpected exception: ", dnsNameResolver.ch, cause); + } +} diff --git a/src/dorkbox/network/dns/resolver/DnsNameResolverSingleResolverContext.java b/src/dorkbox/network/dns/resolver/DnsNameResolverSingleResolverContext.java new file mode 100644 index 00000000..dd4b840e --- /dev/null +++ b/src/dorkbox/network/dns/resolver/DnsNameResolverSingleResolverContext.java @@ -0,0 +1,62 @@ +/* + * 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 dorkbox.network.dns.resolver; + +import java.net.InetAddress; +import java.util.List; + +import dorkbox.network.dns.resolver.addressProvider.DnsServerAddressStream; +import dorkbox.network.dns.resolver.cache.DnsCache; +import dorkbox.network.dns.resolver.cache.DnsCacheEntry; +import io.netty.util.concurrent.Promise; + +/** + * + */ +@SuppressWarnings("ForLoopReplaceableByForEach") +final +class DnsNameResolverSingleResolverContext extends DnsNameResolverContext { + DnsNameResolverSingleResolverContext(DnsNameResolver parent, + String hostname, + DnsCache resolveCache, + DnsServerAddressStream nameServerAddrs) { + super(parent, hostname, 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)) { + DnsNameResolver.trySuccess(promise, a); + return true; + } + } + + return false; + } + + @Override + DnsNameResolverContext newResolverContext(DnsNameResolver parent, + String hostname, + DnsCache resolveCache, + DnsServerAddressStream nameServerAddrs) { + return new DnsNameResolverSingleResolverContext(parent, hostname, resolveCache, nameServerAddrs); + } +} diff --git a/src/dorkbox/network/dns/resolver/DnsQueryContext.java b/src/dorkbox/network/dns/resolver/DnsQueryContext.java new file mode 100644 index 00000000..a190ae00 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/DnsQueryContext.java @@ -0,0 +1,233 @@ +/* + * 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 dorkbox.network.dns.resolver; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import java.net.InetSocketAddress; + +import dorkbox.network.dns.DnsQuestion; +import dorkbox.network.dns.DnsResponse; +import dorkbox.network.dns.constants.DnsSection; +import dorkbox.network.dns.records.DnsRecord; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelPromise; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.concurrent.Promise; +import io.netty.util.concurrent.ScheduledFuture; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +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 InetSocketAddress nameServerAddr; + + private volatile ScheduledFuture timeoutFuture; + + DnsQueryContext(DnsNameResolver parent, + InetSocketAddress nameServerAddr, + DnsQuestion question, + Promise> promise) { + + this.parent = checkNotNull(parent, "parent"); + this.nameServerAddr = checkNotNull(nameServerAddr, "nameServerAddr"); + this.question = checkNotNull(question, "question"); + this.promise = checkNotNull(promise, "promise"); + + id = parent.queryContextManager.add(this); + + question.init(id, nameServerAddr); + } + + void query(ChannelPromise writePromise) { + final DnsQuestion question = question(); + final InetSocketAddress nameServerAddr = nameServerAddr(); + + if (logger.isDebugEnabled()) { + logger.debug("{} WRITE: [{}: {}], {}", parent.ch, id, nameServerAddr, question); + } + + sendQuery(question, writePromise); + } + + InetSocketAddress nameServerAddr() { + return nameServerAddr; + } + + DnsQuestion question() { + return question; + } + + private + void sendQuery(final DnsQuestion query, final ChannelPromise writePromise) { + if (parent.channelFuture.isDone()) { + writeQuery(query, writePromise); + } + else { + parent.channelFuture.addListener(new GenericFutureListener>() { + @Override + public + void operationComplete(Future future) throws Exception { + if (future.isSuccess()) { + writeQuery(query, writePromise); + } + else { + Throwable cause = future.cause(); + promise.tryFailure(cause); + writePromise.setFailure(cause); + } + } + }); + } + } + + private + void writeQuery(final DnsQuestion query, final ChannelPromise writePromise) { + final ChannelFuture writeFuture = parent.ch.writeAndFlush(query, writePromise); + if (writeFuture.isDone()) { + onQueryWriteCompletion(writeFuture); + } + else { + writeFuture.addListener(new ChannelFutureListener() { + @Override + public + void operationComplete(ChannelFuture future) throws Exception { + onQueryWriteCompletion(writeFuture); + } + }); + } + } + + private + void onQueryWriteCompletion(ChannelFuture writeFuture) { + if (!writeFuture.isSuccess()) { + writeFuture.cause() + .printStackTrace(); + setFailure("failed to send a query", writeFuture.cause()); + return; + } + + // Schedule a query timeout task if necessary. + final long queryTimeoutMillis = parent.queryTimeoutMillis(); + if (queryTimeoutMillis > 0) { + // TODO UNCOMMENT! + // timeoutFuture = parent.ch.eventLoop() + // .schedule(new Runnable() { + // @Override + // public + // void run() { + // if (promise.isDone()) { + // // Received a response before the query times out. + // return; + // } + // + // setFailure("query timed out after " + queryTimeoutMillis + " milliseconds", null); + // } + // }, queryTimeoutMillis, TimeUnit.MILLISECONDS); + } + } + + private + void setFailure(String message, Throwable cause) { + final InetSocketAddress nameServerAddr = nameServerAddr(); + parent.queryContextManager.remove(nameServerAddr, id); + + final StringBuilder buf = new StringBuilder(message.length() + 64); + buf.append('[') + .append(nameServerAddr) + .append("] ") + .append(message) + .append(" (no stack trace available)"); + + final DnsNameResolverException e; + if (cause != null) { + e = new DnsNameResolverException(nameServerAddr, question(), buf.toString(), cause); + } + else { + e = new DnsNameResolverException(nameServerAddr, question(), buf.toString()); + } + + promise.tryFailure(e); + } + + void finish(AddressedEnvelope envelope) { + final DnsResponse response = envelope.content(); + + try { + DnsRecord[] sectionArray = response.getSectionArray(DnsSection.QUESTION); + if (sectionArray.length != 1) { + logger.warn("Received a DNS response with invalid number of questions: {}", envelope); + return; + } + + DnsRecord[] questionArray = question.getSectionArray(DnsSection.QUESTION); + if (questionArray.length != 1) { + logger.warn("Received a DNS response with invalid number of query questions: {}", envelope); + return; + } + + + if (!questionArray[0].equals(sectionArray[0])) { + logger.warn("Received a mismatching DNS response: {}", envelope); + return; + } + + setSuccess(envelope); + } finally { + if (question.isResolveQuestion()) { + // for resolve questions (always A/AAAA), we convert the answer into InetAddress, however with OTHER TYPES, we pass + // back the result to the user, and if we release it, all of the content will be cleared. + response.release(); + } + } + } + + private + void setSuccess(AddressedEnvelope envelope) { + parent.queryContextManager.remove(nameServerAddr(), id); + + // Cancel the timeout task. + final ScheduledFuture timeoutFuture = this.timeoutFuture; + if (timeoutFuture != null) { + timeoutFuture.cancel(false); + } + + Promise> promise = this.promise; + if (promise.setUncancellable()) { + @SuppressWarnings("unchecked") + AddressedEnvelope castResponse = envelope.retain(); + // envelope now has a refCnt = 2 + if (!promise.trySuccess(castResponse)) { // question is used here! + // We failed to notify the promise as it was failed before, thus we need to release the envelope + envelope.release(); + } + + envelope.release(); + } + } +} diff --git a/src/dorkbox/network/dns/resolver/DnsQueryContextManager.java b/src/dorkbox/network/dns/resolver/DnsQueryContextManager.java new file mode 100644 index 00000000..f62516a0 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/DnsQueryContextManager.java @@ -0,0 +1,156 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package dorkbox.network.dns.resolver; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; + +import io.netty.util.NetUtil; +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; +import io.netty.util.internal.PlatformDependent; + +final +class DnsQueryContextManager { + + /** + * A map whose key is the DNS server address and value is the map of the DNS query ID and its corresponding {@link DnsQueryContext}. + */ + final Map> map = new HashMap>(); + + int add(DnsQueryContext queryContext) { + final IntObjectMap contexts = getOrCreateContextMap(queryContext.nameServerAddr()); + + int id = PlatformDependent.threadLocalRandom() + .nextInt(65536 - 1) + 1; + final int maxTries = 65535 << 1; + int tries = 0; + + synchronized (contexts) { + for (; ; ) { + if (!contexts.containsKey(id)) { + contexts.put(id, queryContext); + return id; + } + + id = id + 1 & 0xFFFF; + + if (++tries >= maxTries) { + throw new IllegalStateException("query ID space exhausted: " + queryContext.question()); + } + } + } + } + + private + IntObjectMap getOrCreateContextMap(InetSocketAddress nameServerAddr) { + synchronized (map) { + final IntObjectMap contexts = map.get(nameServerAddr); + if (contexts != null) { + return contexts; + } + + final IntObjectMap newContexts = new IntObjectHashMap(); + final InetAddress a = nameServerAddr.getAddress(); + final int port = nameServerAddr.getPort(); + map.put(nameServerAddr, newContexts); + + if (a instanceof Inet4Address) { + // Also add the mapping for the IPv4-compatible IPv6 address. + final Inet4Address a4 = (Inet4Address) a; + if (a4.isLoopbackAddress()) { + map.put(new InetSocketAddress(NetUtil.LOCALHOST6, port), newContexts); + } + else { + map.put(new InetSocketAddress(toCompactAddress(a4), port), newContexts); + } + } + else if (a instanceof Inet6Address) { + // Also add the mapping for the IPv4 address if this IPv6 address is compatible. + final Inet6Address a6 = (Inet6Address) a; + if (a6.isLoopbackAddress()) { + map.put(new InetSocketAddress(NetUtil.LOCALHOST4, port), newContexts); + } + else if (a6.isIPv4CompatibleAddress()) { + map.put(new InetSocketAddress(toIPv4Address(a6), port), newContexts); + } + } + + return newContexts; + } + } + + private static + Inet6Address toCompactAddress(Inet4Address a4) { + byte[] b4 = a4.getAddress(); + byte[] b6 = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, b4[0], b4[1], b4[2], b4[3]}; + try { + return (Inet6Address) InetAddress.getByAddress(b6); + } catch (UnknownHostException e) { + throw new Error(e); + } + } + + private static + Inet4Address toIPv4Address(Inet6Address a6) { + byte[] b6 = a6.getAddress(); + byte[] b4 = {b6[12], b6[13], b6[14], b6[15]}; + try { + return (Inet4Address) InetAddress.getByAddress(b4); + } catch (UnknownHostException e) { + throw new Error(e); + } + } + + DnsQueryContext get(InetSocketAddress nameServerAddr, int id) { + final IntObjectMap contexts = getContextMap(nameServerAddr); + final DnsQueryContext qCtx; + if (contexts != null) { + synchronized (contexts) { + qCtx = contexts.get(id); + } + } + else { + qCtx = null; + } + + return qCtx; + } + + private + IntObjectMap getContextMap(InetSocketAddress nameServerAddr) { + synchronized (map) { + return map.get(nameServerAddr); + } + } + + DnsQueryContext remove(InetSocketAddress nameServerAddr, int id) { + final IntObjectMap contexts = getContextMap(nameServerAddr); + if (contexts == null) { + return null; + } + + synchronized (contexts) { + return contexts.remove(id); + } + } +} diff --git a/src/dorkbox/network/dns/resolver/DnsQueryLifecycleObserver.java b/src/dorkbox/network/dns/resolver/DnsQueryLifecycleObserver.java new file mode 100644 index 00000000..d16cacab --- /dev/null +++ b/src/dorkbox/network/dns/resolver/DnsQueryLifecycleObserver.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver; + +import java.net.InetSocketAddress; +import java.util.List; + +import dorkbox.network.dns.constants.DnsRecordType; +import dorkbox.network.dns.constants.DnsResponseCode; +import dorkbox.network.dns.records.DnsMessage; +import io.netty.channel.ChannelFuture; +import io.netty.util.internal.UnstableApi; + +/** + * This interface provides visibility into individual DNS queries. The lifecycle of an objects is as follows: + *
    + *
  1. Object creation
  2. + *
  3. {@link #queryCancelled(int)}
  4. + *
+ * OR + *
    + *
  1. Object creation
  2. + *
  3. {@link #queryWritten(InetSocketAddress, ChannelFuture)}
  4. + *
  5. {@link #queryRedirected(List)} or {@link #queryCNAMEd(DnsQuestion)} or + * {@link #queryNoAnswer(int)} or {@link #queryCancelled(int)} or + * {@link #queryFailed(Throwable)} or {@link #querySucceed()}
  6. + *
+ *

+ * This interface can be used to track metrics for individual DNS servers. Methods which may lead to another DNS query + * return an object of type {@link DnsQueryLifecycleObserver}. Implementations may use this to build a query tree to + * understand the "sub queries" generated by a single query. + */ +@UnstableApi +public +interface DnsQueryLifecycleObserver { + /** + * The query has been written. + * + * @param dnsServerAddress The DNS server address which the query was sent to. + * @param future The future which represents the status of the write operation for the DNS query. + */ + void queryWritten(InetSocketAddress dnsServerAddress, ChannelFuture future); + + /** + * The query may have been written but it was cancelled at some point. + * + * @param queriesRemaining The number of queries remaining. + */ + void queryCancelled(int queriesRemaining); + + /** + * The query has been redirected to another list of DNS servers. + * + * @param nameServers The name servers the query has been redirected to. + * + * @return An observer for the new query which we may issue. + */ + DnsQueryLifecycleObserver queryRedirected(List nameServers); + + /** + * The query returned a CNAME which we may attempt to follow with a new query. + *

+ * Note that multiple queries may be encountering a CNAME. For example a if both {@link DnsRecordType#AAAA} and + * {@link DnsRecordType#A} are supported we may query for both. + * + * @param cnameQuestion the question we would use if we issue a new query. + * + * @return An observer for the new query which we may issue. + */ + DnsQueryLifecycleObserver queryCNAMEd(DnsMessage cnameQuestion); + + /** + * The response to the query didn't provide the expected response code, but it didn't return + * {@link DnsResponseCode#NXDOMAIN} so we may try to query again. + * + * @param code the unexpected response code. + * + * @return An observer for the new query which we may issue. + */ + DnsQueryLifecycleObserver queryNoAnswer(int code); + + /** + * The following criteria are possible: + *

    + *
  • IO Error
  • + *
  • Server responded with an invalid DNS response
  • + *
  • Server responded with a valid DNS response, but it didn't progress the resolution
  • + *
+ * + * @param cause The cause which for the failure. + */ + void queryFailed(Throwable cause); + + /** + * The query received the expected results. + */ + void querySucceed(); +} diff --git a/src/dorkbox/network/dns/resolver/DnsQueryLifecycleObserverFactory.java b/src/dorkbox/network/dns/resolver/DnsQueryLifecycleObserverFactory.java new file mode 100644 index 00000000..54a7f405 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/DnsQueryLifecycleObserverFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver; + +import dorkbox.network.dns.records.DnsMessage; +import io.netty.util.internal.UnstableApi; + +/** + * Used to generate new instances of {@link DnsQueryLifecycleObserver}. + */ +@UnstableApi +public +interface DnsQueryLifecycleObserverFactory { + /** + * Create a new instance of a {@link DnsQueryLifecycleObserver}. This will be called at the start of a new query. + * + * @param question The question being asked. + * + * @return a new instance of a {@link DnsQueryLifecycleObserver}. + */ + DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsMessage question); +} diff --git a/src/dorkbox/network/dns/resolver/InetNameGroupResolver.java b/src/dorkbox/network/dns/resolver/InetNameGroupResolver.java new file mode 100644 index 00000000..2dd674a6 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/InetNameGroupResolver.java @@ -0,0 +1,39 @@ +package dorkbox.network.dns.resolver; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.List; + +import io.netty.resolver.AddressResolver; +import io.netty.resolver.SimpleNameResolver; +import io.netty.util.concurrent.EventExecutor; + +public abstract class InetNameGroupResolver extends SimpleNameResolver> { + private volatile AddressResolver addressResolver; + + /** + * @param executor the {@link EventExecutor} which is used to notify the listeners of the {@link Future} returned + * by {@link #resolve(String)} + */ + protected + InetNameGroupResolver(EventExecutor executor) { + super(executor); + } + + /** + * Return a {@link AddressResolver} that will use this name resolver underneath. + * It's cached internally, so the same instance is always returned. + */ + public AddressResolver asAddressResolver() { + AddressResolver result = addressResolver; + if (result == null) { + synchronized (this) { + result = addressResolver; + if (result == null) { + addressResolver = result = new InetSocketAddressGroupResolver(executor(), this); + } + } + } + return result; + } +} diff --git a/src/dorkbox/network/dns/resolver/InetSocketAddressGroupResolver.java b/src/dorkbox/network/dns/resolver/InetSocketAddressGroupResolver.java new file mode 100644 index 00000000..bc15364b --- /dev/null +++ b/src/dorkbox/network/dns/resolver/InetSocketAddressGroupResolver.java @@ -0,0 +1,86 @@ +package dorkbox.network.dns.resolver; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; + +import io.netty.resolver.AbstractAddressResolver; +import io.netty.resolver.NameResolver; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.Promise; + +public class InetSocketAddressGroupResolver extends AbstractAddressResolver { + + final NameResolver> nameResolver; + + /** + * @param executor the {@link EventExecutor} which is used to notify the listeners of the {@link Future} returned + * by {@link #resolve(java.net.SocketAddress)} + * @param nameResolver the {@link NameResolver} used for name resolution + */ + public + InetSocketAddressGroupResolver(EventExecutor executor, NameResolver> nameResolver) { + super(executor, InetSocketAddress.class); + this.nameResolver = nameResolver; + } + + @Override + protected boolean doIsResolved(InetSocketAddress address) { + return !address.isUnresolved(); + } + + @Override + protected void doResolve(final InetSocketAddress unresolvedAddress, final Promise promise) throws Exception { + // Note that InetSocketAddress.getHostName() will never incur a reverse lookup here, + // because an unresolved address always has a host name. + nameResolver.resolve(unresolvedAddress.getHostName()) + .addListener(new FutureListener>() { + @Override + public void operationComplete(Future> future) throws Exception { + if (future.isSuccess()) { + ArrayList arrayList = new ArrayList(); + List now = future.getNow(); + for (InetAddress inetAddress : now) { + arrayList.add(new InetSocketAddress(inetAddress, unresolvedAddress.getPort())); + } + // promise.setSuccess(arrayList); + } else { + promise.setFailure(future.cause()); + } + } + }); + } + + @Override + protected void doResolveAll(final InetSocketAddress unresolvedAddress, final Promise> promise) throws Exception { + // Note that InetSocketAddress.getHostName() will never incur a reverse lookup here, + // because an unresolved address always has a host name. + nameResolver.resolveAll(unresolvedAddress.getHostName()) + .addListener(new FutureListener>>() { + @Override + public void operationComplete(Future>> future) throws Exception { + if (future.isSuccess()) { + List> inetAddresseses = future.getNow(); + List socketAddresses = new ArrayList(inetAddresseses.size()); + for (List inetAddresses : inetAddresseses) { + for (InetAddress inetAddress : inetAddresses) { + socketAddresses.add(new InetSocketAddress(inetAddress, unresolvedAddress.getPort())); + } + } + + promise.setSuccess(socketAddresses); + } else { + promise.setFailure(future.cause()); + } + } + }); + } + + @Override + public void close() { + nameResolver.close(); + } +} diff --git a/src/dorkbox/network/dns/resolver/InflightNameResolver.java b/src/dorkbox/network/dns/resolver/InflightNameResolver.java new file mode 100644 index 00000000..659e1a5c --- /dev/null +++ b/src/dorkbox/network/dns/resolver/InflightNameResolver.java @@ -0,0 +1,149 @@ +/* + * Copyright 2016 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 dorkbox.network.dns.resolver; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import java.util.List; +import java.util.concurrent.ConcurrentMap; + +import io.netty.resolver.NameResolver; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.StringUtil; + +// FIXME(trustin): Find a better name and move it to the 'resolver' module. +public final +class InflightNameResolver implements NameResolver { + + private final EventExecutor executor; + private final NameResolver delegate; + private final ConcurrentMap> resolvesInProgress; + private final ConcurrentMap>> resolveAllsInProgress; + + InflightNameResolver(EventExecutor executor, + NameResolver delegate, + ConcurrentMap> resolvesInProgress, + ConcurrentMap>> resolveAllsInProgress) { + + this.executor = checkNotNull(executor, "executor"); + this.delegate = checkNotNull(delegate, "delegate"); + this.resolvesInProgress = checkNotNull(resolvesInProgress, "resolvesInProgress"); + this.resolveAllsInProgress = checkNotNull(resolveAllsInProgress, "resolveAllsInProgress"); + } + + @Override + public + Future resolve(String inetHost) { + return resolve(inetHost, executor.newPromise()); + } + + @Override + public + Promise resolve(String inetHost, Promise promise) { + return resolve(resolvesInProgress, inetHost, promise, false); + } + + @Override + public + Future> resolveAll(String inetHost) { + return resolveAll(inetHost, executor.>newPromise()); + } + + @Override + public + Promise> resolveAll(String inetHost, Promise> promise) { + return resolve(resolveAllsInProgress, inetHost, promise, true); + } + + @Override + public + void close() { + delegate.close(); + } + + private + Promise resolve(final ConcurrentMap> resolveMap, + final String inetHost, + final Promise promise, + boolean resolveAll) { + + final Promise earlyPromise = resolveMap.putIfAbsent(inetHost, promise); + if (earlyPromise != null) { + // Name resolution for the specified inetHost is in progress already. + if (earlyPromise.isDone()) { + transferResult(earlyPromise, promise); + } + else { + earlyPromise.addListener(new FutureListener() { + @Override + public + void operationComplete(Future f) throws Exception { + transferResult(f, promise); + } + }); + } + } + else { + try { + if (resolveAll) { + @SuppressWarnings("unchecked") + final Promise> castPromise = (Promise>) promise; // U is List + delegate.resolveAll(inetHost, castPromise); + } + else { + @SuppressWarnings("unchecked") + final Promise castPromise = (Promise) promise; // U is T + delegate.resolve(inetHost, castPromise); + } + } finally { + if (promise.isDone()) { + resolveMap.remove(inetHost); + } + else { + promise.addListener(new FutureListener() { + @Override + public + void operationComplete(Future f) throws Exception { + resolveMap.remove(inetHost); + } + }); + } + } + } + + return promise; + } + + private static + void transferResult(Future src, Promise dst) { + if (src.isSuccess()) { + dst.trySuccess(src.getNow()); + } + else { + dst.tryFailure(src.cause()); + } + } + + @Override + public + String toString() { + return StringUtil.simpleClassName(this) + '(' + delegate + ')'; + } +} diff --git a/src/dorkbox/network/dns/resolver/MultiDnsServerAddressStreamProvider.java b/src/dorkbox/network/dns/resolver/MultiDnsServerAddressStreamProvider.java new file mode 100644 index 00000000..a025260f --- /dev/null +++ b/src/dorkbox/network/dns/resolver/MultiDnsServerAddressStreamProvider.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver; + +import java.util.List; + +import dorkbox.network.dns.resolver.addressProvider.DnsServerAddressStream; +import dorkbox.network.dns.resolver.addressProvider.DnsServerAddressStreamProvider; +import io.netty.util.internal.UnstableApi; + +/** + * A {@link DnsServerAddressStreamProvider} which iterates through a collection of + * {@link DnsServerAddressStreamProvider} until the first non-{@code null} result is found. + */ +@UnstableApi +public final +class MultiDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { + private final DnsServerAddressStreamProvider[] providers; + + /** + * Create a new instance. + * + * @param providers The providers to use for DNS resolution. They will be queried in order. + */ + public + MultiDnsServerAddressStreamProvider(List providers) { + this.providers = providers.toArray(new DnsServerAddressStreamProvider[0]); + } + + /** + * Create a new instance. + * + * @param providers The providers to use for DNS resolution. They will be queried in order. + */ + public + MultiDnsServerAddressStreamProvider(DnsServerAddressStreamProvider... providers) { + this.providers = providers.clone(); + } + + @Override + public + DnsServerAddressStream nameServerAddressStream(String hostname) { + for (DnsServerAddressStreamProvider provider : providers) { + DnsServerAddressStream stream = provider.nameServerAddressStream(hostname); + if (stream != null) { + return stream; + } + } + return null; + } +} diff --git a/src/dorkbox/network/dns/resolver/NoopDnsQueryLifecycleObserver.java b/src/dorkbox/network/dns/resolver/NoopDnsQueryLifecycleObserver.java new file mode 100644 index 00000000..0e99df5b --- /dev/null +++ b/src/dorkbox/network/dns/resolver/NoopDnsQueryLifecycleObserver.java @@ -0,0 +1,69 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver; + +import java.net.InetSocketAddress; +import java.util.List; + +import dorkbox.network.dns.records.DnsMessage; +import io.netty.channel.ChannelFuture; + +final +class NoopDnsQueryLifecycleObserver implements DnsQueryLifecycleObserver { + static final NoopDnsQueryLifecycleObserver INSTANCE = new NoopDnsQueryLifecycleObserver(); + + private + NoopDnsQueryLifecycleObserver() { + } + + @Override + public + void queryWritten(InetSocketAddress dnsServerAddress, ChannelFuture future) { + } + + @Override + public + void queryCancelled(int queriesRemaining) { + } + + @Override + public + DnsQueryLifecycleObserver queryRedirected(List nameServers) { + return this; + } + + @Override + public + DnsQueryLifecycleObserver queryCNAMEd(DnsMessage cnameQuestion) { + return this; + } + + @Override + public + DnsQueryLifecycleObserver queryNoAnswer(int code) { + return this; + } + + @Override + public + void queryFailed(Throwable cause) { + } + + @Override + public + void querySucceed() { + } +} diff --git a/src/io/nettyxbill/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java b/src/dorkbox/network/dns/resolver/NoopDnsQueryLifecycleObserverFactory.java similarity index 96% rename from src/io/nettyxbill/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java rename to src/dorkbox/network/dns/resolver/NoopDnsQueryLifecycleObserverFactory.java index d8de8399..0ab8d4c7 100644 --- a/src/io/nettyxbill/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java +++ b/src/dorkbox/network/dns/resolver/NoopDnsQueryLifecycleObserverFactory.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -package io.nettyxbill.resolver.dns; +package dorkbox.network.dns.resolver; import dorkbox.network.dns.records.DnsMessage; import io.netty.util.internal.UnstableApi; diff --git a/src/dorkbox/network/dns/resolver/TraceDnsQueryLifeCycleObserverFactory.java b/src/dorkbox/network/dns/resolver/TraceDnsQueryLifeCycleObserverFactory.java new file mode 100644 index 00000000..0a8dbdc6 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/TraceDnsQueryLifeCycleObserverFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import dorkbox.network.dns.records.DnsMessage; +import io.netty.util.internal.logging.InternalLogLevel; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +final +class TraceDnsQueryLifeCycleObserverFactory implements DnsQueryLifecycleObserverFactory { + private static final InternalLogger DEFAULT_LOGGER = InternalLoggerFactory.getInstance(TraceDnsQueryLifeCycleObserverFactory.class); + private static final InternalLogLevel DEFAULT_LEVEL = InternalLogLevel.DEBUG; + private final InternalLogger logger; + private final InternalLogLevel level; + + TraceDnsQueryLifeCycleObserverFactory() { + this(DEFAULT_LOGGER, DEFAULT_LEVEL); + } + + TraceDnsQueryLifeCycleObserverFactory(InternalLogger logger, InternalLogLevel level) { + this.logger = checkNotNull(logger, "logger"); + this.level = checkNotNull(level, "level"); + } + + @Override + public + DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsMessage question) { + return new TraceDnsQueryLifecycleObserver(question, logger, level); + } +} diff --git a/src/dorkbox/network/dns/resolver/TraceDnsQueryLifecycleObserver.java b/src/dorkbox/network/dns/resolver/TraceDnsQueryLifecycleObserver.java new file mode 100644 index 00000000..b87882ba --- /dev/null +++ b/src/dorkbox/network/dns/resolver/TraceDnsQueryLifecycleObserver.java @@ -0,0 +1,94 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import java.net.InetSocketAddress; +import java.util.List; + +import dorkbox.network.dns.records.DnsMessage; +import io.netty.channel.ChannelFuture; +import io.netty.util.internal.logging.InternalLogLevel; +import io.netty.util.internal.logging.InternalLogger; + +final +class TraceDnsQueryLifecycleObserver implements DnsQueryLifecycleObserver { + private final InternalLogger logger; + private final InternalLogLevel level; + private final DnsMessage question; + private InetSocketAddress dnsServerAddress; + + TraceDnsQueryLifecycleObserver(DnsMessage question, InternalLogger logger, InternalLogLevel level) { + this.question = checkNotNull(question, "question"); + this.logger = checkNotNull(logger, "logger"); + this.level = checkNotNull(level, "level"); + } + + @Override + public + void queryWritten(InetSocketAddress dnsServerAddress, ChannelFuture future) { + this.dnsServerAddress = dnsServerAddress; + } + + @Override + public + void queryCancelled(int queriesRemaining) { + if (dnsServerAddress != null) { + logger.log(level, "from {} : {} cancelled with {} queries remaining", dnsServerAddress, question, queriesRemaining); + } + else { + logger.log(level, "{} query never written and cancelled with {} queries remaining", question, queriesRemaining); + } + } + + @Override + public + DnsQueryLifecycleObserver queryRedirected(List nameServers) { + logger.log(level, "from {} : {} redirected", dnsServerAddress, question); + return this; + } + + @Override + public + DnsQueryLifecycleObserver queryCNAMEd(DnsMessage cnameQuestion) { + logger.log(level, "from {} : {} CNAME question {}", dnsServerAddress, question, cnameQuestion); + return this; + } + + @Override + public + DnsQueryLifecycleObserver queryNoAnswer(int code) { + logger.log(level, "from {} : {} no answer {}", dnsServerAddress, question, code); + return this; + } + + @Override + public + void queryFailed(Throwable cause) { + if (dnsServerAddress != null) { + logger.log(level, "from {} : {} failure", dnsServerAddress, question, cause); + } + else { + logger.log(level, "{} query never written and failed", question, cause); + } + } + + @Override + public + void querySucceed() { + } +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/DefaultDnsServerAddressStreamProvider.java b/src/dorkbox/network/dns/resolver/addressProvider/DefaultDnsServerAddressStreamProvider.java new file mode 100644 index 00000000..0af2c4a5 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/DefaultDnsServerAddressStreamProvider.java @@ -0,0 +1,168 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver.addressProvider; + +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Hashtable; +import java.util.List; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; + +import io.netty.util.internal.SocketUtils; +import io.netty.util.internal.UnstableApi; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +/** + * A {@link DnsServerAddressStreamProvider} which will use predefined default DNS servers to use for DNS resolution. + * These defaults do not respect your host's machines defaults. + *

+ * This may use the JDK's blocking DNS resolution to bootstrap the default DNS server addresses. + */ +@UnstableApi +public final +class DefaultDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultDnsServerAddressStreamProvider.class); + public static final DefaultDnsServerAddressStreamProvider INSTANCE = new DefaultDnsServerAddressStreamProvider(); + + private static final List DEFAULT_NAME_SERVER_LIST; + private static final InetSocketAddress[] DEFAULT_NAME_SERVER_ARRAY; + private static final DnsServerAddresses DEFAULT_NAME_SERVERS; + + public static final int DNS_PORT = 53; + + static { + final List defaultNameServers = new ArrayList(2); + + // Using jndi-dns to obtain the default name servers. + // + // See: + // - http://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-dns.html + // - http://mail.openjdk.java.net/pipermail/net-dev/2017-March/010695.html + Hashtable env = new Hashtable(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); + env.put("java.naming.provider.url", "dns://"); + try { + DirContext ctx = new InitialDirContext(env); + String dnsUrls = (String) ctx.getEnvironment() + .get("java.naming.provider.url"); + String[] servers = dnsUrls.split(" "); + for (String server : servers) { + try { + defaultNameServers.add(SocketUtils.socketAddress(new URI(server).getHost(), DNS_PORT)); + } catch (URISyntaxException e) { + logger.debug("Skipping a malformed nameserver URI: {}", server, e); + } + } + } catch (NamingException ignore) { + // Will try reflection if this fails. + } + + if (defaultNameServers.isEmpty()) { + 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); + for (String a : list) { + if (a != null) { + defaultNameServers.add(new InetSocketAddress(SocketUtils.addressByName(a), DNS_PORT)); + } + } + } catch (Exception ignore) { + // Failed to get the system name server list via reflection. + // 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, + SocketUtils.socketAddress("8.8.8.8", DNS_PORT), + SocketUtils.socketAddress("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()]); + DEFAULT_NAME_SERVERS = DnsServerAddresses.sequential(DEFAULT_NAME_SERVER_ARRAY); + } + + private + DefaultDnsServerAddressStreamProvider() { + } + + @Override + public + DnsServerAddressStream nameServerAddressStream(String hostname) { + return DEFAULT_NAME_SERVERS.stream(); + } + + /** + * 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. + */ + public static + List defaultAddressList() { + return DEFAULT_NAME_SERVER_LIST; + } + + /** + * Returns the {@link DnsServerAddresses} that yields the system DNS server addresses sequentially. If it failed to + * retrieve the list of the system DNS server addresses from the environment, it will use {@code "8.8.8.8"} and + * {@code "8.8.4.4"}, the addresses of the Google public DNS servers. + *

+ * This method has the same effect with the following code: + *

+     * DnsServerAddresses.sequential(DnsServerAddresses.defaultAddressList());
+     * 
+ *

+ */ + public static + DnsServerAddresses defaultAddresses() { + return DEFAULT_NAME_SERVERS; + } + + /** + * Get the array form of {@link #defaultAddressList()}. + * + * @return The array form of {@link #defaultAddressList()}. + */ + static + InetSocketAddress[] defaultAddressArray() { + return DEFAULT_NAME_SERVER_ARRAY.clone(); + } +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/DefaultDnsServerAddresses.java b/src/dorkbox/network/dns/resolver/addressProvider/DefaultDnsServerAddresses.java new file mode 100644 index 00000000..c20b4796 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/DefaultDnsServerAddresses.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package dorkbox.network.dns.resolver.addressProvider; + +import java.net.InetSocketAddress; + +abstract +class DefaultDnsServerAddresses extends DnsServerAddresses { + + protected final InetSocketAddress[] addresses; + private final String strVal; + + DefaultDnsServerAddresses(String type, InetSocketAddress[] addresses) { + this.addresses = addresses; + + final StringBuilder buf = new StringBuilder(type.length() + 2 + addresses.length * 16); + buf.append(type) + .append('('); + + for (InetSocketAddress a : addresses) { + buf.append(a) + .append(", "); + } + + buf.setLength(buf.length() - 2); + buf.append(')'); + + strVal = buf.toString(); + } + + @Override + public + String toString() { + return strVal; + } +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/DnsServerAddressStream.java b/src/dorkbox/network/dns/resolver/addressProvider/DnsServerAddressStream.java new file mode 100644 index 00000000..1fb2fa08 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/DnsServerAddressStream.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package dorkbox.network.dns.resolver.addressProvider; + +import java.net.InetSocketAddress; + +import io.netty.util.internal.UnstableApi; + +/** + * An infinite stream of DNS server addresses. + */ +@UnstableApi +public +interface DnsServerAddressStream { + /** + * Retrieves the next DNS server address from the stream. + */ + InetSocketAddress next(); + + /** + * Get the number of times {@link #next()} will return a distinct element before repeating or terminating. + * + * @return the number of times {@link #next()} will return a distinct element before repeating or terminating. + */ + int size(); + + /** + * Duplicate this object. The result of this should be able to be independently iterated over via {@link #next()}. + *

+ * Note that {@link #clone()} isn't used because it may make sense for some implementations to have the following + * relationship {@code x.duplicate() == x}. + * + * @return A duplicate of this object. + */ + DnsServerAddressStream duplicate(); +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/DnsServerAddressStreamProvider.java b/src/dorkbox/network/dns/resolver/addressProvider/DnsServerAddressStreamProvider.java new file mode 100644 index 00000000..42324461 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/DnsServerAddressStreamProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver.addressProvider; + +import io.netty.util.internal.UnstableApi; + +/** + * Provides an opportunity to override which {@link DnsServerAddressStream} is used to resolve a specific hostname. + *

+ * For example this can be used to represent /etc/resolv.conf and + * + * /etc/resolver. + */ +@UnstableApi +public +interface DnsServerAddressStreamProvider { + /** + * Ask this provider for the name servers to query for {@code hostname}. + * + * @param hostname The hostname for which to lookup the DNS server addressed to use. + * If this is the final {@link DnsServerAddressStreamProvider} to be queried then generally empty + * string or {@code '.'} correspond to the default {@link DnsServerAddressStream}. + * + * @return The {@link DnsServerAddressStream} which should be used to resolve {@code hostname}. + */ + DnsServerAddressStream nameServerAddressStream(String hostname); +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/DnsServerAddressStreamProviders.java b/src/dorkbox/network/dns/resolver/addressProvider/DnsServerAddressStreamProviders.java new file mode 100644 index 00000000..ef4b510e --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/DnsServerAddressStreamProviders.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver.addressProvider; + +import io.netty.util.internal.UnstableApi; + +/** + * Utility methods related to {@link DnsServerAddressStreamProvider}. + */ +@UnstableApi +public final +class DnsServerAddressStreamProviders { + // TODO(scott): how is this done on Windows? This may require a JNI call to GetNetworkParams + // https://msdn.microsoft.com/en-us/library/aa365968(VS.85).aspx. + private static final DnsServerAddressStreamProvider DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER = UnixResolverDnsServerAddressStreamProvider.parseSilently(); + + private + DnsServerAddressStreamProviders() { + } + + /** + * A {@link DnsServerAddressStreamProvider} which inherits the DNS servers from your local host's configuration. + *

+ * Note that only macOS and Linux are currently supported. + * + * @return A {@link DnsServerAddressStreamProvider} which inherits the DNS servers from your local host's + * configuration. + */ + public static + DnsServerAddressStreamProvider platformDefault() { + return DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER; + } +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/DnsServerAddresses.java b/src/dorkbox/network/dns/resolver/addressProvider/DnsServerAddresses.java new file mode 100644 index 00000000..2c69cedc --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/DnsServerAddresses.java @@ -0,0 +1,239 @@ +/* + * 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 dorkbox.network.dns.resolver.addressProvider; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import dorkbox.network.dns.resolver.DnsNameResolver; +import io.netty.util.internal.UnstableApi; + +/** + * Provides an infinite sequence of DNS server addresses to {@link DnsNameResolver}. + */ +@UnstableApi +@SuppressWarnings("IteratorNextCanNotThrowNoSuchElementException") +public abstract +class DnsServerAddresses { + /** + * @deprecated Use {@link DefaultDnsServerAddressStreamProvider#defaultAddressList()}. + *

+ * 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. + */ + @Deprecated + public static + List defaultAddressList() { + return DefaultDnsServerAddressStreamProvider.defaultAddressList(); + } + + /** + * @deprecated Use {@link DefaultDnsServerAddressStreamProvider#defaultAddresses()}. + *

+ * Returns the {@link DnsServerAddresses} that yields the system DNS server addresses sequentially. If it failed to + * retrieve the list of the system DNS server addresses from the environment, it will use {@code "8.8.8.8"} and + * {@code "8.8.4.4"}, the addresses of the Google public DNS servers. + *

+ * This method has the same effect with the following code: + *

+     *         DnsServerAddresses.sequential(DnsServerAddresses.defaultAddressList());
+     *         
+ *

+ */ + @Deprecated + public static + DnsServerAddresses defaultAddresses() { + return DefaultDnsServerAddressStreamProvider.defaultAddresses(); + } + + /** + * Returns the {@link DnsServerAddresses} that yields the specified {@code addresses} sequentially. Once the + * last address is yielded, it will start again from the first address. + */ + public static + DnsServerAddresses sequential(Iterable addresses) { + return sequential0(sanitize(addresses)); + } + + private static + DnsServerAddresses sequential0(final InetSocketAddress... addresses) { + if (addresses.length == 1) { + return singleton(addresses[0]); + } + + return new DefaultDnsServerAddresses("sequential", addresses) { + @Override + public + DnsServerAddressStream stream() { + return new SequentialDnsServerAddressStream(addresses, 0); + } + }; + } + + /** + * Returns the {@link DnsServerAddresses} that yields only a single {@code address}. + */ + public static + DnsServerAddresses 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 SingletonDnsServerAddresses(address); + } + + 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()) { + throw new IllegalArgumentException("empty addresses"); + } + + return list.toArray(new InetSocketAddress[list.size()]); + } + + /** + * Returns the {@link DnsServerAddresses} that yields the specified {@code addresses} sequentially. Once the + * last address is yielded, it will start again from the first address. + */ + public static + DnsServerAddresses sequential(InetSocketAddress... addresses) { + return sequential0(sanitize(addresses)); + } + + 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 DefaultDnsServerAddressStreamProvider.defaultAddressArray(); + } + + return list.toArray(new InetSocketAddress[list.size()]); + } + + /** + * Returns the {@link DnsServerAddresses} that yields the specified {@code address} in a shuffled order. Once all + * addresses are yielded, the addresses are shuffled again. + */ + public static + DnsServerAddresses shuffled(Iterable addresses) { + return shuffled0(sanitize(addresses)); + } + + private static + DnsServerAddresses shuffled0(final InetSocketAddress[] addresses) { + if (addresses.length == 1) { + return singleton(addresses[0]); + } + + return new DefaultDnsServerAddresses("shuffled", addresses) { + @Override + public + DnsServerAddressStream stream() { + return new ShuffledDnsServerAddressStream(addresses); + } + }; + } + + /** + * Returns the {@link DnsServerAddresses} that yields the specified {@code addresses} in a shuffled order. Once all + * addresses are yielded, the addresses are shuffled again. + */ + public static + DnsServerAddresses shuffled(InetSocketAddress... addresses) { + return shuffled0(sanitize(addresses)); + } + + /** + * Returns the {@link DnsServerAddresses} that yields the specified {@code addresses} in a rotational sequential + * order. It is similar to {@link #sequential(Iterable)}, but each {@link DnsServerAddressStream} starts from + * a different starting point. For example, the first {@link #stream()} will start from the first address, the + * second one will start from the second address, and so on. + */ + public static + DnsServerAddresses rotational(Iterable addresses) { + return rotational0(sanitize(addresses)); + } + + private static + DnsServerAddresses rotational0(final InetSocketAddress[] addresses) { + if (addresses.length == 1) { + return singleton(addresses[0]); + } + + return new RotationalDnsServerAddresses(addresses); + } + + /** + * Returns the {@link DnsServerAddresses} that yields the specified {@code addresses} in a rotational sequential + * order. It is similar to {@link #sequential(Iterable)}, but each {@link DnsServerAddressStream} starts from + * a different starting point. For example, the first {@link #stream()} will start from the first address, the + * second one will start from the second address, and so on. + */ + public static + DnsServerAddresses rotational(InetSocketAddress... addresses) { + return rotational0(sanitize(addresses)); + } + + /** + * Starts a new infinite stream of DNS server addresses. This method is invoked by {@link DnsNameResolver} on every + * uncached {@link DnsNameResolver#resolve(String)}or {@link DnsNameResolver#resolveAll(String)}. + */ + public abstract + DnsServerAddressStream stream(); +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/RotationalDnsServerAddresses.java b/src/dorkbox/network/dns/resolver/addressProvider/RotationalDnsServerAddresses.java new file mode 100644 index 00000000..67c905e2 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/RotationalDnsServerAddresses.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package dorkbox.network.dns.resolver.addressProvider; + +import java.net.InetSocketAddress; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +final +class RotationalDnsServerAddresses extends DefaultDnsServerAddresses { + + private static final AtomicIntegerFieldUpdater startIdxUpdater = AtomicIntegerFieldUpdater.newUpdater( + RotationalDnsServerAddresses.class, + "startIdx"); + + @SuppressWarnings("UnusedDeclaration") + private volatile int startIdx; + + RotationalDnsServerAddresses(InetSocketAddress[] addresses) { + super("rotational", addresses); + } + + @Override + public + DnsServerAddressStream stream() { + for (; ; ) { + int curStartIdx = startIdx; + int nextStartIdx = curStartIdx + 1; + if (nextStartIdx >= addresses.length) { + nextStartIdx = 0; + } + if (startIdxUpdater.compareAndSet(this, curStartIdx, nextStartIdx)) { + return new SequentialDnsServerAddressStream(addresses, curStartIdx); + } + } + } +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/SequentialDnsServerAddressStream.java b/src/dorkbox/network/dns/resolver/addressProvider/SequentialDnsServerAddressStream.java new file mode 100644 index 00000000..62f6cff3 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/SequentialDnsServerAddressStream.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package dorkbox.network.dns.resolver.addressProvider; + +import java.net.InetSocketAddress; + +final +class SequentialDnsServerAddressStream implements DnsServerAddressStream { + + private final InetSocketAddress[] addresses; + private int i; + + SequentialDnsServerAddressStream(InetSocketAddress[] addresses, int startIdx) { + this.addresses = addresses; + i = startIdx; + } + + @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 + int size() { + return addresses.length; + } + + @Override + public + SequentialDnsServerAddressStream duplicate() { + return new SequentialDnsServerAddressStream(addresses, i); + } + + @Override + public + String toString() { + return toString("sequential", i, addresses); + } + + static + String toString(String type, int index, InetSocketAddress[] addresses) { + final StringBuilder buf = new StringBuilder(type.length() + 2 + addresses.length * 16); + buf.append(type) + .append("(index: ") + .append(index); + buf.append(", addrs: ("); + for (InetSocketAddress a : addresses) { + buf.append(a) + .append(", "); + } + + buf.setLength(buf.length() - 2); + buf.append("))"); + + return buf.toString(); + } +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/SequentialDnsServerAddressStreamProvider.java b/src/dorkbox/network/dns/resolver/addressProvider/SequentialDnsServerAddressStreamProvider.java new file mode 100644 index 00000000..33abcd02 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/SequentialDnsServerAddressStreamProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver.addressProvider; + +import java.net.InetSocketAddress; + +import io.netty.util.internal.UnstableApi; + +/** + * A {@link DnsServerAddressStreamProvider} which is backed by a sequential list of DNS servers. + */ +@UnstableApi +public final +class SequentialDnsServerAddressStreamProvider + extends UniSequentialDnsServerAddressStreamProvider { + /** + * Create a new instance. + * + * @param addresses The addresses which will be be returned in sequential order via + * {@link #nameServerAddressStream(String)} + */ + public + SequentialDnsServerAddressStreamProvider(InetSocketAddress... addresses) { + super(DnsServerAddresses.sequential(addresses)); + } + + /** + * Create a new instance. + * + * @param addresses The addresses which will be be returned in sequential order via + * {@link #nameServerAddressStream(String)} + */ + public + SequentialDnsServerAddressStreamProvider(Iterable addresses) { + super(DnsServerAddresses.sequential(addresses)); + } +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/ShuffledDnsServerAddressStream.java b/src/dorkbox/network/dns/resolver/addressProvider/ShuffledDnsServerAddressStream.java new file mode 100644 index 00000000..7298822a --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/ShuffledDnsServerAddressStream.java @@ -0,0 +1,93 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package dorkbox.network.dns.resolver.addressProvider; + +import java.net.InetSocketAddress; +import java.util.Random; + +import io.netty.util.internal.PlatformDependent; + +final +class ShuffledDnsServerAddressStream implements DnsServerAddressStream { + + private final InetSocketAddress[] addresses; + private int i; + + /** + * Create a new instance. + * + * @param addresses The addresses are not cloned. It is assumed the caller has cloned this array or otherwise will + * not modify the contents. + */ + ShuffledDnsServerAddressStream(InetSocketAddress[] addresses) { + this.addresses = addresses; + + shuffle(); + } + + private + void shuffle() { + final InetSocketAddress[] addresses = this.addresses; + final Random r = PlatformDependent.threadLocalRandom(); + + 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; + } + } + + private + ShuffledDnsServerAddressStream(InetSocketAddress[] addresses, int startIdx) { + this.addresses = addresses; + i = startIdx; + } + + @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 + int size() { + return addresses.length; + } + + @Override + public + ShuffledDnsServerAddressStream duplicate() { + return new ShuffledDnsServerAddressStream(addresses, i); + } + + @Override + public + String toString() { + return SequentialDnsServerAddressStream.toString("shuffled", i, addresses); + } +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/SingletonDnsServerAddressStreamProvider.java b/src/dorkbox/network/dns/resolver/addressProvider/SingletonDnsServerAddressStreamProvider.java new file mode 100644 index 00000000..a1de14d0 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/SingletonDnsServerAddressStreamProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver.addressProvider; + +import java.net.InetSocketAddress; + +import io.netty.util.internal.UnstableApi; + +/** + * A {@link DnsServerAddressStreamProvider} which always uses a single DNS server for resolution. + */ +@UnstableApi +public final +class SingletonDnsServerAddressStreamProvider + extends UniSequentialDnsServerAddressStreamProvider { + /** + * Create a new instance. + * + * @param address The singleton address to use for every DNS resolution. + */ + public + SingletonDnsServerAddressStreamProvider(final InetSocketAddress address) { + super(DnsServerAddresses.singleton(address)); + } +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/SingletonDnsServerAddresses.java b/src/dorkbox/network/dns/resolver/addressProvider/SingletonDnsServerAddresses.java new file mode 100644 index 00000000..0fe2e49f --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/SingletonDnsServerAddresses.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package dorkbox.network.dns.resolver.addressProvider; + +import java.net.InetSocketAddress; + +final +class SingletonDnsServerAddresses extends DnsServerAddresses { + + private final InetSocketAddress address; + + private final DnsServerAddressStream stream = new DnsServerAddressStream() { + @Override + public + InetSocketAddress next() { + return address; + } + + @Override + public + int size() { + return 1; + } + + @Override + public + DnsServerAddressStream duplicate() { + return this; + } + + @Override + public + String toString() { + return SingletonDnsServerAddresses.this.toString(); + } + }; + + SingletonDnsServerAddresses(InetSocketAddress address) { + this.address = address; + } + + @Override + public + DnsServerAddressStream stream() { + return stream; + } + + @Override + public + String toString() { + return "singleton(" + address + ")"; + } +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/UniSequentialDnsServerAddressStreamProvider.java b/src/dorkbox/network/dns/resolver/addressProvider/UniSequentialDnsServerAddressStreamProvider.java new file mode 100644 index 00000000..d5724d27 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/UniSequentialDnsServerAddressStreamProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver.addressProvider; + +import io.netty.util.internal.ObjectUtil; + +/** + * A {@link DnsServerAddressStreamProvider} which is backed by a single {@link DnsServerAddresses}. + */ +abstract +class UniSequentialDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { + private final DnsServerAddresses addresses; + + UniSequentialDnsServerAddressStreamProvider(DnsServerAddresses addresses) { + this.addresses = ObjectUtil.checkNotNull(addresses, "addresses"); + } + + @Override + public final + DnsServerAddressStream nameServerAddressStream(String hostname) { + return addresses.stream(); + } +} diff --git a/src/dorkbox/network/dns/resolver/addressProvider/UnixResolverDnsServerAddressStreamProvider.java b/src/dorkbox/network/dns/resolver/addressProvider/UnixResolverDnsServerAddressStreamProvider.java new file mode 100644 index 00000000..700ee8cf --- /dev/null +++ b/src/dorkbox/network/dns/resolver/addressProvider/UnixResolverDnsServerAddressStreamProvider.java @@ -0,0 +1,312 @@ +/* + * Copyright 2017 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 dorkbox.network.dns.resolver.addressProvider; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.StringUtil.indexOfNonWhiteSpace; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.netty.util.NetUtil; +import io.netty.util.internal.SocketUtils; +import io.netty.util.internal.UnstableApi; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +/** + * Able to parse files such as /etc/resolv.conf and + * + * /etc/resolver to respect the system default domain servers. + */ +@UnstableApi +public final +class UnixResolverDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(UnixResolverDnsServerAddressStreamProvider.class); + + private static final String ETC_RESOLV_CONF_FILE = "/etc/resolv.conf"; + private static final String ETC_RESOLVER_DIR = "/etc/resolver"; + private static final String NAMESERVER_ROW_LABEL = "nameserver"; + private static final String SORTLIST_ROW_LABEL = "sortlist"; + private static final String OPTIONS_ROW_LABEL = "options"; + private static final String DOMAIN_ROW_LABEL = "domain"; + private static final String PORT_ROW_LABEL = "port"; + + private static final String NDOTS_LABEL = "ndots:"; + + public static final int DEFAULT_NDOTS = 1; + + private final DnsServerAddresses defaultNameServerAddresses; + private final Map domainToNameServerStreamMap; + + /** + * Attempt to parse {@code /etc/resolv.conf} and files in the {@code /etc/resolver} directory by default. + * A failure to parse will return {@link DefaultDnsServerAddressStreamProvider}. + */ + static + DnsServerAddressStreamProvider parseSilently() { + try { + UnixResolverDnsServerAddressStreamProvider nameServerCache = new UnixResolverDnsServerAddressStreamProvider(ETC_RESOLV_CONF_FILE, + ETC_RESOLVER_DIR); + return nameServerCache.mayOverrideNameServers() ? nameServerCache : DefaultDnsServerAddressStreamProvider.INSTANCE; + } catch (Exception e) { + logger.debug("failed to parse {} and/or {}", ETC_RESOLV_CONF_FILE, ETC_RESOLVER_DIR, e); + return DefaultDnsServerAddressStreamProvider.INSTANCE; + } + } + + private + boolean mayOverrideNameServers() { + return !domainToNameServerStreamMap.isEmpty() || defaultNameServerAddresses.stream() + .next() != null; + } + + /** + * Parse a file of the format /etc/resolv.conf which may contain + * the default DNS server to use, and also overrides for individual domains. Also parse a directory of the format + * + * /etc/resolver which may contain multiple files to override the name servers used for multimple domains. + * + * @param etcResolvConf /etc/resolv.conf. + * @param etcResolverDir Directory containing files of the format defined in + * + * /etc/resolver. + * + * @throws IOException If an error occurs while parsing the input files. + */ + public + UnixResolverDnsServerAddressStreamProvider(String etcResolvConf, String etcResolverDir) throws IOException { + this(etcResolvConf == null ? null : new File(etcResolvConf), etcResolverDir == null ? null : new File(etcResolverDir).listFiles()); + } + + /** + * Parse a file of the format /etc/resolv.conf which may contain + * the default DNS server to use, and also overrides for individual domains. Also parse list of files of the format + * + * /etc/resolver which may contain multiple files to override the name servers used for multimple domains. + * + * @param etcResolvConf /etc/resolv.conf. + * @param etcResolverFiles List of files of the format defined in + * + * /etc/resolver. + * + * @throws IOException If an error occurs while parsing the input files. + */ + public + UnixResolverDnsServerAddressStreamProvider(File etcResolvConf, File... etcResolverFiles) throws IOException { + Map etcResolvConfMap = parse(checkNotNull(etcResolvConf, "etcResolvConf")); + final boolean useEtcResolverFiles = etcResolverFiles != null && etcResolverFiles.length != 0; + domainToNameServerStreamMap = useEtcResolverFiles ? parse(etcResolverFiles) : etcResolvConfMap; + + DnsServerAddresses defaultNameServerAddresses = etcResolvConfMap.get(etcResolvConf.getName()); + if (defaultNameServerAddresses == null) { + Collection values = etcResolvConfMap.values(); + if (values.isEmpty()) { + throw new IllegalArgumentException(etcResolvConf + " didn't provide any name servers"); + } + this.defaultNameServerAddresses = values.iterator() + .next(); + } + else { + this.defaultNameServerAddresses = defaultNameServerAddresses; + } + + if (useEtcResolverFiles) { + domainToNameServerStreamMap.putAll(etcResolvConfMap); + } + } + + private static + Map parse(File... etcResolverFiles) throws IOException { + Map domainToNameServerStreamMap = new HashMap(etcResolverFiles.length << 1); + for (File etcResolverFile : etcResolverFiles) { + if (!etcResolverFile.isFile()) { + continue; + } + FileReader fr = new FileReader(etcResolverFile); + BufferedReader br = null; + try { + br = new BufferedReader(fr); + List addresses = new ArrayList(2); + String domainName = etcResolverFile.getName(); + int port = DefaultDnsServerAddressStreamProvider.DNS_PORT; + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + char c; + if (line.isEmpty() || (c = line.charAt(0)) == '#' || c == ';') { + continue; + } + if (line.startsWith(NAMESERVER_ROW_LABEL)) { + int i = indexOfNonWhiteSpace(line, NAMESERVER_ROW_LABEL.length()); + if (i < 0) { + throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL + " in file " + + etcResolverFile + ". value: " + line); + } + String maybeIP = line.substring(i); + // There may be a port appended onto the IP address so we attempt to extract it. + if (!NetUtil.isValidIpV4Address(maybeIP) && !NetUtil.isValidIpV6Address(maybeIP)) { + i = maybeIP.lastIndexOf('.'); + if (i + 1 >= maybeIP.length()) { + throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL + " in file " + + etcResolverFile + ". invalid IP value: " + line); + } + port = Integer.parseInt(maybeIP.substring(i + 1)); + maybeIP = maybeIP.substring(0, i); + } + addresses.add(SocketUtils.socketAddress(maybeIP, port)); + } + else if (line.startsWith(DOMAIN_ROW_LABEL)) { + int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length()); + if (i < 0) { + throw new IllegalArgumentException("error parsing label " + DOMAIN_ROW_LABEL + " in file " + etcResolverFile + + " value: " + line); + } + domainName = line.substring(i); + if (!addresses.isEmpty()) { + putIfAbsent(domainToNameServerStreamMap, domainName, addresses); + } + addresses = new ArrayList(2); + } + else if (line.startsWith(PORT_ROW_LABEL)) { + int i = indexOfNonWhiteSpace(line, PORT_ROW_LABEL.length()); + if (i < 0) { + throw new IllegalArgumentException("error parsing label " + PORT_ROW_LABEL + " in file " + etcResolverFile + + " value: " + line); + } + port = Integer.parseInt(line.substring(i)); + } + else if (line.startsWith(SORTLIST_ROW_LABEL)) { + logger.info("row type {} not supported. ignoring line: {}", SORTLIST_ROW_LABEL, line); + } + } + if (!addresses.isEmpty()) { + putIfAbsent(domainToNameServerStreamMap, domainName, addresses); + } + } finally { + if (br == null) { + fr.close(); + } + else { + br.close(); + } + } + } + return domainToNameServerStreamMap; + } + + private static + void putIfAbsent(Map domainToNameServerStreamMap, String domainName, List addresses) { + // TODO(scott): sortlist is being ignored. + putIfAbsent(domainToNameServerStreamMap, domainName, DnsServerAddresses.sequential(addresses)); + } + + private static + void putIfAbsent(Map domainToNameServerStreamMap, String domainName, DnsServerAddresses addresses) { + DnsServerAddresses existingAddresses = domainToNameServerStreamMap.put(domainName, addresses); + if (existingAddresses != null) { + domainToNameServerStreamMap.put(domainName, existingAddresses); + logger.debug("Domain name {} already maps to addresses {} so new addresses {} will be discarded", + domainName, + existingAddresses, + addresses); + } + } + + @Override + public + DnsServerAddressStream nameServerAddressStream(String hostname) { + for (; ; ) { + int i = hostname.indexOf('.', 1); + if (i < 0 || i == hostname.length() - 1) { + return defaultNameServerAddresses.stream(); + } + + DnsServerAddresses addresses = domainToNameServerStreamMap.get(hostname); + if (addresses != null) { + return addresses.stream(); + } + + hostname = hostname.substring(i + 1); + } + } + + /** + * Parse a file of the format /etc/resolv.conf and return the + * value corresponding to the first ndots in an options configuration. + * + * @return the value corresponding to the first ndots in an options configuration, or {@link #DEFAULT_NDOTS} if not + * found. + * + * @throws IOException If a failure occurs parsing the file. + */ + public static + int parseEtcResolverFirstNdots() throws IOException { + return parseEtcResolverFirstNdots(new File(ETC_RESOLV_CONF_FILE)); + } + + /** + * Parse a file of the format /etc/resolv.conf and return the + * value corresponding to the first ndots in an options configuration. + * + * @param etcResolvConf a file of the format /etc/resolv.conf. + * + * @return the value corresponding to the first ndots in an options configuration, or {@link #DEFAULT_NDOTS} if not + * found. + * + * @throws IOException If a failure occurs parsing the file. + */ + static + int parseEtcResolverFirstNdots(File etcResolvConf) throws IOException { + FileReader fr = new FileReader(etcResolvConf); + BufferedReader br = null; + try { + br = new BufferedReader(fr); + String line; + while ((line = br.readLine()) != null) { + if (line.startsWith(OPTIONS_ROW_LABEL)) { + int i = line.indexOf(NDOTS_LABEL); + if (i >= 0) { + i += NDOTS_LABEL.length(); + final int j = line.indexOf(' ', i); + return Integer.parseInt(line.substring(i, j < 0 ? line.length() : j)); + } + break; + } + } + } finally { + if (br == null) { + fr.close(); + } + else { + br.close(); + } + } + return DEFAULT_NDOTS; + } +} diff --git a/src/dorkbox/network/dns/resolver/cache/DefaultDnsCache.java b/src/dorkbox/network/dns/resolver/cache/DefaultDnsCache.java new file mode 100644 index 00000000..51f7062e --- /dev/null +++ b/src/dorkbox/network/dns/resolver/cache/DefaultDnsCache.java @@ -0,0 +1,224 @@ +/* + * Copyright 2016 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 dorkbox.network.dns.resolver.cache; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +import dorkbox.network.dns.records.DnsRecord; +import io.netty.channel.EventLoop; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.UnstableApi; + +/** + * Default implementation of {@link DnsCache}, backed by a {@link ConcurrentMap}. + * If any additional {@link DnsRecord} is used, no caching takes place. + */ +@UnstableApi +public class DefaultDnsCache implements DnsCache { + + private final ConcurrentMap> resolveCache = PlatformDependent.newConcurrentHashMap(); + private final int minTtl; + private final int maxTtl; + private final int negativeTtl; + + /** + * Create a cache that respects the TTL returned by the DNS server + * and doesn't cache negative responses. + */ + public DefaultDnsCache() { + this(0, Integer.MAX_VALUE, 0); + } + + /** + * Create a cache. + * @param minTtl the minimum TTL + * @param maxTtl the maximum TTL + * @param negativeTtl the TTL for failed queries + */ + public DefaultDnsCache(int minTtl, int maxTtl, int negativeTtl) { + this.minTtl = checkPositiveOrZero(minTtl, "minTtl"); + this.maxTtl = checkPositiveOrZero(maxTtl, "maxTtl"); + if (minTtl > maxTtl) { + throw new IllegalArgumentException( + "minTtl: " + minTtl + ", maxTtl: " + maxTtl + " (expected: 0 <= minTtl <= maxTtl)"); + } + this.negativeTtl = checkPositiveOrZero(negativeTtl, "negativeTtl"); + } + + /** + * Returns the minimum TTL of the cached DNS resource records (in seconds). + * + * @see #maxTtl() + */ + public int minTtl() { + return minTtl; + } + + /** + * Returns the maximum TTL of the cached DNS resource records (in seconds). + * + * @see #minTtl() + */ + public int maxTtl() { + return maxTtl; + } + + /** + * 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. + */ + public int negativeTtl() { + return negativeTtl; + } + + @Override + public void clear() { + for (Iterator>> i = resolveCache.entrySet().iterator(); i.hasNext();) { + final Map.Entry> e = i.next(); + i.remove(); + cancelExpiration(e.getValue()); + } + } + + @Override + public boolean clear(String hostname) { + checkNotNull(hostname, "hostname"); + boolean removed = false; + for (Iterator>> i = resolveCache.entrySet().iterator(); i.hasNext();) { + final Map.Entry> e = i.next(); + if (e.getKey().equals(hostname)) { + i.remove(); + cancelExpiration(e.getValue()); + removed = true; + } + } + return removed; + } + + @Override + public List get(String hostname) { + checkNotNull(hostname, "hostname"); + return resolveCache.get(hostname); + } + + private List cachedEntries(String hostname) { + List oldEntries = resolveCache.get(hostname); + final List entries; + if (oldEntries == null) { + List newEntries = new ArrayList(8); + oldEntries = resolveCache.putIfAbsent(hostname, newEntries); + entries = oldEntries != null? oldEntries : newEntries; + } else { + entries = oldEntries; + } + return entries; + } + + @Override + public void cache(String hostname, InetAddress address, long originalTtl, EventLoop loop) { + checkNotNull(hostname, "hostname"); + checkNotNull(address, "address"); + checkNotNull(loop, "loop"); + if (maxTtl == 0) { + return; + } + final int ttl = Math.max(minTtl, (int) Math.min(maxTtl, originalTtl)); + final List entries = cachedEntries(hostname); + final DnsCacheEntry e = new DnsCacheEntry(hostname, address); + + synchronized (entries) { + if (!entries.isEmpty()) { + final DnsCacheEntry firstEntry = entries.get(0); + if (firstEntry.cause() != null) { + assert entries.size() == 1; + firstEntry.cancelExpiration(); + entries.clear(); + } + } + entries.add(e); + } + + scheduleCacheExpiration(entries, e, ttl, loop); + } + + @Override + public void cache(String hostname, Throwable cause, EventLoop loop) { + checkNotNull(hostname, "hostname"); + checkNotNull(cause, "cause"); + checkNotNull(loop, "loop"); + + if (negativeTtl == 0) { + return; + } + final List entries = cachedEntries(hostname); + final DnsCacheEntry e = new DnsCacheEntry(hostname, cause); + + synchronized (entries) { + final int numEntries = entries.size(); + for (int i = 0; i < numEntries; i ++) { + entries.get(i).cancelExpiration(); + } + entries.clear(); + entries.add(e); + } + + scheduleCacheExpiration(entries, e, negativeTtl, loop); + } + + private static void cancelExpiration(List entries) { + final int numEntries = entries.size(); + for (int i = 0; i < numEntries; i++) { + entries.get(i).cancelExpiration(); + } + } + + private void scheduleCacheExpiration(final List entries, + final DnsCacheEntry e, + int ttl, + EventLoop loop) { + e.scheduleExpiration(loop, new Runnable() { + @Override + public void run() { + synchronized (entries) { + entries.remove(e); + if (entries.isEmpty()) { + resolveCache.remove(e.hostname()); + } + } + } + }, ttl, TimeUnit.SECONDS); + } + + @Override + public String toString() { + return new StringBuilder() + .append("DefaultDnsCache(minTtl=") + .append(minTtl).append(", maxTtl=") + .append(maxTtl).append(", negativeTtl=") + .append(negativeTtl).append(", cached resolved hostname=") + .append(resolveCache.size()).append(")") + .toString(); + } +} diff --git a/src/dorkbox/network/dns/resolver/cache/DnsCache.java b/src/dorkbox/network/dns/resolver/cache/DnsCache.java new file mode 100644 index 00000000..839a04a9 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/cache/DnsCache.java @@ -0,0 +1,68 @@ +/* + * Copyright 2016 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 dorkbox.network.dns.resolver.cache; + +import java.net.InetAddress; +import java.util.List; + +import io.netty.channel.EventLoop; +import io.netty.util.internal.UnstableApi; + +/** + * A cache for DNS resolution entries. + */ +@UnstableApi +public interface DnsCache { + + /** + * Clears all the resolved addresses cached by this resolver. + * + * @see #clear(String) + */ + void clear(); + + /** + * Clears the resolved addresses of the specified host name from the cache of this resolver. + * + * @return {@code true} if and only if there was an entry for the specified host name in the cache and + * it has been removed by this method + */ + boolean clear(String hostname); + + /** + * Return the cached entries for the given hostname. + * @param hostname the hostname + * @return the cached entries + */ + List get(String hostname); + + /** + * Cache a resolved address for a given hostname. + * @param hostname the hostname + * @param address the resolved address + * @param originalTtl the TLL as returned by the DNS server + * @param loop the {@link EventLoop} used to register the TTL timeout + */ + void cache(String hostname, InetAddress address, long originalTtl, EventLoop loop); + + /** + * Cache the resolution failure for a given hostname. + * @param hostname the hostname + * @param cause the resolution failure + * @param loop the {@link EventLoop} used to register the TTL timeout + */ + void cache(String hostname, Throwable cause, EventLoop loop); +} diff --git a/src/dorkbox/network/dns/resolver/cache/DnsCacheEntry.java b/src/dorkbox/network/dns/resolver/cache/DnsCacheEntry.java new file mode 100644 index 00000000..a3694d4c --- /dev/null +++ b/src/dorkbox/network/dns/resolver/cache/DnsCacheEntry.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package dorkbox.network.dns.resolver.cache; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import java.net.InetAddress; +import java.util.concurrent.TimeUnit; + +import io.netty.channel.EventLoop; +import io.netty.util.concurrent.ScheduledFuture; +import io.netty.util.internal.UnstableApi; + +/** + * Entry in {@link DnsCache}. + */ +@UnstableApi +public final class DnsCacheEntry { + + private final String hostname; + private final InetAddress address; + private final Throwable cause; + private volatile ScheduledFuture expirationFuture; + + public DnsCacheEntry(String hostname, InetAddress address) { + this.hostname = checkNotNull(hostname, "hostname"); + this.address = checkNotNull(address, "address"); + cause = null; + } + + public DnsCacheEntry(String hostname, Throwable cause) { + this.hostname = checkNotNull(hostname, "hostname"); + this.cause = checkNotNull(cause, "cause"); + address = null; + } + + public String hostname() { + return hostname; + } + + public InetAddress address() { + return address; + } + + public Throwable cause() { + return cause; + } + + void scheduleExpiration(EventLoop loop, Runnable task, long delay, TimeUnit unit) { + assert expirationFuture == null: "expiration task scheduled already"; + expirationFuture = loop.schedule(task, delay, unit); + } + + void cancelExpiration() { + ScheduledFuture expirationFuture = this.expirationFuture; + if (expirationFuture != null) { + expirationFuture.cancel(false); + } + } + + @Override + public String toString() { + if (cause != null) { + return hostname + '/' + cause; + } else { + return address.toString(); + } + } +} diff --git a/src/dorkbox/network/dns/resolver/cache/NoopDnsCache.java b/src/dorkbox/network/dns/resolver/cache/NoopDnsCache.java new file mode 100644 index 00000000..7e5a11a2 --- /dev/null +++ b/src/dorkbox/network/dns/resolver/cache/NoopDnsCache.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016 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 dorkbox.network.dns.resolver.cache; + +import java.net.InetAddress; +import java.util.Collections; +import java.util.List; + +import io.netty.channel.EventLoop; +import io.netty.util.internal.UnstableApi; + +/** + * A noop DNS cache that actually never caches anything. + */ +@UnstableApi +public final class NoopDnsCache implements DnsCache { + + public static final NoopDnsCache INSTANCE = new NoopDnsCache(); + + /** + * Private singleton constructor. + */ + private NoopDnsCache() { + } + + @Override + public void clear() { + } + + @Override + public boolean clear(String hostname) { + return false; + } + + @Override + public List get(String hostname) { + return Collections.emptyList(); + } + + @Override + public void cache(String hostname, InetAddress address, long originalTtl, EventLoop loop) { + } + + @Override + public void cache(String hostname, Throwable cause, EventLoop loop) { + } + + @Override + public String toString() { + return NoopDnsCache.class.getSimpleName(); + } +}