2014-09-19 15:36:32 +02:00
|
|
|
/*
|
|
|
|
* Copyright 2014 The Netty Project
|
|
|
|
*
|
|
|
|
* The Netty Project licenses this file to you under the Apache License,
|
|
|
|
* version 2.0 (the "License"); you may not use this file except in compliance
|
|
|
|
* with the License. You may obtain a copy of the License at:
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
|
|
* License for the specific language governing permissions and limitations
|
|
|
|
* under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package io.netty.resolver.dns;
|
|
|
|
|
|
|
|
import io.netty.buffer.ByteBuf;
|
2015-03-16 07:46:14 +01:00
|
|
|
import io.netty.buffer.ByteBufHolder;
|
|
|
|
import io.netty.channel.AddressedEnvelope;
|
2017-04-07 03:09:28 +02:00
|
|
|
import io.netty.channel.ChannelPromise;
|
2018-03-29 22:01:25 +02:00
|
|
|
import io.netty.channel.EventLoop;
|
2016-06-13 21:16:22 +02:00
|
|
|
import io.netty.handler.codec.CorruptedFrameException;
|
2015-03-16 07:46:14 +01:00
|
|
|
import io.netty.handler.codec.dns.DefaultDnsQuestion;
|
|
|
|
import io.netty.handler.codec.dns.DefaultDnsRecordDecoder;
|
2014-09-19 15:36:32 +02:00
|
|
|
import io.netty.handler.codec.dns.DnsQuestion;
|
2015-03-16 07:46:14 +01:00
|
|
|
import io.netty.handler.codec.dns.DnsRawRecord;
|
|
|
|
import io.netty.handler.codec.dns.DnsRecord;
|
|
|
|
import io.netty.handler.codec.dns.DnsRecordType;
|
2014-09-19 15:36:32 +02:00
|
|
|
import io.netty.handler.codec.dns.DnsResponse;
|
2017-02-02 10:32:33 +01:00
|
|
|
import io.netty.handler.codec.dns.DnsResponseCode;
|
|
|
|
import io.netty.handler.codec.dns.DnsSection;
|
2014-09-19 15:36:32 +02:00
|
|
|
import io.netty.util.ReferenceCountUtil;
|
|
|
|
import io.netty.util.concurrent.Future;
|
|
|
|
import io.netty.util.concurrent.FutureListener;
|
|
|
|
import io.netty.util.concurrent.Promise;
|
2017-03-31 02:02:16 +02:00
|
|
|
import io.netty.util.internal.ObjectUtil;
|
2017-04-07 03:09:28 +02:00
|
|
|
import io.netty.util.internal.PlatformDependent;
|
2014-09-19 15:36:32 +02:00
|
|
|
import io.netty.util.internal.StringUtil;
|
2017-04-07 03:09:28 +02:00
|
|
|
import io.netty.util.internal.ThrowableUtil;
|
2014-09-19 15:36:32 +02:00
|
|
|
|
|
|
|
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;
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
import static io.netty.resolver.dns.DnsAddressDecoder.decodeAddress;
|
|
|
|
import static io.netty.resolver.dns.DnsNameResolver.trySuccess;
|
2017-04-07 03:09:28 +02:00
|
|
|
import static java.lang.Math.min;
|
|
|
|
import static java.util.Collections.unmodifiableList;
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
abstract class DnsResolveContext<T> {
|
2014-09-19 15:36:32 +02:00
|
|
|
|
2015-03-16 07:46:14 +01:00
|
|
|
private static final FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>> RELEASE_RESPONSE =
|
|
|
|
new FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>>() {
|
|
|
|
@Override
|
|
|
|
public void operationComplete(Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> future) {
|
|
|
|
if (future.isSuccess()) {
|
|
|
|
future.getNow().release();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2017-04-07 03:09:28 +02:00
|
|
|
private static final RuntimeException NXDOMAIN_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace(
|
|
|
|
new RuntimeException("No answer found and NXDOMAIN response code returned"),
|
2018-03-29 22:01:25 +02:00
|
|
|
DnsResolveContext.class,
|
2017-04-07 03:09:28 +02:00
|
|
|
"onResponse(..)");
|
|
|
|
private static final RuntimeException CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace(
|
|
|
|
new RuntimeException("No matching CNAME record found"),
|
2018-03-29 22:01:25 +02:00
|
|
|
DnsResolveContext.class,
|
2017-04-07 03:09:28 +02:00
|
|
|
"onResponseCNAME(..)");
|
|
|
|
private static final RuntimeException NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace(
|
2017-07-03 23:46:36 +02:00
|
|
|
new RuntimeException("No matching record type found"),
|
2018-03-29 22:01:25 +02:00
|
|
|
DnsResolveContext.class,
|
2017-04-07 03:09:28 +02:00
|
|
|
"onResponseAorAAAA(..)");
|
|
|
|
private static final RuntimeException UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace(
|
|
|
|
new RuntimeException("Response type was unrecognized"),
|
2018-03-29 22:01:25 +02:00
|
|
|
DnsResolveContext.class,
|
2017-04-07 03:09:28 +02:00
|
|
|
"onResponse(..)");
|
2017-07-03 23:46:36 +02:00
|
|
|
private static final RuntimeException NAME_SERVERS_EXHAUSTED_EXCEPTION = ThrowableUtil.unknownStackTrace(
|
|
|
|
new RuntimeException("No name servers returned an answer"),
|
2018-03-29 22:01:25 +02:00
|
|
|
DnsResolveContext.class,
|
2017-07-03 23:46:36 +02:00
|
|
|
"tryToFinishResolve(..)");
|
2014-09-19 15:36:32 +02:00
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
final DnsNameResolver parent;
|
2015-08-19 04:51:15 +02:00
|
|
|
private final DnsServerAddressStream nameServerAddrs;
|
2014-09-19 15:36:32 +02:00
|
|
|
private final String hostname;
|
2018-03-29 22:01:25 +02:00
|
|
|
private final int dnsClass;
|
|
|
|
private final DnsRecordType[] expectedTypes;
|
2014-09-19 15:36:32 +02:00
|
|
|
private final int maxAllowedQueries;
|
2016-07-29 10:30:08 +02:00
|
|
|
private final DnsRecord[] additionals;
|
2014-09-19 15:36:32 +02:00
|
|
|
|
2015-03-16 07:46:14 +01:00
|
|
|
private final Set<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> queriesInProgress =
|
|
|
|
Collections.newSetFromMap(
|
|
|
|
new IdentityHashMap<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>, Boolean>());
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
private List<T> finalResult;
|
2014-09-19 15:36:32 +02:00
|
|
|
private int allowedQueries;
|
|
|
|
private boolean triedCNAME;
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
DnsResolveContext(DnsNameResolver parent,
|
|
|
|
String hostname, int dnsClass, DnsRecordType[] expectedTypes,
|
|
|
|
DnsRecord[] additionals, DnsServerAddressStream nameServerAddrs) {
|
|
|
|
|
|
|
|
assert expectedTypes.length > 0;
|
|
|
|
|
2014-09-19 15:36:32 +02:00
|
|
|
this.parent = parent;
|
|
|
|
this.hostname = hostname;
|
2018-03-29 22:01:25 +02:00
|
|
|
this.dnsClass = dnsClass;
|
|
|
|
this.expectedTypes = expectedTypes;
|
2016-07-29 10:30:08 +02:00
|
|
|
this.additionals = additionals;
|
2014-09-19 15:36:32 +02:00
|
|
|
|
2017-03-31 02:02:16 +02:00
|
|
|
this.nameServerAddrs = ObjectUtil.checkNotNull(nameServerAddrs, "nameServerAddrs");
|
2014-09-19 15:36:32 +02:00
|
|
|
maxAllowedQueries = parent.maxQueriesPerResolve();
|
|
|
|
allowedQueries = maxAllowedQueries;
|
|
|
|
}
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
/**
|
|
|
|
* Creates a new context with the given parameters.
|
|
|
|
*/
|
|
|
|
abstract DnsResolveContext<T> newResolverContext(DnsNameResolver parent, String hostname,
|
|
|
|
int dnsClass, DnsRecordType[] expectedTypes,
|
|
|
|
DnsRecord[] additionals,
|
|
|
|
DnsServerAddressStream nameServerAddrs);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts the given {@link DnsRecord} into {@code T}.
|
|
|
|
*/
|
|
|
|
abstract T convertRecord(DnsRecord record, String hostname, DnsRecord[] additionals, EventLoop eventLoop);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns {@code true} if the given list contains any expected records. {@code finalResult} always contains
|
|
|
|
* at least one element.
|
|
|
|
*/
|
|
|
|
abstract boolean containsExpectedResult(List<T> finalResult);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Caches a successful resolution.
|
|
|
|
*/
|
|
|
|
abstract void cache(String hostname, DnsRecord[] additionals,
|
|
|
|
DnsRecord result, T convertedResult);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Caches a failed resolution.
|
|
|
|
*/
|
|
|
|
abstract void cache(String hostname, DnsRecord[] additionals,
|
|
|
|
UnknownHostException cause);
|
|
|
|
|
|
|
|
void resolve(final Promise<List<T>> promise) {
|
2017-11-17 08:15:30 +01:00
|
|
|
final String[] searchDomains = parent.searchDomains();
|
|
|
|
if (searchDomains.length == 0 || parent.ndots() == 0 || StringUtil.endsWith(hostname, '.')) {
|
2016-06-30 23:12:11 +02:00
|
|
|
internalResolve(promise);
|
|
|
|
} else {
|
2017-11-17 08:15:30 +01:00
|
|
|
final boolean startWithoutSearchDomain = hasNDots();
|
|
|
|
final String initialHostname = startWithoutSearchDomain ? hostname : hostname + '.' + searchDomains[0];
|
|
|
|
final int initialSearchDomainIdx = startWithoutSearchDomain ? 0 : 1;
|
2017-06-21 22:48:45 +02:00
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
doSearchDomainQuery(initialHostname, new FutureListener<List<T>>() {
|
2017-11-17 08:15:30 +01:00
|
|
|
private int searchDomainIdx = initialSearchDomainIdx;
|
2016-06-30 23:12:11 +02:00
|
|
|
@Override
|
2018-03-29 22:01:25 +02:00
|
|
|
public void operationComplete(Future<List<T>> future) throws Exception {
|
2017-11-23 08:51:38 +01:00
|
|
|
Throwable cause = future.cause();
|
|
|
|
if (cause == null) {
|
2017-06-21 22:48:45 +02:00
|
|
|
promise.trySuccess(future.getNow());
|
2016-06-30 23:12:11 +02:00
|
|
|
} else {
|
2017-11-23 08:51:38 +01:00
|
|
|
if (DnsNameResolver.isTransportOrTimeoutError(cause)) {
|
|
|
|
promise.tryFailure(new SearchDomainUnknownHostException(cause, hostname));
|
|
|
|
} else if (searchDomainIdx < searchDomains.length) {
|
|
|
|
doSearchDomainQuery(hostname + '.' + searchDomains[searchDomainIdx++], this);
|
|
|
|
} else if (!startWithoutSearchDomain) {
|
|
|
|
internalResolve(promise);
|
|
|
|
} else {
|
|
|
|
promise.tryFailure(new SearchDomainUnknownHostException(cause, hostname));
|
|
|
|
}
|
2016-06-30 23:12:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2015-07-12 12:34:05 +02:00
|
|
|
}
|
|
|
|
|
2017-11-17 08:15:30 +01:00
|
|
|
private boolean hasNDots() {
|
|
|
|
for (int idx = hostname.length() - 1, dots = 0; idx >= 0; idx--) {
|
|
|
|
if (hostname.charAt(idx) == '.' && ++dots >= parent.ndots()) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-06-22 09:13:13 +02:00
|
|
|
private static final class SearchDomainUnknownHostException extends UnknownHostException {
|
2017-10-14 16:16:04 +02:00
|
|
|
private static final long serialVersionUID = -8573510133644997085L;
|
|
|
|
|
2017-06-22 09:13:13 +02:00
|
|
|
SearchDomainUnknownHostException(Throwable cause, String originalHostname) {
|
|
|
|
super("Search domain query failed. Original hostname: '" + originalHostname + "' " + cause.getMessage());
|
|
|
|
setStackTrace(cause.getStackTrace());
|
2017-11-23 08:51:38 +01:00
|
|
|
|
|
|
|
// Preserve the cause
|
|
|
|
initCause(cause.getCause());
|
2017-06-22 09:13:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Throwable fillInStackTrace() {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
private void doSearchDomainQuery(String hostname, FutureListener<List<T>> listener) {
|
|
|
|
DnsResolveContext<T> nextContext = newResolverContext(parent, hostname, dnsClass, expectedTypes,
|
|
|
|
additionals, nameServerAddrs);
|
|
|
|
Promise<List<T>> nextPromise = parent.executor().newPromise();
|
2017-06-21 22:48:45 +02:00
|
|
|
nextContext.internalResolve(nextPromise);
|
|
|
|
nextPromise.addListener(listener);
|
|
|
|
}
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
private void internalResolve(Promise<List<T>> promise) {
|
2017-01-25 09:28:22 +01:00
|
|
|
DnsServerAddressStream nameServerAddressStream = getNameServers(hostname);
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
final int end = expectedTypes.length - 1;
|
2017-07-03 23:46:36 +02:00
|
|
|
for (int i = 0; i < end; ++i) {
|
2018-03-29 22:01:25 +02:00
|
|
|
if (!query(hostname, expectedTypes[i], nameServerAddressStream.duplicate(), promise, null)) {
|
2017-01-20 15:46:46 +01:00
|
|
|
return;
|
|
|
|
}
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
2018-03-29 22:01:25 +02:00
|
|
|
query(hostname, expectedTypes[end], nameServerAddressStream, promise, null);
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
|
2017-01-25 09:28:22 +01:00
|
|
|
/**
|
|
|
|
* 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(),
|
|
|
|
additionals, 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;
|
|
|
|
|
2017-08-19 04:16:51 +02:00
|
|
|
List<? extends DnsCacheEntry> entries = parent.authoritativeDnsServerCache().get(hostname, additionals);
|
2017-01-25 09:28:22 +01:00
|
|
|
if (entries != null && !entries.isEmpty()) {
|
2017-04-07 03:09:28 +02:00
|
|
|
return DnsServerAddresses.sequential(new DnsCacheIterable(entries)).stream();
|
2017-01-25 09:28:22 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private final class DnsCacheIterable implements Iterable<InetSocketAddress> {
|
2017-08-19 04:16:51 +02:00
|
|
|
private final List<? extends DnsCacheEntry> entries;
|
2017-01-25 09:28:22 +01:00
|
|
|
|
2017-08-19 04:16:51 +02:00
|
|
|
DnsCacheIterable(List<? extends DnsCacheEntry> entries) {
|
2017-01-25 09:28:22 +01:00
|
|
|
this.entries = entries;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Iterator<InetSocketAddress> iterator() {
|
|
|
|
return new Iterator<InetSocketAddress>() {
|
2017-08-19 04:16:51 +02:00
|
|
|
Iterator<? extends DnsCacheEntry> entryIterator = entries.iterator();
|
2017-01-25 09:28:22 +01:00
|
|
|
|
|
|
|
@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();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-03 23:46:36 +02:00
|
|
|
private void query(final DnsServerAddressStream nameServerAddrStream, final int nameServerAddrStreamIndex,
|
|
|
|
final DnsQuestion question,
|
2018-03-29 22:01:25 +02:00
|
|
|
final Promise<List<T>> promise, Throwable cause) {
|
2017-07-03 23:46:36 +02:00
|
|
|
query(nameServerAddrStream, nameServerAddrStreamIndex, question,
|
2017-11-23 08:51:38 +01:00
|
|
|
parent.dnsQueryLifecycleObserverFactory().newDnsQueryLifecycleObserver(question), promise, cause);
|
2017-04-07 03:09:28 +02:00
|
|
|
}
|
|
|
|
|
2017-07-03 23:46:36 +02:00
|
|
|
private void query(final DnsServerAddressStream nameServerAddrStream,
|
|
|
|
final int nameServerAddrStreamIndex,
|
|
|
|
final DnsQuestion question,
|
2017-04-07 03:09:28 +02:00
|
|
|
final DnsQueryLifecycleObserver queryLifecycleObserver,
|
2018-03-29 22:01:25 +02:00
|
|
|
final Promise<List<T>> promise,
|
2017-11-23 08:51:38 +01:00
|
|
|
final Throwable cause) {
|
2017-07-03 23:46:36 +02:00
|
|
|
if (nameServerAddrStreamIndex >= nameServerAddrStream.size() || allowedQueries == 0 || promise.isCancelled()) {
|
|
|
|
tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, queryLifecycleObserver,
|
2017-11-23 08:51:38 +01:00
|
|
|
promise, cause);
|
2014-09-19 15:36:32 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-07-03 23:46:36 +02:00
|
|
|
--allowedQueries;
|
2017-04-07 03:09:28 +02:00
|
|
|
final InetSocketAddress nameServerAddr = nameServerAddrStream.next();
|
|
|
|
final ChannelPromise writePromise = parent.ch.newPromise();
|
2016-07-29 10:30:08 +02:00
|
|
|
final Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> f = parent.query0(
|
2017-04-07 03:09:28 +02:00
|
|
|
nameServerAddr, question, additionals, writePromise,
|
2016-07-29 10:30:08 +02:00
|
|
|
parent.ch.eventLoop().<AddressedEnvelope<? extends DnsResponse, InetSocketAddress>>newPromise());
|
2014-09-19 15:36:32 +02:00
|
|
|
queriesInProgress.add(f);
|
|
|
|
|
2017-04-07 03:09:28 +02:00
|
|
|
queryLifecycleObserver.queryWritten(nameServerAddr, writePromise);
|
|
|
|
|
2015-03-16 07:46:14 +01:00
|
|
|
f.addListener(new FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>>() {
|
2014-09-19 15:36:32 +02:00
|
|
|
@Override
|
2015-03-16 07:46:14 +01:00
|
|
|
public void operationComplete(Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> future) {
|
2014-09-19 15:36:32 +02:00
|
|
|
queriesInProgress.remove(future);
|
|
|
|
|
2015-10-30 04:53:05 +01:00
|
|
|
if (promise.isDone() || future.isCancelled()) {
|
2017-04-07 03:09:28 +02:00
|
|
|
queryLifecycleObserver.queryCancelled(allowedQueries);
|
2018-02-19 20:18:14 +01:00
|
|
|
|
|
|
|
// Check if we need to release the envelope itself. If the query was cancelled the getNow() will
|
|
|
|
// return null as well as the Future will be failed with a CancellationException.
|
|
|
|
AddressedEnvelope<DnsResponse, InetSocketAddress> result = future.getNow();
|
|
|
|
if (result != null) {
|
|
|
|
result.release();
|
|
|
|
}
|
2014-09-19 15:36:32 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-11-23 08:51:38 +01:00
|
|
|
final Throwable queryCause = future.cause();
|
2014-09-19 15:36:32 +02:00
|
|
|
try {
|
2017-11-23 08:51:38 +01:00
|
|
|
if (queryCause == null) {
|
2017-07-03 23:46:36 +02:00
|
|
|
onResponse(nameServerAddrStream, nameServerAddrStreamIndex, question, future.getNow(),
|
|
|
|
queryLifecycleObserver, promise);
|
2014-09-19 15:36:32 +02:00
|
|
|
} else {
|
2015-07-12 12:34:05 +02:00
|
|
|
// Server did not respond or I/O error occurred; try again.
|
2017-11-23 08:51:38 +01:00
|
|
|
queryLifecycleObserver.queryFailed(queryCause);
|
|
|
|
query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, promise, queryCause);
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
} finally {
|
2017-07-03 23:46:36 +02:00
|
|
|
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,
|
2017-11-23 08:51:38 +01:00
|
|
|
promise, queryCause);
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-07-03 23:46:36 +02:00
|
|
|
void onResponse(final DnsServerAddressStream nameServerAddrStream, final int nameServerAddrStreamIndex,
|
|
|
|
final DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
|
2017-04-07 03:09:28 +02:00
|
|
|
final DnsQueryLifecycleObserver queryLifecycleObserver,
|
2018-03-29 22:01:25 +02:00
|
|
|
Promise<List<T>> promise) {
|
2014-09-19 15:36:32 +02:00
|
|
|
try {
|
2015-07-12 12:34:05 +02:00
|
|
|
final DnsResponse res = envelope.content();
|
|
|
|
final DnsResponseCode code = res.code();
|
|
|
|
if (code == DnsResponseCode.NOERROR) {
|
2017-04-07 03:09:28 +02:00
|
|
|
if (handleRedirect(question, envelope, queryLifecycleObserver, promise)) {
|
2017-01-25 09:28:22 +01:00
|
|
|
// Was a redirect so return here as everything else is handled in handleRedirect(...)
|
|
|
|
return;
|
|
|
|
}
|
2015-07-12 12:34:05 +02:00
|
|
|
final DnsRecordType type = question.type();
|
2017-01-25 09:28:22 +01:00
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
if (type == DnsRecordType.CNAME) {
|
2017-04-07 03:09:28 +02:00
|
|
|
onResponseCNAME(question, envelope, queryLifecycleObserver, promise);
|
2018-03-29 22:01:25 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (DnsRecordType expectedType : expectedTypes) {
|
|
|
|
if (type == expectedType) {
|
|
|
|
onExpectedResponse(question, envelope, queryLifecycleObserver, promise);
|
|
|
|
return;
|
|
|
|
}
|
2015-07-12 12:34:05 +02:00
|
|
|
}
|
2018-03-29 22:01:25 +02:00
|
|
|
|
|
|
|
queryLifecycleObserver.queryFailed(UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION);
|
2015-07-12 12:34:05 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retry with the next server if the server did not tell us that the domain does not exist.
|
|
|
|
if (code != DnsResponseCode.NXDOMAIN) {
|
2017-07-03 23:46:36 +02:00
|
|
|
query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
|
2017-11-23 08:51:38 +01:00
|
|
|
queryLifecycleObserver.queryNoAnswer(code), promise, null);
|
2017-04-07 03:09:28 +02:00
|
|
|
} else {
|
|
|
|
queryLifecycleObserver.queryFailed(NXDOMAIN_QUERY_FAILED_EXCEPTION);
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
} finally {
|
2015-07-12 12:34:05 +02:00
|
|
|
ReferenceCountUtil.safeRelease(envelope);
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-25 09:28:22 +01:00
|
|
|
/**
|
2017-04-04 10:23:34 +02:00
|
|
|
* Handles a redirect answer if needed and returns {@code true} if a redirect query has been made.
|
2017-01-25 09:28:22 +01:00
|
|
|
*/
|
|
|
|
private boolean handleRedirect(
|
2017-04-07 03:09:28 +02:00
|
|
|
DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
|
2018-03-29 22:01:25 +02:00
|
|
|
final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<T>> promise) {
|
2017-01-25 09:28:22 +01:00
|
|
|
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.
|
|
|
|
if (res.count(DnsSection.ANSWER) == 0) {
|
|
|
|
AuthoritativeNameServerList serverNames = extractAuthoritativeNameServers(question.name(), res);
|
|
|
|
|
|
|
|
if (serverNames != null) {
|
|
|
|
List<InetSocketAddress> nameServers = new ArrayList<InetSocketAddress>(serverNames.size());
|
|
|
|
int additionalCount = res.count(DnsSection.ADDITIONAL);
|
|
|
|
|
|
|
|
for (int i = 0; i < additionalCount; i++) {
|
|
|
|
final DnsRecord r = res.recordAt(DnsSection.ADDITIONAL, i);
|
|
|
|
|
2017-04-04 10:23:34 +02:00
|
|
|
if (r.type() == DnsRecordType.A && !parent.supportsARecords() ||
|
|
|
|
r.type() == DnsRecordType.AAAA && !parent.supportsAAAARecords()) {
|
2017-01-25 09:28:22 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
final String recordName = r.name();
|
2018-03-29 22:01:25 +02:00
|
|
|
final AuthoritativeNameServer authoritativeNameServer = serverNames.remove(recordName);
|
2017-01-25 09:28:22 +01:00
|
|
|
|
|
|
|
if (authoritativeNameServer == null) {
|
|
|
|
// Not a server we are interested in.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
InetAddress resolved = decodeAddress(r, recordName, parent.isDecodeIdn());
|
2017-01-25 09:28:22 +01:00
|
|
|
if (resolved == null) {
|
|
|
|
// Could not parse it, move to the next.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
nameServers.add(new InetSocketAddress(resolved, parent.dnsRedirectPort(resolved)));
|
|
|
|
addNameServerToCache(authoritativeNameServer, resolved, r.timeToLive());
|
|
|
|
}
|
|
|
|
|
2017-06-22 10:20:23 +02:00
|
|
|
if (!nameServers.isEmpty()) {
|
2017-07-03 23:46:36 +02:00
|
|
|
query(parent.uncachedRedirectDnsServerStream(nameServers), 0, question,
|
2017-11-23 08:51:38 +01:00
|
|
|
queryLifecycleObserver.queryRedirected(unmodifiableList(nameServers)), promise, null);
|
2017-04-04 10:23:34 +02:00
|
|
|
return true;
|
2017-01-25 09:28:22 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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) {
|
|
|
|
int authorityCount = res.count(DnsSection.AUTHORITY);
|
|
|
|
if (authorityCount == 0) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
AuthoritativeNameServerList serverNames = new AuthoritativeNameServerList(questionName);
|
|
|
|
for (int i = 0; i < authorityCount; i++) {
|
|
|
|
serverNames.add(res.recordAt(DnsSection.AUTHORITY, i));
|
|
|
|
}
|
|
|
|
return serverNames;
|
|
|
|
}
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
private void onExpectedResponse(
|
|
|
|
DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
|
|
|
|
final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<T>> promise) {
|
2015-03-16 07:46:14 +01:00
|
|
|
|
2014-09-19 15:36:32 +02:00
|
|
|
// We often get a bunch of CNAMES as well when we asked for A/AAAA.
|
2015-03-16 07:46:14 +01:00
|
|
|
final DnsResponse response = envelope.content();
|
2014-09-19 15:36:32 +02:00
|
|
|
final Map<String, String> cnames = buildAliasMap(response);
|
2015-03-16 07:46:14 +01:00
|
|
|
final int answerCount = response.count(DnsSection.ANSWER);
|
2014-09-19 15:36:32 +02:00
|
|
|
|
|
|
|
boolean found = false;
|
2015-03-16 07:46:14 +01:00
|
|
|
for (int i = 0; i < answerCount; i ++) {
|
|
|
|
final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
|
|
|
|
final DnsRecordType type = r.type();
|
2018-03-29 22:01:25 +02:00
|
|
|
boolean matches = false;
|
|
|
|
for (DnsRecordType expectedType : expectedTypes) {
|
|
|
|
if (type == expectedType) {
|
|
|
|
matches = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!matches) {
|
2014-09-19 15:36:32 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-01-25 09:28:22 +01:00
|
|
|
final String questionName = question.name().toLowerCase(Locale.US);
|
|
|
|
final String recordName = r.name().toLowerCase(Locale.US);
|
2014-09-19 15:36:32 +02:00
|
|
|
|
|
|
|
// Make sure the record is for the questioned domain.
|
2017-01-25 09:28:22 +01:00
|
|
|
if (!recordName.equals(questionName)) {
|
2014-09-19 15:36:32 +02:00
|
|
|
// Even if the record's name is not exactly same, it might be an alias defined in the CNAME records.
|
2017-01-25 09:28:22 +01:00
|
|
|
String resolved = questionName;
|
2014-09-19 15:36:32 +02:00
|
|
|
do {
|
|
|
|
resolved = cnames.get(resolved);
|
2017-01-25 09:28:22 +01:00
|
|
|
if (recordName.equals(resolved)) {
|
2014-09-19 15:36:32 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
} while (resolved != null);
|
|
|
|
|
|
|
|
if (resolved == null) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
final T converted = convertRecord(r, hostname, additionals, parent.ch.eventLoop());
|
|
|
|
if (converted == null) {
|
2015-03-16 07:46:14 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
if (finalResult == null) {
|
|
|
|
finalResult = new ArrayList<T>(8);
|
2015-07-12 12:34:05 +02:00
|
|
|
}
|
2018-03-29 22:01:25 +02:00
|
|
|
finalResult.add(converted);
|
2015-07-12 12:34:05 +02:00
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
cache(hostname, additionals, r, converted);
|
2015-07-12 12:34:05 +02:00
|
|
|
found = true;
|
|
|
|
|
|
|
|
// Note that we do not break from the loop here, so we decode/cache all A/AAAA records.
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (found) {
|
2017-04-07 03:09:28 +02:00
|
|
|
queryLifecycleObserver.querySucceed();
|
2014-09-19 15:36:32 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-04-07 03:09:28 +02:00
|
|
|
if (cnames.isEmpty()) {
|
|
|
|
queryLifecycleObserver.queryFailed(NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION);
|
|
|
|
} else {
|
2018-03-29 22:01:25 +02:00
|
|
|
// We got only CNAME, not one of expectedTypes.
|
|
|
|
onResponseCNAME(question, cnames, queryLifecycleObserver, promise);
|
2017-01-25 09:28:22 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-30 23:12:11 +02:00
|
|
|
private void onResponseCNAME(DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
|
2018-03-29 22:01:25 +02:00
|
|
|
final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<T>> promise) {
|
|
|
|
onResponseCNAME(question, buildAliasMap(envelope.content()), queryLifecycleObserver, promise);
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private void onResponseCNAME(
|
2018-03-29 22:01:25 +02:00
|
|
|
DnsQuestion question, Map<String, String> cnames,
|
|
|
|
final DnsQueryLifecycleObserver queryLifecycleObserver,
|
|
|
|
Promise<List<T>> promise) {
|
2014-09-19 15:36:32 +02:00
|
|
|
|
|
|
|
// Resolve the host name in the question into the real host name.
|
2018-03-29 22:01:25 +02:00
|
|
|
String resolved = question.name().toLowerCase(Locale.US);
|
2014-09-19 15:36:32 +02:00
|
|
|
boolean found = false;
|
2016-03-07 03:12:33 +01:00
|
|
|
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);
|
2014-09-19 15:36:32 +02:00
|
|
|
if (next != null) {
|
|
|
|
found = true;
|
|
|
|
resolved = next;
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (found) {
|
2018-02-02 02:17:01 +01:00
|
|
|
followCname(question, resolved, queryLifecycleObserver, promise);
|
2017-04-07 03:09:28 +02:00
|
|
|
} else {
|
|
|
|
queryLifecycleObserver.queryFailed(CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION);
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static Map<String, String> buildAliasMap(DnsResponse response) {
|
2015-03-16 07:46:14 +01:00
|
|
|
final int answerCount = response.count(DnsSection.ANSWER);
|
2014-09-19 15:36:32 +02:00
|
|
|
Map<String, String> cnames = null;
|
2015-03-16 07:46:14 +01:00
|
|
|
for (int i = 0; i < answerCount; i ++) {
|
|
|
|
final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
|
|
|
|
final DnsRecordType type = r.type();
|
|
|
|
if (type != DnsRecordType.CNAME) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!(r instanceof DnsRawRecord)) {
|
2014-09-19 15:36:32 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2015-03-16 07:46:14 +01:00
|
|
|
final ByteBuf recordContent = ((ByteBufHolder) r).content();
|
|
|
|
final String domainName = decodeDomainName(recordContent);
|
|
|
|
if (domainName == null) {
|
2014-09-19 15:36:32 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cnames == null) {
|
2017-04-07 03:09:28 +02:00
|
|
|
cnames = new HashMap<String, String>(min(8, answerCount));
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
|
2015-03-16 07:46:14 +01:00
|
|
|
cnames.put(r.name().toLowerCase(Locale.US), domainName.toLowerCase(Locale.US));
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return cnames != null? cnames : Collections.<String, String>emptyMap();
|
|
|
|
}
|
|
|
|
|
2017-07-03 23:46:36 +02:00
|
|
|
void tryToFinishResolve(final DnsServerAddressStream nameServerAddrStream,
|
|
|
|
final int nameServerAddrStreamIndex,
|
|
|
|
final DnsQuestion question,
|
|
|
|
final DnsQueryLifecycleObserver queryLifecycleObserver,
|
2018-03-29 22:01:25 +02:00
|
|
|
final Promise<List<T>> promise,
|
2017-11-23 08:51:38 +01:00
|
|
|
final Throwable cause) {
|
2017-07-03 23:46:36 +02:00
|
|
|
// There are no queries left to try.
|
2014-09-19 15:36:32 +02:00
|
|
|
if (!queriesInProgress.isEmpty()) {
|
2017-07-03 23:46:36 +02:00
|
|
|
queryLifecycleObserver.queryCancelled(allowedQueries);
|
|
|
|
|
2014-09-19 15:36:32 +02:00
|
|
|
// There are still some queries we did not receive responses for.
|
2018-03-29 22:01:25 +02:00
|
|
|
if (finalResult != null && containsExpectedResult(finalResult)) {
|
|
|
|
// But it's OK to finish the resolution process if we got something expected.
|
2017-11-23 08:51:38 +01:00
|
|
|
finishResolve(promise, cause);
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
// We did not get an expected result yet, so we can't finish the resolution process.
|
2014-09-19 15:36:32 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// There are no queries left to try.
|
2018-03-29 22:01:25 +02:00
|
|
|
if (finalResult == null) {
|
2017-07-03 23:46:36 +02:00
|
|
|
if (nameServerAddrStreamIndex < nameServerAddrStream.size()) {
|
|
|
|
if (queryLifecycleObserver == NoopDnsQueryLifecycleObserver.INSTANCE) {
|
|
|
|
// If the queryLifecycleObserver has already been terminated we should create a new one for this
|
|
|
|
// fresh query.
|
2017-11-23 08:51:38 +01:00
|
|
|
query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, promise, cause);
|
2017-07-03 23:46:36 +02:00
|
|
|
} else {
|
|
|
|
query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, queryLifecycleObserver,
|
2017-11-23 08:51:38 +01:00
|
|
|
promise, cause);
|
2017-07-03 23:46:36 +02:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
queryLifecycleObserver.queryFailed(NAME_SERVERS_EXHAUSTED_EXCEPTION);
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
// .. and we could not find any expected records.
|
2017-11-23 08:51:38 +01:00
|
|
|
|
|
|
|
// If cause != null we know this was caused by a timeout / cancel / transport exception. In this case we
|
2018-03-29 22:01:25 +02:00
|
|
|
// won't try to resolve the CNAME as we only should do this if we could not get the expected records
|
|
|
|
// because they do not exist and the DNS server did probably signal it.
|
2017-11-23 08:51:38 +01:00
|
|
|
if (cause == null && !triedCNAME) {
|
2014-09-19 15:36:32 +02:00
|
|
|
// As the last resort, try to query CNAME, just in case the name server has it.
|
|
|
|
triedCNAME = true;
|
2017-01-25 09:28:22 +01:00
|
|
|
|
2017-11-23 08:51:38 +01:00
|
|
|
query(hostname, DnsRecordType.CNAME, getNameServers(hostname), promise, null);
|
2014-09-19 15:36:32 +02:00
|
|
|
return;
|
|
|
|
}
|
2017-07-03 23:46:36 +02:00
|
|
|
} else {
|
|
|
|
queryLifecycleObserver.queryCancelled(allowedQueries);
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
// We have at least one resolved record or tried CNAME as the last resort..
|
2017-11-23 08:51:38 +01:00
|
|
|
finishResolve(promise, cause);
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
private void finishResolve(Promise<List<T>> promise, Throwable cause) {
|
2014-09-19 15:36:32 +02:00
|
|
|
if (!queriesInProgress.isEmpty()) {
|
|
|
|
// If there are queries in progress, we should cancel it because we already finished the resolution.
|
2015-03-16 07:46:14 +01:00
|
|
|
for (Iterator<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> i = queriesInProgress.iterator();
|
|
|
|
i.hasNext();) {
|
|
|
|
Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> f = i.next();
|
2014-09-19 15:36:32 +02:00
|
|
|
i.remove();
|
|
|
|
|
|
|
|
if (!f.cancel(false)) {
|
|
|
|
f.addListener(RELEASE_RESPONSE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
if (finalResult != null) {
|
|
|
|
// Found at least one resolved record.
|
|
|
|
trySuccess(promise, finalResult);
|
|
|
|
return;
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// No resolved address found.
|
2015-07-12 12:34:05 +02:00
|
|
|
final int tries = maxAllowedQueries - allowedQueries;
|
|
|
|
final StringBuilder buf = new StringBuilder(64);
|
2014-09-19 15:36:32 +02:00
|
|
|
|
2017-06-22 09:13:13 +02:00
|
|
|
buf.append("failed to resolve '").append(hostname).append('\'');
|
2015-07-12 12:34:05 +02:00
|
|
|
if (tries > 1) {
|
2016-05-27 21:34:36 +02:00
|
|
|
if (tries < maxAllowedQueries) {
|
|
|
|
buf.append(" after ")
|
|
|
|
.append(tries)
|
|
|
|
.append(" queries ");
|
2015-07-12 12:34:05 +02:00
|
|
|
} else {
|
2016-05-27 21:34:36 +02:00
|
|
|
buf.append(". Exceeded max queries per resolve ")
|
|
|
|
.append(maxAllowedQueries)
|
|
|
|
.append(' ');
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
}
|
2017-11-23 08:51:38 +01:00
|
|
|
final UnknownHostException unknownHostException = new UnknownHostException(buf.toString());
|
|
|
|
if (cause == null) {
|
|
|
|
// Only cache if the failure was not because of an IO error / timeout that was caused by the query
|
|
|
|
// itself.
|
2018-03-29 22:01:25 +02:00
|
|
|
cache(hostname, additionals, unknownHostException);
|
2017-11-23 08:51:38 +01:00
|
|
|
} else {
|
|
|
|
unknownHostException.initCause(cause);
|
|
|
|
}
|
|
|
|
promise.tryFailure(unknownHostException);
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
|
2016-06-13 21:16:22 +02:00
|
|
|
static String decodeDomainName(ByteBuf in) {
|
|
|
|
in.markReaderIndex();
|
2014-09-19 15:36:32 +02:00
|
|
|
try {
|
2016-06-13 21:16:22 +02:00
|
|
|
return DefaultDnsRecordDecoder.decodeName(in);
|
|
|
|
} catch (CorruptedFrameException e) {
|
|
|
|
// In this case we just return null.
|
|
|
|
return null;
|
2014-09-19 15:36:32 +02:00
|
|
|
} finally {
|
2016-06-13 21:16:22 +02:00
|
|
|
in.resetReaderIndex();
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-19 22:37:03 +02:00
|
|
|
private DnsServerAddressStream getNameServers(String hostname) {
|
|
|
|
DnsServerAddressStream stream = getNameServersFromCache(hostname);
|
2018-02-02 02:24:48 +01:00
|
|
|
return stream == null ? nameServerAddrs.duplicate() : stream;
|
2017-01-25 09:28:22 +01:00
|
|
|
}
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
private void followCname(DnsQuestion question, String cname, DnsQueryLifecycleObserver queryLifecycleObserver,
|
|
|
|
Promise<List<T>> promise) {
|
2018-02-01 07:11:35 +01:00
|
|
|
DnsServerAddressStream stream = getNameServers(cname);
|
2017-01-25 09:28:22 +01:00
|
|
|
|
2018-02-02 02:17:01 +01:00
|
|
|
final DnsQuestion cnameQuestion;
|
|
|
|
try {
|
|
|
|
cnameQuestion = newQuestion(cname, question.type());
|
|
|
|
} catch (Throwable cause) {
|
|
|
|
queryLifecycleObserver.queryFailed(cause);
|
|
|
|
PlatformDependent.throwException(cause);
|
|
|
|
return;
|
2017-01-20 15:46:46 +01:00
|
|
|
}
|
2018-02-02 02:17:01 +01:00
|
|
|
query(stream, 0, cnameQuestion, queryLifecycleObserver.queryCNAMEd(cnameQuestion), promise, null);
|
2017-01-20 15:46:46 +01:00
|
|
|
}
|
|
|
|
|
2017-04-07 03:09:28 +02:00
|
|
|
private boolean query(String hostname, DnsRecordType type, DnsServerAddressStream dnsServerAddressStream,
|
2018-03-29 22:01:25 +02:00
|
|
|
Promise<List<T>> promise, Throwable cause) {
|
2017-04-07 03:09:28 +02:00
|
|
|
final DnsQuestion question = newQuestion(hostname, type);
|
|
|
|
if (question == null) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-11-23 08:51:38 +01:00
|
|
|
query(dnsServerAddressStream, 0, question, promise, cause);
|
2017-04-07 03:09:28 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-03-29 22:01:25 +02:00
|
|
|
private DnsQuestion newQuestion(String hostname, DnsRecordType type) {
|
2017-01-20 15:46:46 +01:00
|
|
|
try {
|
2018-03-29 22:01:25 +02:00
|
|
|
return new DefaultDnsQuestion(hostname, type, dnsClass);
|
2017-01-20 15:46:46 +01:00
|
|
|
} catch (IllegalArgumentException e) {
|
|
|
|
// java.net.IDN.toASCII(...) may throw an IllegalArgumentException if it fails to parse the hostname
|
2017-04-07 03:09:28 +02:00
|
|
|
return null;
|
2017-01-19 13:18:19 +01:00
|
|
|
}
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|
|
|
|
|
2017-01-25 09:28:22 +01:00
|
|
|
/**
|
|
|
|
* 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 r) {
|
|
|
|
if (r.type() != DnsRecordType.NS || !(r instanceof DnsRawRecord)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only include servers that serve the correct domain.
|
|
|
|
if (questionName.length() < r.name().length()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
String recordName = r.name().toLowerCase(Locale.US);
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
final ByteBuf recordContent = ((ByteBufHolder) r).content();
|
|
|
|
final String domainName = decodeDomainName(recordContent);
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2014-09-19 15:36:32 +02:00
|
|
|
}
|