com.couchbase.client.core.deps.org.xbill.DNS.Lookup Maven / Gradle / Ivy
Show all versions of core-io Show documentation
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2002-2004 Brian Wellington ([email protected])
package com.couchbase.client.core.deps.org.xbill.DNS;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import com.couchbase.client.core.deps.org.xbill.DNS.hosts.HostsFileParser;
/**
* The Lookup object issues queries to the local hosts database ({@code /etc/hosts}) and to
* recursive DNS servers. The input consists of a name, an optional type, and an optional class.
* Caching is enabled by default and used when possible to reduce the number of DNS requests. A
* {@link Resolver}, which defaults to an {@link ExtendedResolver} initialized with the resolvers
* located by the {@link ResolverConfig} class, performs the queries. A search path of domain
* suffixes is used to resolve relative names, and is also determined by the {@link ResolverConfig}
* class.
*
* A Lookup object may be reused, but should not be used by multiple threads.
*
*
Lookup is considered legacy (but not yet deprecated). Use {@link
* com.couchbase.client.core.deps.org.xbill.DNS.lookup.LookupSession} instead, which is thread safe and fully async.
*
* @see org.xbill.DNS.lookup.LookupSession
* @see Cache
* @see Resolver
* @see ResolverConfig
* @see HostsFileParser
* @author Brian Wellington
*/
@Slf4j
public final class Lookup {
private static Resolver defaultResolver;
private static List defaultSearchPath;
private static Map defaultCaches;
private static int defaultNdots;
private static HostsFileParser defaultHostsFileParser;
private Resolver resolver;
private List searchPath;
private int ndots;
private Cache cache;
private boolean temporary_cache;
private int credibility;
private final Name name;
private final int type;
private final int dclass;
private int iterations;
private boolean foundAlias;
private boolean done;
private boolean doneCurrent;
private List aliases;
private Record[] answers;
private int result;
private String error;
private boolean nxdomain;
private boolean badresponse;
private String badresponse_error;
private boolean networkerror;
private boolean timedout;
private boolean nametoolong;
private boolean referral;
private boolean cycleResults = true;
private final int maxIterations;
/**
* Gets or sets the local hosts database parser to use for lookup before using a {@link Resolver}.
*
* @since 3.4
*/
@Getter @Setter private HostsFileParser hostsFileParser;
private static final Name[] noAliases = new Name[0];
/** The lookup was successful. */
public static final int SUCCESSFUL = 0;
/** The lookup failed due to a data or server error. Repeating the lookup would not be helpful. */
public static final int UNRECOVERABLE = 1;
/** The lookup failed due to a network error. Repeating the lookup may be helpful. */
public static final int TRY_AGAIN = 2;
/** The host does not exist. */
public static final int HOST_NOT_FOUND = 3;
/** The host exists, but has no records associated with the queried type. */
public static final int TYPE_NOT_FOUND = 4;
public static synchronized void refreshDefault() {
defaultResolver = new ExtendedResolver();
defaultSearchPath = ResolverConfig.getCurrentConfig().searchPath();
defaultCaches = new HashMap<>();
defaultNdots = ResolverConfig.getCurrentConfig().ndots();
defaultHostsFileParser = new HostsFileParser();
}
static {
refreshDefault();
}
/**
* Gets the Resolver that will be used as the default by future Lookups.
*
* @return The default resolver.
*/
public static synchronized Resolver getDefaultResolver() {
return defaultResolver;
}
/**
* Sets the default Resolver to be used as the default by future Lookups.
*
* @param resolver The default resolver.
*/
public static synchronized void setDefaultResolver(Resolver resolver) {
defaultResolver = resolver;
}
/**
* Gets the Cache that will be used as the default for the specified class by future Lookups.
*
* @param dclass The class whose cache is being retrieved.
* @return The default cache for the specified class.
*/
public static synchronized Cache getDefaultCache(int dclass) {
DClass.check(dclass);
Cache c = defaultCaches.get(dclass);
if (c == null) {
c = new Cache(dclass);
defaultCaches.put(dclass, c);
}
return c;
}
/**
* Sets the Cache to be used as the default for the specified class by future Lookups.
*
* @param cache The default cache for the specified class.
* @param dclass The class whose cache is being set.
*/
public static synchronized void setDefaultCache(Cache cache, int dclass) {
DClass.check(dclass);
defaultCaches.put(dclass, cache);
}
/**
* Gets the search path that will be used as the default by future Lookups.
*
* @return The default search path.
*/
public static synchronized List getDefaultSearchPath() {
return defaultSearchPath;
}
/**
* Sets the search path to be used as the default by future Lookups.
*
* @param domains The default search path.
* @throws IllegalArgumentException if a domain in the search path is not absolute and cannot be
* made absolute.
*/
public static synchronized void setDefaultSearchPath(List domains) {
defaultSearchPath = convertSearchPathDomainList(domains);
}
/**
* Sets the search path to be used as the default by future Lookups.
*
* @param domains The default search path.
* @throws IllegalArgumentException if a domain in the search path is not absolute and cannot be
* made absolute.
*/
public static synchronized void setDefaultSearchPath(Name... domains) {
setDefaultSearchPath(Arrays.asList(domains));
}
/**
* Sets the search path that will be used as the default by future Lookups.
*
* @param domains The default search path.
* @throws TextParseException A name in the array is not a valid DNS name.
*/
public static synchronized void setDefaultSearchPath(String... domains)
throws TextParseException {
if (domains == null) {
defaultSearchPath = null;
return;
}
List newdomains = new ArrayList<>(domains.length);
for (String domain : domains) {
newdomains.add(Name.fromString(domain, Name.root));
}
defaultSearchPath = newdomains;
}
/**
* Gets the default {@link HostsFileParser} to use for new Lookup instances.
*
* @since 3.4
*/
public static synchronized HostsFileParser getDefaultHostsFileParser() {
return defaultHostsFileParser;
}
/**
* Sets the default {@link HostsFileParser} to use for new Lookup instances.
*
* @since 3.4
*/
public static synchronized void setDefaultHostsFileParser(HostsFileParser hostsFileParser) {
defaultHostsFileParser = hostsFileParser;
}
private static List convertSearchPathDomainList(List domains) {
try {
return domains.stream()
.map(
n -> {
try {
return Name.concatenate(n, Name.root);
} catch (NameTooLongException e) {
throw new RuntimeException(e);
}
})
.collect(Collectors.toList());
} catch (RuntimeException e) {
if (e.getCause() instanceof NameTooLongException) {
throw new IllegalArgumentException(e.getCause());
} else {
throw e;
}
}
}
/**
* Sets a custom logger that will be used to log the sent and received packets.
*
* @param logger The logger
*/
public static synchronized void setPacketLogger(PacketLogger logger) {
NioClient.setPacketLogger(logger);
}
private void reset() {
iterations = 0;
foundAlias = false;
done = false;
doneCurrent = false;
aliases = null;
answers = null;
result = -1;
error = null;
nxdomain = false;
badresponse = false;
badresponse_error = null;
networkerror = false;
timedout = false;
nametoolong = false;
referral = false;
if (temporary_cache) {
cache.clearCache();
}
}
/**
* Create a Lookup object that will find records of the given name, type, and class. The lookup
* will use the default cache, resolver, and search path, and look for records that are reasonably
* credible.
*
* @param name The name of the desired records
* @param type The type of the desired records
* @param dclass The class of the desired records
* @throws IllegalArgumentException The type is a meta type other than ANY.
* @see Cache
* @see Resolver
* @see Credibility
* @see Name
* @see Type
* @see DClass
*/
public Lookup(Name name, int type, int dclass) {
Type.check(type);
DClass.check(dclass);
if (!Type.isRR(type) && type != Type.ANY) {
throw new IllegalArgumentException("Cannot query for meta-types other than ANY");
}
this.name = name;
this.type = type;
this.dclass = dclass;
synchronized (Lookup.class) {
this.resolver = getDefaultResolver();
this.searchPath = getDefaultSearchPath();
this.cache = getDefaultCache(dclass);
}
this.ndots = defaultNdots;
this.credibility = Credibility.NORMAL;
this.result = -1;
this.maxIterations =
Integer.parseInt(System.getProperty("dnsjava.lookup.max_iterations", "16"));
if (Boolean.parseBoolean(System.getProperty("dnsjava.lookup.use_hosts_file", "true"))) {
this.hostsFileParser = getDefaultHostsFileParser();
}
}
/**
* Create a Lookup object that will find records of the given name and type in the IN class.
*
* @param name The name of the desired records
* @param type The type of the desired records
* @throws IllegalArgumentException The type is a meta type other than ANY.
* @see #Lookup(Name,int,int)
*/
public Lookup(Name name, int type) {
this(name, type, DClass.IN);
}
/**
* Create a Lookup object that will find records of type A at the given name in the IN class.
*
* @param name The name of the desired records
* @see #Lookup(Name,int,int)
*/
public Lookup(Name name) {
this(name, Type.A, DClass.IN);
}
/**
* Create a Lookup object that will find records of the given name, type, and class.
*
* @param name The name of the desired records
* @param type The type of the desired records
* @param dclass The class of the desired records
* @throws TextParseException The name is not a valid DNS name
* @throws IllegalArgumentException The type is a meta type other than ANY.
* @see #Lookup(Name,int,int)
*/
public Lookup(String name, int type, int dclass) throws TextParseException {
this(Name.fromString(name), type, dclass);
}
/**
* Create a Lookup object that will find records of the given name and type in the IN class.
*
* @param name The name of the desired records
* @param type The type of the desired records
* @throws TextParseException The name is not a valid DNS name
* @throws IllegalArgumentException The type is a meta type other than ANY.
* @see #Lookup(Name,int,int)
*/
public Lookup(String name, int type) throws TextParseException {
this(Name.fromString(name), type, DClass.IN);
}
/**
* Create a Lookup object that will find records of type A at the given name in the IN class.
*
* @param name The name of the desired records
* @throws TextParseException The name is not a valid DNS name
* @see #Lookup(Name,int,int)
*/
public Lookup(String name) throws TextParseException {
this(Name.fromString(name), Type.A, DClass.IN);
}
/**
* Sets the resolver to use when performing this lookup. This overrides the default value.
*
* @param resolver The resolver to use.
*/
public void setResolver(Resolver resolver) {
this.resolver = resolver;
}
/**
* Sets the search path to use when performing this lookup. This overrides the default value.
*
* @param domains An array of names containing the search path.
* @throws IllegalArgumentException if a domain in the search path is not absolute and cannot be
* made absolute.
*/
public void setSearchPath(List domains) {
this.searchPath = convertSearchPathDomainList(domains);
}
/**
* Sets the search path to use when performing this lookup. This overrides the default value.
*
* @param domains An array of names containing the search path.
* @throws IllegalArgumentException if a domain in the search path is not absolute and cannot be
* made absolute.
*/
public void setSearchPath(Name... domains) {
setSearchPath(Arrays.asList(domains));
}
/**
* Sets the search path to use when performing this lookup. This overrides the default value.
*
* @param domains An array of names containing the search path.
* @throws TextParseException A name in the array is not a valid DNS name.
*/
public void setSearchPath(String... domains) throws TextParseException {
if (domains == null) {
this.searchPath = null;
return;
}
List newdomains = new ArrayList<>(domains.length);
for (String domain : domains) {
newdomains.add(Name.fromString(domain, Name.root));
}
this.searchPath = newdomains;
}
/**
* Sets the cache to use when performing this lookup. This overrides the default value. If the
* results of this lookup should not be permanently cached, null can be provided here.
*
* @param cache The cache to use.
*/
public void setCache(Cache cache) {
if (cache == null) {
this.cache = new Cache(dclass);
this.temporary_cache = true;
} else {
this.cache = cache;
this.temporary_cache = false;
}
}
/**
* Sets the default ndots to use when performing a lookup, overriding the default value.
* Specifically, this refers to the number of "dots" which, if present in a name, indicate that a
* lookup for the absolute name should be attempted before appending any search path elements.
*
* @param ndots The ndots value to use, which must be greater than or equal to 0.
*/
public static void setDefaultNdots(int ndots) {
if (ndots < 0) {
throw new IllegalArgumentException("Illegal ndots value: " + ndots);
}
defaultNdots = ndots;
}
/**
* Sets ndots to use when performing this lookup, overriding the default value. Specifically, this
* refers to the number of "dots" which, if present in a name, indicate that a lookup for the
* absolute name should be attempted before appending any search path elements.
*
* @param ndots The ndots value to use, which must be greater than or equal to 0.
*/
public void setNdots(int ndots) {
if (ndots < 0) {
throw new IllegalArgumentException("Illegal ndots value: " + ndots);
}
this.ndots = ndots;
}
/**
* Sets the minimum credibility level that will be accepted when performing the lookup. This
* defaults to Credibility.NORMAL.
*
* @param credibility The minimum credibility level.
*/
public void setCredibility(int credibility) {
this.credibility = credibility;
}
/**
* Controls the behavior if results being returned from the cache should be cycled in a
* round-robin style (true) or if the raw lookup results should be returned (false).
*
* @param cycleResults The desired behavior of the order of the results
*/
public void setCycleResults(boolean cycleResults) {
this.cycleResults = cycleResults;
}
private void follow(Name name, Name oldname) {
foundAlias = true;
badresponse = false;
networkerror = false;
timedout = false;
nxdomain = false;
referral = false;
iterations++;
if (iterations >= maxIterations || name.equals(oldname)) {
result = UNRECOVERABLE;
error = "CNAME loop";
done = true;
return;
}
if (aliases == null) {
aliases = new ArrayList<>();
}
aliases.add(oldname);
lookup(name);
}
private void processResponse(Name name, SetResponse response) {
if (response.isSuccessful()) {
List rrsets = response.answers();
List l = new ArrayList<>();
for (RRset set : rrsets) {
l.addAll(set.rrs(cycleResults));
}
result = SUCCESSFUL;
answers = l.toArray(new Record[0]);
done = true;
} else if (response.isNXDOMAIN()) {
nxdomain = true;
doneCurrent = true;
if (iterations > 0) {
result = HOST_NOT_FOUND;
done = true;
}
} else if (response.isNXRRSET()) {
result = TYPE_NOT_FOUND;
answers = null;
done = true;
} else if (response.isCNAME()) {
CNAMERecord cname = response.getCNAME();
follow(cname.getTarget(), name);
} else if (response.isDNAME()) {
DNAMERecord dname = response.getDNAME();
try {
follow(name.fromDNAME(dname), name);
} catch (NameTooLongException e) {
result = UNRECOVERABLE;
error = "Invalid DNAME target";
done = true;
}
} else if (response.isDelegation()) {
// We shouldn't get a referral. Ignore it.
referral = true;
}
}
private void lookup(Name current) {
if (lookupFromHostsFile(current)) {
return;
}
SetResponse sr = cache.lookupRecords(current, type, credibility);
log.debug("Lookup for {}/{}, cache answer: {}", current, Type.string(type), sr);
processResponse(current, sr);
if (done || doneCurrent) {
return;
}
Record question = Record.newRecord(current, type, dclass);
Message query = Message.newQuery(question);
Message response;
try {
response = resolver.send(query);
} catch (IOException e) {
log.debug(
"Lookup for {}/{}, id={} failed using resolver {}",
current,
Type.string(query.getQuestion().getType()),
query.getHeader().getID(),
resolver,
e);
// A network error occurred. Press on.
if (e instanceof InterruptedIOException) {
timedout = true;
} else {
networkerror = true;
}
return;
}
int rcode = response.getHeader().getRcode();
if (rcode != Rcode.NOERROR && rcode != Rcode.NXDOMAIN) {
// The server we contacted is broken or otherwise unhelpful.
// Press on.
badresponse = true;
badresponse_error = Rcode.string(rcode);
return;
}
if (!query.getQuestion().equals(response.getQuestion())) {
// The answer doesn't match the question. That's not good.
badresponse = true;
badresponse_error = "response does not match query";
return;
}
sr = cache.addMessage(response);
if (sr == null) {
sr = cache.lookupRecords(current, type, credibility);
}
log.debug(
"Queried {}/{}, id={}: {}", current, Type.string(type), response.getHeader().getID(), sr);
processResponse(current, sr);
}
private boolean lookupFromHostsFile(Name current) {
if (hostsFileParser != null && (type == Type.A || type == Type.AAAA)) {
try {
Optional localLookup = hostsFileParser.getAddressForHost(current, type);
if (localLookup.isPresent()) {
result = SUCCESSFUL;
done = true;
if (type == Type.A) {
answers = new ARecord[] {new ARecord(current, dclass, 0L, localLookup.get())};
} else {
answers = new AAAARecord[] {new AAAARecord(current, dclass, 0L, localLookup.get())};
}
return true;
}
} catch (IOException e) {
log.debug("Local hosts database parsing failed, ignoring and using resolver", e);
}
}
return false;
}
private void resolve(Name current, Name suffix) {
doneCurrent = false;
Name tname;
if (suffix == null) {
tname = current;
} else {
try {
tname = Name.concatenate(current, suffix);
} catch (NameTooLongException e) {
nametoolong = true;
return;
}
}
lookup(tname);
}
/**
* Performs the lookup, using the specified Cache, Resolver, and search path.
*
* @return The answers, or null if none are found.
*/
public Record[] run() {
if (done) {
reset();
}
if (name.isAbsolute()) {
resolve(name, null);
} else if (searchPath == null) {
resolve(name, Name.root);
} else {
if (name.labels() > ndots) {
resolve(name, Name.root);
}
if (done) {
return answers;
}
for (Name value : searchPath) {
resolve(name, value);
if (done) {
return answers;
} else if (foundAlias) {
break;
}
}
resolve(name, Name.root);
}
if (!done) {
if (badresponse) {
result = TRY_AGAIN;
error = badresponse_error;
done = true;
} else if (timedout) {
result = TRY_AGAIN;
error = "timed out";
done = true;
} else if (networkerror) {
result = TRY_AGAIN;
error = "network error";
done = true;
} else if (nxdomain) {
result = HOST_NOT_FOUND;
done = true;
} else if (referral) {
result = UNRECOVERABLE;
error = "referral";
done = true;
} else if (nametoolong) {
result = UNRECOVERABLE;
error = "name too long";
done = true;
}
}
return answers;
}
private void checkDone() {
if (done && result != -1) {
return;
}
StringBuilder sb = new StringBuilder("Lookup of " + name + " ");
if (dclass != DClass.IN) {
sb.append(DClass.string(dclass)).append(" ");
}
sb.append(Type.string(type)).append(" isn't done");
throw new IllegalStateException(sb.toString());
}
/**
* Returns the answers from the lookup.
*
* @return The answers, or null if none are found.
* @throws IllegalStateException The lookup has not completed.
*/
public Record[] getAnswers() {
checkDone();
return answers;
}
/**
* Returns all known aliases for this name. Whenever a CNAME/DNAME is followed, an alias is added
* to this array. The last element in this array will be the owner name for records in the answer,
* if there are any.
*
* @return The aliases.
* @throws IllegalStateException The lookup has not completed.
*/
public Name[] getAliases() {
checkDone();
if (aliases == null) {
return noAliases;
}
return aliases.toArray(new Name[0]);
}
/**
* Returns the result code of the lookup.
*
* @return The result code, which can be SUCCESSFUL, UNRECOVERABLE, TRY_AGAIN, HOST_NOT_FOUND, or
* TYPE_NOT_FOUND.
* @throws IllegalStateException The lookup has not completed.
*/
public int getResult() {
checkDone();
return result;
}
/**
* Returns an error string describing the result code of this lookup.
*
* @return A string, which may either directly correspond the result code or be more specific.
* @throws IllegalStateException The lookup has not completed.
*/
public String getErrorString() {
checkDone();
if (error != null) {
return error;
}
switch (result) {
case SUCCESSFUL:
return "successful";
case UNRECOVERABLE:
return "unrecoverable error";
case TRY_AGAIN:
return "try again";
case HOST_NOT_FOUND:
return "host not found";
case TYPE_NOT_FOUND:
return "type not found";
}
throw new IllegalStateException("unknown result");
}
}