// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org) package com.netifera.platform.net.dns.service.client; import java.io.InterruptedIOException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.TimeoutException; import org.xbill.DNS.CNAMERecord; import org.xbill.DNS.Cache; import org.xbill.DNS.Credibility; import org.xbill.DNS.DClass; import org.xbill.DNS.DNAMERecord; import org.xbill.DNS.Message; import org.xbill.DNS.Name; import org.xbill.DNS.NameTooLongException; import org.xbill.DNS.Options; import org.xbill.DNS.RRset; import org.xbill.DNS.Rcode; import org.xbill.DNS.Record; import org.xbill.DNS.Resolver; import org.xbill.DNS.ResolverConfig; import org.xbill.DNS.ResolverListener; import org.xbill.DNS.SetResponse; import org.xbill.DNS.TextParseException; import org.xbill.DNS.Type; import com.netifera.platform.net.sockets.CompletionHandler; /** * 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 AsynchronousLookup { private static Resolver defaultResolver; private static Name[] defaultSearchPath; private static Map<Integer, Cache> defaultCaches = new HashMap<Integer,Cache>(); 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<Name> 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; /** * 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 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 AsynchronousLookup(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 (AsynchronousLookup.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 AsynchronousLookup(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 AsynchronousLookup(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 AsynchronousLookup(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 AsynchronousLookup(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 AsynchronousLookup(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 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 <A> void follow(Name name, Name oldname, A attachment, CompletionHandler<Record[],A> handler) { 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; handler.failed(new Exception(error),attachment); return; } if (aliases == null) aliases = new ArrayList<Name>(); aliases.add(oldname); lookup(name, attachment, handler); } private <A> void processResponse(Name name, SetResponse response, A attachment, CompletionHandler<Record[],A> handler) { if (response.isSuccessful()) { RRset[] rrsets = response.answers(); List<Record> l = new ArrayList<Record>(); Iterator<Record> it; int i; for (i = 0; i < rrsets.length; i++) { it = rrsets[i].rrs(); while (it.hasNext()) l.add(it.next()); } result = SUCCESSFUL; answers = l.toArray(new Record[l.size()]); done = true; handler.completed(getAnswers(),attachment); } else if (response.isNXDOMAIN()) { nxdomain = true; doneCurrent = true; //if (iterations > 0) { result = HOST_NOT_FOUND; done = true; handler.failed(new UnknownHostException(),attachment); // } } else if (response.isNXRRSET()) { result = TYPE_NOT_FOUND; answers = null; done = true; handler.failed(new Exception(getErrorString()),attachment); } else if (response.isCNAME()) { CNAMERecord cname = response.getCNAME(); follow(cname.getTarget(), name, attachment, handler); } else if (response.isDNAME()) { DNAMERecord dname = response.getDNAME(); // Name newname = null; try { follow(name.fromDNAME(dname), name, attachment, handler); } catch (NameTooLongException e) { result = UNRECOVERABLE; error = "Invalid DNAME target"; done = true; // checkError(); handler.failed(new Exception(error),attachment); } } else if (response.isDelegation()) { // We shouldn't get a referral. Ignore it. referral = true; } } private synchronized <A> void lookup(final Name current, final A attachment, final CompletionHandler<Record[],A> handler) { // SetResponse sr = cache.lookupRecords(current, type, credibility); // if (verbose) { // System.err.println("lookup " + current + " " + Type.string(type)); // System.err.println(sr); // } // processResponse(current, sr, attachment, handler); // if (done || doneCurrent) // return; final Record question = Record.newRecord(current, type, dclass); final Message query = Message.newQuery(question); // Message response = null; resolver.sendAsync(query, new ResolverListener() { public void handleException(Object id, Exception e) { if (e instanceof InterruptedIOException || e instanceof TimeoutException) timedout = true; else networkerror = true; checkError(); handler.failed(e,attachment); } public void receiveMessage(Object id, Message response) { int rcode = response.getHeader().getRcode(); if (rcode != Rcode.NOERROR && rcode != Rcode.NXDOMAIN) { if(rcode == Rcode.SERVFAIL) result = TRY_AGAIN; // The server we contacted is broken or otherwise unhelpful. // Press on. badresponse = true; badresponse_error = Rcode.string(rcode); checkError(); handler.failed(new Exception(getErrorString()),attachment); 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"; checkError(); handler.failed(new Exception(getErrorString()),attachment); return; } SetResponse 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, attachment, handler); } }); } private synchronized <A> void resolve(Name current, Name suffix, A attachment, CompletionHandler<Record[],A> handler) { doneCurrent = false; Name tname = null; if (suffix == null) tname = current; else { try { tname = Name.concatenate(current, suffix); } catch (NameTooLongException e) { nametoolong = true; checkError(); handler.failed(e, attachment); return; } } lookup(tname, attachment, handler); } /** * Performs the lookup, using the specified Cache, Resolver, and search * path. * * @return The answers, or null if none are found. */ public synchronized <A> void run(A attachment, CompletionHandler<Record[],A> handler) { if (done) reset(); if (name.isAbsolute()) resolve(name, null, attachment, handler); else if (searchPath == null) resolve(name, Name.root, attachment, handler); else { throw new RuntimeException(this.getClass().getName()+": Search path not implemented"); /* if (name.labels() > 1) resolve(name, Name.root, handler); if (done) return; for (int i = 0; i < searchPath.length; i++) { resolve(name, searchPath[i], handler); if (done) return answers; else if (foundAlias) break; } */ } } private void checkError() { 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; } if (verbose) System.err.println(name+" lookup failed: "+getErrorString()); } } private void checkDone() { if (done && result != -1) return; StringBuffer sb = new StringBuffer("Lookup of " + name + " "); if (dclass != DClass.IN) sb.append(DClass.string(dclass) + " "); sb.append(Type.string(type) + " 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[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"); } }