// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org) package io.milton.dns.record; import io.milton.dns.Name; import io.milton.dns.TextParseException; import java.util.*; import java.io.*; import java.net.*; /** * The Lookup object issues queries to caching 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 Resolver, which defaults to an ExtendedResolver initialized with the * resolvers located by the ResolverConfig class, performs the queries. A * search path of domain suffixes is used to resolve relative names, and is * also determined by the ResolverConfig class. * * A Lookup object may be reused, but should not be used by multiple threads. * * @see Cache * @see Resolver * @see ResolverConfig * * @author Brian Wellington */ public final class Lookup { private static Resolver defaultResolver; private static Name [] defaultSearchPath; private static Map defaultCaches; private static int defaultNdots; private Resolver resolver; private Name [] searchPath; private Cache cache; private boolean temporary_cache; private int credibility; private Name name; private int type; private int dclass; private boolean verbose; 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 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() { try { defaultResolver = new ExtendedResolver(); } catch (UnknownHostException e) { throw new RuntimeException("Failed to initialize resolver"); } defaultSearchPath = ResolverConfig.getCurrentConfig().searchPath(); defaultCaches = new HashMap(); defaultNdots = ResolverConfig.getCurrentConfig().ndots(); } 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 = (Cache) defaultCaches.get(Mnemonic.toInteger(dclass)); if (c == null) { c = new Cache(dclass); defaultCaches.put(Mnemonic.toInteger(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(Mnemonic.toInteger(dclass), cache); } /** * Gets the search path that will be used as the default by future Lookups. * @return The default search path. */ public static synchronized Name [] getDefaultSearchPath() { return defaultSearchPath; } /** * Sets the search path to be used as the default by future Lookups. * @param domains The default search path. */ public static synchronized void setDefaultSearchPath(Name [] domains) { defaultSearchPath = 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; } Name [] newdomains = new Name[domains.length]; for (int i = 0; i < domains.length; i++) newdomains[i] = Name.fromString(domains[i], Name.root); defaultSearchPath = newdomains; } private final 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.credibility = Credibility.NORMAL; this.verbose = Options.check("verbose"); this.result = -1; } /** * 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. */ public void setSearchPath(Name [] domains) { this.searchPath = 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; } Name [] newdomains = new Name[domains.length]; for (int i = 0; i < domains.length; i++) newdomains[i] = Name.fromString(domains[i], 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 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); defaultNdots = 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; } private void follow(Name name, Name oldname) { foundAlias = true; badresponse = false; networkerror = false; timedout = false; nxdomain = false; referral = false; iterations++; if (iterations >= 6 || 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()) { RRset [] rrsets = response.answers(); List l = new ArrayList(); Iterator it; int i; for (i = 0; i < rrsets.length; i++) { it = rrsets[i].rrs(); while (it.hasNext()) l.add(it.next()); } result = SUCCESSFUL; answers = (Record []) l.toArray(new Record[l.size()]); 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) { SetResponse sr = cache.lookupRecords(current, type, credibility); if (verbose) { System.err.println("lookup " + current + " " + Type.string(type)); System.err.println(sr); } processResponse(current, sr); if (done || doneCurrent) return; Record question = Record.newRecord(current, type, dclass); Message query = Message.newQuery(question); Message response = null; try { response = resolver.send(query); } catch (IOException 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); if (verbose) { System.err.println("queried " + current + " " + Type.string(type)); System.err.println(sr); } processResponse(current, sr); } private void resolve(Name current, Name suffix) { doneCurrent = false; Name tname = null; 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() > defaultNdots) resolve(name, Name.root); if (done) return answers; for (int i = 0; i < searchPath.length; i++) { resolve(name, searchPath[i]); if (done) return answers; else if (foundAlias) break; } } 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 (Name []) aliases.toArray(new Name[aliases.size()]); } /** * 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"); } }