1// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org)
2
3package org.xbill.DNS;
4
5import java.util.*;
6import java.io.*;
7import java.net.*;
8
9/**
10 * The Lookup object issues queries to caching DNS servers.  The input consists
11 * of a name, an optional type, and an optional class.  Caching is enabled
12 * by default and used when possible to reduce the number of DNS requests.
13 * A Resolver, which defaults to an ExtendedResolver initialized with the
14 * resolvers located by the ResolverConfig class, performs the queries.  A
15 * search path of domain suffixes is used to resolve relative names, and is
16 * also determined by the ResolverConfig class.
17 *
18 * A Lookup object may be reused, but should not be used by multiple threads.
19 *
20 * @see Cache
21 * @see Resolver
22 * @see ResolverConfig
23 *
24 * @author Brian Wellington
25 */
26
27public final class Lookup {
28
29private static Resolver defaultResolver;
30private static Name [] defaultSearchPath;
31private static Map defaultCaches;
32private static int defaultNdots;
33
34private Resolver resolver;
35private Name [] searchPath;
36private Cache cache;
37private boolean temporary_cache;
38private int credibility;
39private Name name;
40private int type;
41private int dclass;
42private boolean verbose;
43private int iterations;
44private boolean foundAlias;
45private boolean done;
46private boolean doneCurrent;
47private List aliases;
48private Record [] answers;
49private int result;
50private String error;
51private boolean nxdomain;
52private boolean badresponse;
53private String badresponse_error;
54private boolean networkerror;
55private boolean timedout;
56private boolean nametoolong;
57private boolean referral;
58
59private static final Name [] noAliases = new Name[0];
60
61/** The lookup was successful. */
62public static final int SUCCESSFUL = 0;
63
64/**
65 * The lookup failed due to a data or server error. Repeating the lookup
66 * would not be helpful.
67 */
68public static final int UNRECOVERABLE = 1;
69
70/**
71 * The lookup failed due to a network error. Repeating the lookup may be
72 * helpful.
73 */
74public static final int TRY_AGAIN = 2;
75
76/** The host does not exist. */
77public static final int HOST_NOT_FOUND = 3;
78
79/** The host exists, but has no records associated with the queried type. */
80public static final int TYPE_NOT_FOUND = 4;
81
82public static synchronized void
83refreshDefault() {
84
85	try {
86		defaultResolver = new ExtendedResolver();
87	}
88	catch (UnknownHostException e) {
89		throw new RuntimeException("Failed to initialize resolver");
90	}
91	defaultSearchPath = ResolverConfig.getCurrentConfig().searchPath();
92	defaultCaches = new HashMap();
93	defaultNdots = ResolverConfig.getCurrentConfig().ndots();
94}
95
96static {
97	refreshDefault();
98}
99
100/**
101 * Gets the Resolver that will be used as the default by future Lookups.
102 * @return The default resolver.
103 */
104public static synchronized Resolver
105getDefaultResolver() {
106	return defaultResolver;
107}
108
109/**
110 * Sets the default Resolver to be used as the default by future Lookups.
111 * @param resolver The default resolver.
112 */
113public static synchronized void
114setDefaultResolver(Resolver resolver) {
115	defaultResolver = resolver;
116}
117
118/**
119 * Gets the Cache that will be used as the default for the specified
120 * class by future Lookups.
121 * @param dclass The class whose cache is being retrieved.
122 * @return The default cache for the specified class.
123 */
124public static synchronized Cache
125getDefaultCache(int dclass) {
126	DClass.check(dclass);
127	Cache c = (Cache) defaultCaches.get(Mnemonic.toInteger(dclass));
128	if (c == null) {
129		c = new Cache(dclass);
130		defaultCaches.put(Mnemonic.toInteger(dclass), c);
131	}
132	return c;
133}
134
135/**
136 * Sets the Cache to be used as the default for the specified class by future
137 * Lookups.
138 * @param cache The default cache for the specified class.
139 * @param dclass The class whose cache is being set.
140 */
141public static synchronized void
142setDefaultCache(Cache cache, int dclass) {
143	DClass.check(dclass);
144	defaultCaches.put(Mnemonic.toInteger(dclass), cache);
145}
146
147/**
148 * Gets the search path that will be used as the default by future Lookups.
149 * @return The default search path.
150 */
151public static synchronized Name []
152getDefaultSearchPath() {
153	return defaultSearchPath;
154}
155
156/**
157 * Sets the search path to be used as the default by future Lookups.
158 * @param domains The default search path.
159 */
160public static synchronized void
161setDefaultSearchPath(Name [] domains) {
162	defaultSearchPath = domains;
163}
164
165/**
166 * Sets the search path that will be used as the default by future Lookups.
167 * @param domains The default search path.
168 * @throws TextParseException A name in the array is not a valid DNS name.
169 */
170public static synchronized void
171setDefaultSearchPath(String [] domains) throws TextParseException {
172	if (domains == null) {
173		defaultSearchPath = null;
174		return;
175	}
176	Name [] newdomains = new Name[domains.length];
177	for (int i = 0; i < domains.length; i++)
178		newdomains[i] = Name.fromString(domains[i], Name.root);
179	defaultSearchPath = newdomains;
180}
181
182private final void
183reset() {
184	iterations = 0;
185	foundAlias = false;
186	done = false;
187	doneCurrent = false;
188	aliases = null;
189	answers = null;
190	result = -1;
191	error = null;
192	nxdomain = false;
193	badresponse = false;
194	badresponse_error = null;
195	networkerror = false;
196	timedout = false;
197	nametoolong = false;
198	referral = false;
199	if (temporary_cache)
200		cache.clearCache();
201}
202
203/**
204 * Create a Lookup object that will find records of the given name, type,
205 * and class.  The lookup will use the default cache, resolver, and search
206 * path, and look for records that are reasonably credible.
207 * @param name The name of the desired records
208 * @param type The type of the desired records
209 * @param dclass The class of the desired records
210 * @throws IllegalArgumentException The type is a meta type other than ANY.
211 * @see Cache
212 * @see Resolver
213 * @see Credibility
214 * @see Name
215 * @see Type
216 * @see DClass
217 */
218public
219Lookup(Name name, int type, int dclass) {
220	Type.check(type);
221	DClass.check(dclass);
222	if (!Type.isRR(type) && type != Type.ANY)
223		throw new IllegalArgumentException("Cannot query for " +
224						   "meta-types other than ANY");
225	this.name = name;
226	this.type = type;
227	this.dclass = dclass;
228	synchronized (Lookup.class) {
229		this.resolver = getDefaultResolver();
230		this.searchPath = getDefaultSearchPath();
231		this.cache = getDefaultCache(dclass);
232	}
233	this.credibility = Credibility.NORMAL;
234	this.verbose = Options.check("verbose");
235	this.result = -1;
236}
237
238/**
239 * Create a Lookup object that will find records of the given name and type
240 * in the IN class.
241 * @param name The name of the desired records
242 * @param type The type of the desired records
243 * @throws IllegalArgumentException The type is a meta type other than ANY.
244 * @see #Lookup(Name,int,int)
245 */
246public
247Lookup(Name name, int type) {
248	this(name, type, DClass.IN);
249}
250
251/**
252 * Create a Lookup object that will find records of type A at the given name
253 * in the IN class.
254 * @param name The name of the desired records
255 * @see #Lookup(Name,int,int)
256 */
257public
258Lookup(Name name) {
259	this(name, Type.A, DClass.IN);
260}
261
262/**
263 * Create a Lookup object that will find records of the given name, type,
264 * and class.
265 * @param name The name of the desired records
266 * @param type The type of the desired records
267 * @param dclass The class of the desired records
268 * @throws TextParseException The name is not a valid DNS name
269 * @throws IllegalArgumentException The type is a meta type other than ANY.
270 * @see #Lookup(Name,int,int)
271 */
272public
273Lookup(String name, int type, int dclass) throws TextParseException {
274	this(Name.fromString(name), type, dclass);
275}
276
277/**
278 * Create a Lookup object that will find records of the given name and type
279 * in the IN class.
280 * @param name The name of the desired records
281 * @param type The type of the desired records
282 * @throws TextParseException The name is not a valid DNS name
283 * @throws IllegalArgumentException The type is a meta type other than ANY.
284 * @see #Lookup(Name,int,int)
285 */
286public
287Lookup(String name, int type) throws TextParseException {
288	this(Name.fromString(name), type, DClass.IN);
289}
290
291/**
292 * Create a Lookup object that will find records of type A at the given name
293 * in the IN class.
294 * @param name The name of the desired records
295 * @throws TextParseException The name is not a valid DNS name
296 * @see #Lookup(Name,int,int)
297 */
298public
299Lookup(String name) throws TextParseException {
300	this(Name.fromString(name), Type.A, DClass.IN);
301}
302
303/**
304 * Sets the resolver to use when performing this lookup.  This overrides the
305 * default value.
306 * @param resolver The resolver to use.
307 */
308public void
309setResolver(Resolver resolver) {
310	this.resolver = resolver;
311}
312
313/**
314 * Sets the search path to use when performing this lookup.  This overrides the
315 * default value.
316 * @param domains An array of names containing the search path.
317 */
318public void
319setSearchPath(Name [] domains) {
320	this.searchPath = domains;
321}
322
323/**
324 * Sets the search path to use when performing this lookup. This overrides the
325 * default value.
326 * @param domains An array of names containing the search path.
327 * @throws TextParseException A name in the array is not a valid DNS name.
328 */
329public void
330setSearchPath(String [] domains) throws TextParseException {
331	if (domains == null) {
332		this.searchPath = null;
333		return;
334	}
335	Name [] newdomains = new Name[domains.length];
336	for (int i = 0; i < domains.length; i++)
337		newdomains[i] = Name.fromString(domains[i], Name.root);
338	this.searchPath = newdomains;
339}
340
341/**
342 * Sets the cache to use when performing this lookup.  This overrides the
343 * default value.  If the results of this lookup should not be permanently
344 * cached, null can be provided here.
345 * @param cache The cache to use.
346 */
347public void
348setCache(Cache cache) {
349	if (cache == null) {
350		this.cache = new Cache(dclass);
351		this.temporary_cache = true;
352	} else {
353		this.cache = cache;
354		this.temporary_cache = false;
355	}
356}
357
358/**
359 * Sets ndots to use when performing this lookup, overriding the default value.
360 * Specifically, this refers to the number of "dots" which, if present in a
361 * name, indicate that a lookup for the absolute name should be attempted
362 * before appending any search path elements.
363 * @param ndots The ndots value to use, which must be greater than or equal to
364 * 0.
365 */
366public void
367setNdots(int ndots) {
368	if (ndots < 0)
369		throw new IllegalArgumentException("Illegal ndots value: " +
370						   ndots);
371	defaultNdots = ndots;
372}
373
374/**
375 * Sets the minimum credibility level that will be accepted when performing
376 * the lookup.  This defaults to Credibility.NORMAL.
377 * @param credibility The minimum credibility level.
378 */
379public void
380setCredibility(int credibility) {
381	this.credibility = credibility;
382}
383
384private void
385follow(Name name, Name oldname) {
386	foundAlias = true;
387	badresponse = false;
388	networkerror = false;
389	timedout = false;
390	nxdomain = false;
391	referral = false;
392	iterations++;
393	if (iterations >= 6 || name.equals(oldname)) {
394		result = UNRECOVERABLE;
395		error = "CNAME loop";
396		done = true;
397		return;
398	}
399	if (aliases == null)
400		aliases = new ArrayList();
401	aliases.add(oldname);
402	lookup(name);
403}
404
405private void
406processResponse(Name name, SetResponse response) {
407	if (response.isSuccessful()) {
408		RRset [] rrsets = response.answers();
409		List l = new ArrayList();
410		Iterator it;
411		int i;
412
413		for (i = 0; i < rrsets.length; i++) {
414			it = rrsets[i].rrs();
415			while (it.hasNext())
416				l.add(it.next());
417		}
418
419		result = SUCCESSFUL;
420		answers = (Record []) l.toArray(new Record[l.size()]);
421		done = true;
422	} else if (response.isNXDOMAIN()) {
423		nxdomain = true;
424		doneCurrent = true;
425		if (iterations > 0) {
426			result = HOST_NOT_FOUND;
427			done = true;
428		}
429	} else if (response.isNXRRSET()) {
430		result = TYPE_NOT_FOUND;
431		answers = null;
432		done = true;
433	} else if (response.isCNAME()) {
434		CNAMERecord cname = response.getCNAME();
435		follow(cname.getTarget(), name);
436	} else if (response.isDNAME()) {
437		DNAMERecord dname = response.getDNAME();
438		try {
439			follow(name.fromDNAME(dname), name);
440		} catch (NameTooLongException e) {
441			result = UNRECOVERABLE;
442			error = "Invalid DNAME target";
443			done = true;
444		}
445	} else if (response.isDelegation()) {
446		// We shouldn't get a referral.  Ignore it.
447		referral = true;
448	}
449}
450
451private void
452lookup(Name current) {
453	SetResponse sr = cache.lookupRecords(current, type, credibility);
454	if (verbose) {
455		System.err.println("lookup " + current + " " +
456				   Type.string(type));
457		System.err.println(sr);
458	}
459	processResponse(current, sr);
460	if (done || doneCurrent)
461		return;
462
463	Record question = Record.newRecord(current, type, dclass);
464	Message query = Message.newQuery(question);
465	Message response = null;
466	try {
467		response = resolver.send(query);
468	}
469	catch (IOException e) {
470		// A network error occurred.  Press on.
471		if (e instanceof InterruptedIOException)
472			timedout = true;
473		else
474			networkerror = true;
475		return;
476	}
477	int rcode = response.getHeader().getRcode();
478	if (rcode != Rcode.NOERROR && rcode != Rcode.NXDOMAIN) {
479		// The server we contacted is broken or otherwise unhelpful.
480		// Press on.
481		badresponse = true;
482		badresponse_error = Rcode.string(rcode);
483		return;
484	}
485
486	if (!query.getQuestion().equals(response.getQuestion())) {
487		// The answer doesn't match the question.  That's not good.
488		badresponse = true;
489		badresponse_error = "response does not match query";
490		return;
491	}
492
493	sr = cache.addMessage(response);
494	if (sr == null)
495		sr = cache.lookupRecords(current, type, credibility);
496	if (verbose) {
497		System.err.println("queried " + current + " " +
498				   Type.string(type));
499		System.err.println(sr);
500	}
501	processResponse(current, sr);
502}
503
504private void
505resolve(Name current, Name suffix) {
506	doneCurrent = false;
507	Name tname = null;
508	if (suffix == null)
509		tname = current;
510	else {
511		try {
512			tname = Name.concatenate(current, suffix);
513		}
514		catch (NameTooLongException e) {
515			nametoolong = true;
516			return;
517		}
518	}
519	lookup(tname);
520}
521
522/**
523 * Performs the lookup, using the specified Cache, Resolver, and search path.
524 * @return The answers, or null if none are found.
525 */
526public Record []
527run() {
528	if (done)
529		reset();
530	if (name.isAbsolute())
531		resolve(name, null);
532	else if (searchPath == null)
533		resolve(name, Name.root);
534	else {
535		if (name.labels() > defaultNdots)
536			resolve(name, Name.root);
537		if (done)
538			return answers;
539
540		for (int i = 0; i < searchPath.length; i++) {
541			resolve(name, searchPath[i]);
542			if (done)
543				return answers;
544			else if (foundAlias)
545				break;
546		}
547	}
548	if (!done) {
549		if (badresponse) {
550			result = TRY_AGAIN;
551			error = badresponse_error;
552			done = true;
553		} else if (timedout) {
554			result = TRY_AGAIN;
555			error = "timed out";
556			done = true;
557		} else if (networkerror) {
558			result = TRY_AGAIN;
559			error = "network error";
560			done = true;
561		} else if (nxdomain) {
562			result = HOST_NOT_FOUND;
563			done = true;
564		} else if (referral) {
565			result = UNRECOVERABLE;
566			error = "referral";
567			done = true;
568		} else if (nametoolong) {
569			result = UNRECOVERABLE;
570			error = "name too long";
571			done = true;
572		}
573	}
574	return answers;
575}
576
577private void
578checkDone() {
579	if (done && result != -1)
580		return;
581	StringBuffer sb = new StringBuffer("Lookup of " + name + " ");
582	if (dclass != DClass.IN)
583		sb.append(DClass.string(dclass) + " ");
584	sb.append(Type.string(type) + " isn't done");
585	throw new IllegalStateException(sb.toString());
586}
587
588/**
589 * Returns the answers from the lookup.
590 * @return The answers, or null if none are found.
591 * @throws IllegalStateException The lookup has not completed.
592 */
593public Record []
594getAnswers() {
595	checkDone();
596	return answers;
597}
598
599/**
600 * Returns all known aliases for this name.  Whenever a CNAME/DNAME is
601 * followed, an alias is added to this array.  The last element in this
602 * array will be the owner name for records in the answer, if there are any.
603 * @return The aliases.
604 * @throws IllegalStateException The lookup has not completed.
605 */
606public Name []
607getAliases() {
608	checkDone();
609	if (aliases == null)
610		return noAliases;
611	return (Name []) aliases.toArray(new Name[aliases.size()]);
612}
613
614/**
615 * Returns the result code of the lookup.
616 * @return The result code, which can be SUCCESSFUL, UNRECOVERABLE, TRY_AGAIN,
617 * HOST_NOT_FOUND, or TYPE_NOT_FOUND.
618 * @throws IllegalStateException The lookup has not completed.
619 */
620public int
621getResult() {
622	checkDone();
623	return result;
624}
625
626/**
627 * Returns an error string describing the result code of this lookup.
628 * @return A string, which may either directly correspond the result code
629 * or be more specific.
630 * @throws IllegalStateException The lookup has not completed.
631 */
632public String
633getErrorString() {
634	checkDone();
635	if (error != null)
636		return error;
637	switch (result) {
638		case SUCCESSFUL:	return "successful";
639		case UNRECOVERABLE:	return "unrecoverable error";
640		case TRY_AGAIN:		return "try again";
641		case HOST_NOT_FOUND:	return "host not found";
642		case TYPE_NOT_FOUND:	return "type not found";
643	}
644	throw new IllegalStateException("unknown result");
645}
646
647}
648