Cache.java revision d7955ce24d294fb2014c59d11fca184471056f44
1// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
2
3package org.xbill.DNS;
4
5import java.io.*;
6import java.util.*;
7
8/**
9 * A cache of DNS records.  The cache obeys TTLs, so items are purged after
10 * their validity period is complete.  Negative answers are cached, to
11 * avoid repeated failed DNS queries.  The credibility of each RRset is
12 * maintained, so that more credible records replace less credible records,
13 * and lookups can specify the minimum credibility of data they are requesting.
14 * @see RRset
15 * @see Credibility
16 *
17 * @author Brian Wellington
18 */
19
20public class Cache {
21
22private interface Element {
23	public boolean expired();
24	public int compareCredibility(int cred);
25	public int getType();
26}
27
28private static int
29limitExpire(long ttl, long maxttl) {
30	if (maxttl >= 0 && maxttl < ttl)
31		ttl = maxttl;
32	long expire = (System.currentTimeMillis() / 1000) + ttl;
33	if (expire < 0 || expire > Integer.MAX_VALUE)
34		return Integer.MAX_VALUE;
35	return (int)expire;
36}
37
38private static class CacheRRset extends RRset implements Element {
39	private static final long serialVersionUID = 5971755205903597024L;
40
41	int credibility;
42	int expire;
43
44	public
45	CacheRRset(Record rec, int cred, long maxttl) {
46		super();
47		this.credibility = cred;
48		this.expire = limitExpire(rec.getTTL(), maxttl);
49		addRR(rec);
50	}
51
52	public
53	CacheRRset(RRset rrset, int cred, long maxttl) {
54		super(rrset);
55		this.credibility = cred;
56		this.expire = limitExpire(rrset.getTTL(), maxttl);
57	}
58
59	public final boolean
60	expired() {
61		int now = (int)(System.currentTimeMillis() / 1000);
62		return (now >= expire);
63	}
64
65	public final int
66	compareCredibility(int cred) {
67		return credibility - cred;
68	}
69
70	public String
71	toString() {
72		StringBuffer sb = new StringBuffer();
73		sb.append(super.toString());
74		sb.append(" cl = ");
75		sb.append(credibility);
76		return sb.toString();
77	}
78}
79
80private static class NegativeElement implements Element {
81	int type;
82	Name name;
83	int credibility;
84	int expire;
85
86	public
87	NegativeElement(Name name, int type, SOARecord soa, int cred,
88			long maxttl)
89	{
90		this.name = name;
91		this.type = type;
92		long cttl = 0;
93		if (soa != null)
94			cttl = soa.getMinimum();
95		this.credibility = cred;
96		this.expire = limitExpire(cttl, maxttl);
97	}
98
99	public int
100	getType() {
101		return type;
102	}
103
104	public final boolean
105	expired() {
106		int now = (int)(System.currentTimeMillis() / 1000);
107		return (now >= expire);
108	}
109
110	public final int
111	compareCredibility(int cred) {
112		return credibility - cred;
113	}
114
115	public String
116	toString() {
117		StringBuffer sb = new StringBuffer();
118		if (type == 0)
119			sb.append("NXDOMAIN " + name);
120		else
121			sb.append("NXRRSET " + name + " " + Type.string(type));
122		sb.append(" cl = ");
123		sb.append(credibility);
124		return sb.toString();
125	}
126}
127
128private static class CacheMap extends LinkedHashMap {
129	private int maxsize = -1;
130
131	CacheMap(int maxsize) {
132		super(16, (float) 0.75, true);
133		this.maxsize = maxsize;
134	}
135
136	int
137	getMaxSize() {
138		return maxsize;
139	}
140
141	void
142	setMaxSize(int maxsize) {
143		/*
144		 * Note that this doesn't shrink the size of the map if
145		 * the maximum size is lowered, but it should shrink as
146		 * entries expire.
147		 */
148		this.maxsize = maxsize;
149	}
150
151	protected boolean removeEldestEntry(Map.Entry eldest) {
152		return maxsize >= 0 && size() > maxsize;
153	}
154}
155
156private CacheMap data;
157private int maxncache = -1;
158private int maxcache = -1;
159private int dclass;
160
161private static final int defaultMaxEntries = 50000;
162
163/**
164 * Creates an empty Cache
165 *
166 * @param dclass The DNS class of this cache
167 * @see DClass
168 */
169public
170Cache(int dclass) {
171	this.dclass = dclass;
172	data = new CacheMap(defaultMaxEntries);
173}
174
175/**
176 * Creates an empty Cache for class IN.
177 * @see DClass
178 */
179public
180Cache() {
181	this(DClass.IN);
182}
183
184/**
185 * Creates a Cache which initially contains all records in the specified file.
186 */
187public
188Cache(String file) throws IOException {
189	data = new CacheMap(defaultMaxEntries);
190	Master m = new Master(file);
191	Record record;
192	while ((record = m.nextRecord()) != null)
193		addRecord(record, Credibility.HINT, m);
194}
195
196private synchronized Object
197exactName(Name name) {
198	return data.get(name);
199}
200
201private synchronized void
202removeName(Name name) {
203	data.remove(name);
204}
205
206private synchronized Element []
207allElements(Object types) {
208	if (types instanceof List) {
209		List typelist = (List) types;
210		int size = typelist.size();
211		return (Element []) typelist.toArray(new Element[size]);
212	} else {
213		Element set = (Element) types;
214		return new Element[] {set};
215	}
216}
217
218private synchronized Element
219oneElement(Name name, Object types, int type, int minCred) {
220	Element found = null;
221
222	if (type == Type.ANY)
223		throw new IllegalArgumentException("oneElement(ANY)");
224	if (types instanceof List) {
225		List list = (List) types;
226		for (int i = 0; i < list.size(); i++) {
227			Element set = (Element) list.get(i);
228			if (set.getType() == type) {
229				found = set;
230				break;
231			}
232		}
233	} else {
234		Element set = (Element) types;
235		if (set.getType() == type)
236			found = set;
237	}
238	if (found == null)
239		return null;
240	if (found.expired()) {
241		removeElement(name, type);
242		return null;
243	}
244	if (found.compareCredibility(minCred) < 0)
245		return null;
246	return found;
247}
248
249private synchronized Element
250findElement(Name name, int type, int minCred) {
251	Object types = exactName(name);
252	if (types == null)
253		return null;
254	return oneElement(name, types, type, minCred);
255}
256
257private synchronized void
258addElement(Name name, Element element) {
259	Object types = data.get(name);
260	if (types == null) {
261		data.put(name, element);
262		return;
263	}
264	int type = element.getType();
265	if (types instanceof List) {
266		List list = (List) types;
267		for (int i = 0; i < list.size(); i++) {
268			Element elt = (Element) list.get(i);
269			if (elt.getType() == type) {
270				list.set(i, element);
271				return;
272			}
273		}
274		list.add(element);
275	} else {
276		Element elt = (Element) types;
277		if (elt.getType() == type)
278			data.put(name, element);
279		else {
280			LinkedList list = new LinkedList();
281			list.add(elt);
282			list.add(element);
283			data.put(name, list);
284		}
285	}
286}
287
288private synchronized void
289removeElement(Name name, int type) {
290	Object types = data.get(name);
291	if (types == null) {
292		return;
293	}
294	if (types instanceof List) {
295		List list = (List) types;
296		for (int i = 0; i < list.size(); i++) {
297			Element elt = (Element) list.get(i);
298			if (elt.getType() == type) {
299				list.remove(i);
300				if (list.size() == 0)
301					data.remove(name);
302				return;
303			}
304		}
305	} else {
306		Element elt = (Element) types;
307		if (elt.getType() != type)
308			return;
309		data.remove(name);
310	}
311}
312
313/** Empties the Cache. */
314public synchronized void
315clearCache() {
316	data.clear();
317}
318
319/**
320 * Adds a record to the Cache.
321 * @param r The record to be added
322 * @param cred The credibility of the record
323 * @param o The source of the record (this could be a Message, for example)
324 * @see Record
325 */
326public synchronized void
327addRecord(Record r, int cred, Object o) {
328	Name name = r.getName();
329	int type = r.getRRsetType();
330	if (!Type.isRR(type))
331		return;
332	Element element = findElement(name, type, cred);
333	if (element == null) {
334		CacheRRset crrset = new CacheRRset(r, cred, maxcache);
335		addRRset(crrset, cred);
336	} else if (element.compareCredibility(cred) == 0) {
337		if (element instanceof CacheRRset) {
338			CacheRRset crrset = (CacheRRset) element;
339			crrset.addRR(r);
340		}
341	}
342}
343
344/**
345 * Adds an RRset to the Cache.
346 * @param rrset The RRset to be added
347 * @param cred The credibility of these records
348 * @see RRset
349 */
350public synchronized void
351addRRset(RRset rrset, int cred) {
352	long ttl = rrset.getTTL();
353	Name name = rrset.getName();
354	int type = rrset.getType();
355	Element element = findElement(name, type, 0);
356	if (ttl == 0) {
357		if (element != null && element.compareCredibility(cred) <= 0)
358			removeElement(name, type);
359	} else {
360		if (element != null && element.compareCredibility(cred) <= 0)
361			element = null;
362		if (element == null) {
363			CacheRRset crrset;
364			if (rrset instanceof CacheRRset)
365				crrset = (CacheRRset) rrset;
366			else
367				crrset = new CacheRRset(rrset, cred, maxcache);
368			addElement(name, crrset);
369		}
370	}
371}
372
373/**
374 * Adds a negative entry to the Cache.
375 * @param name The name of the negative entry
376 * @param type The type of the negative entry
377 * @param soa The SOA record to add to the negative cache entry, or null.
378 * The negative cache ttl is derived from the SOA.
379 * @param cred The credibility of the negative entry
380 */
381public synchronized void
382addNegative(Name name, int type, SOARecord soa, int cred) {
383	long ttl = 0;
384	if (soa != null)
385		ttl = soa.getTTL();
386	Element element = findElement(name, type, 0);
387	if (ttl == 0) {
388		if (element != null && element.compareCredibility(cred) <= 0)
389			removeElement(name, type);
390	} else {
391		if (element != null && element.compareCredibility(cred) <= 0)
392			element = null;
393		if (element == null)
394			addElement(name, new NegativeElement(name, type,
395							     soa, cred,
396							     maxncache));
397	}
398}
399
400/**
401 * Finds all matching sets or something that causes the lookup to stop.
402 */
403protected synchronized SetResponse
404lookup(Name name, int type, int minCred) {
405	int labels;
406	int tlabels;
407	Element element;
408	Name tname;
409	Object types;
410	SetResponse sr;
411
412	labels = name.labels();
413
414	for (tlabels = labels; tlabels >= 1; tlabels--) {
415		boolean isRoot = (tlabels == 1);
416		boolean isExact = (tlabels == labels);
417
418		if (isRoot)
419			tname = Name.root;
420		else if (isExact)
421			tname = name;
422		else
423			tname = new Name(name, labels - tlabels);
424
425		types = data.get(tname);
426		if (types == null)
427			continue;
428
429		/*
430		 * If this is the name, look for the actual type or a CNAME
431		 * (unless it's an ANY query, where we return everything).
432		 * Otherwise, look for a DNAME.
433		 */
434		if (isExact && type == Type.ANY) {
435			sr = new SetResponse(SetResponse.SUCCESSFUL);
436			Element [] elements = allElements(types);
437			int added = 0;
438			for (int i = 0; i < elements.length; i++) {
439				element = elements[i];
440				if (element.expired()) {
441					removeElement(tname, element.getType());
442					continue;
443				}
444				if (!(element instanceof CacheRRset))
445					continue;
446				if (element.compareCredibility(minCred) < 0)
447					continue;
448				sr.addRRset((CacheRRset)element);
449				added++;
450			}
451			/* There were positive entries */
452			if (added > 0)
453				return sr;
454		} else if (isExact) {
455			element = oneElement(tname, types, type, minCred);
456			if (element != null &&
457			    element instanceof CacheRRset)
458			{
459				sr = new SetResponse(SetResponse.SUCCESSFUL);
460				sr.addRRset((CacheRRset) element);
461				return sr;
462			} else if (element != null) {
463				sr = new SetResponse(SetResponse.NXRRSET);
464				return sr;
465			}
466
467			element = oneElement(tname, types, Type.CNAME, minCred);
468			if (element != null &&
469			    element instanceof CacheRRset)
470			{
471				return new SetResponse(SetResponse.CNAME,
472						       (CacheRRset) element);
473			}
474		} else {
475			element = oneElement(tname, types, Type.DNAME, minCred);
476			if (element != null &&
477			    element instanceof CacheRRset)
478			{
479				return new SetResponse(SetResponse.DNAME,
480						       (CacheRRset) element);
481			}
482		}
483
484		/* Look for an NS */
485		element = oneElement(tname, types, Type.NS, minCred);
486		if (element != null && element instanceof CacheRRset)
487			return new SetResponse(SetResponse.DELEGATION,
488					       (CacheRRset) element);
489
490		/* Check for the special NXDOMAIN element. */
491		if (isExact) {
492			element = oneElement(tname, types, 0, minCred);
493			if (element != null)
494				return SetResponse.ofType(SetResponse.NXDOMAIN);
495		}
496
497	}
498	return SetResponse.ofType(SetResponse.UNKNOWN);
499}
500
501/**
502 * Looks up Records in the Cache.  This follows CNAMEs and handles negatively
503 * cached data.
504 * @param name The name to look up
505 * @param type The type to look up
506 * @param minCred The minimum acceptable credibility
507 * @return A SetResponse object
508 * @see SetResponse
509 * @see Credibility
510 */
511public SetResponse
512lookupRecords(Name name, int type, int minCred) {
513	return lookup(name, type, minCred);
514}
515
516private RRset []
517findRecords(Name name, int type, int minCred) {
518	SetResponse cr = lookupRecords(name, type, minCred);
519	if (cr.isSuccessful())
520		return cr.answers();
521	else
522		return null;
523}
524
525/**
526 * Looks up credible Records in the Cache (a wrapper around lookupRecords).
527 * Unlike lookupRecords, this given no indication of why failure occurred.
528 * @param name The name to look up
529 * @param type The type to look up
530 * @return An array of RRsets, or null
531 * @see Credibility
532 */
533public RRset []
534findRecords(Name name, int type) {
535	return findRecords(name, type, Credibility.NORMAL);
536}
537
538/**
539 * Looks up Records in the Cache (a wrapper around lookupRecords).  Unlike
540 * lookupRecords, this given no indication of why failure occurred.
541 * @param name The name to look up
542 * @param type The type to look up
543 * @return An array of RRsets, or null
544 * @see Credibility
545 */
546public RRset []
547findAnyRecords(Name name, int type) {
548	return findRecords(name, type, Credibility.GLUE);
549}
550
551private final int
552getCred(int section, boolean isAuth) {
553	if (section == Section.ANSWER) {
554		if (isAuth)
555			return Credibility.AUTH_ANSWER;
556		else
557			return Credibility.NONAUTH_ANSWER;
558	} else if (section == Section.AUTHORITY) {
559		if (isAuth)
560			return Credibility.AUTH_AUTHORITY;
561		else
562			return Credibility.NONAUTH_AUTHORITY;
563	} else if (section == Section.ADDITIONAL) {
564		return Credibility.ADDITIONAL;
565	} else
566		throw new IllegalArgumentException("getCred: invalid section");
567}
568
569private static void
570markAdditional(RRset rrset, Set names) {
571	Record first = rrset.first();
572	if (first.getAdditionalName() == null)
573		return;
574
575	Iterator it = rrset.rrs();
576	while (it.hasNext()) {
577		Record r = (Record) it.next();
578		Name name = r.getAdditionalName();
579		if (name != null)
580			names.add(name);
581	}
582}
583
584/**
585 * Adds all data from a Message into the Cache.  Each record is added with
586 * the appropriate credibility, and negative answers are cached as such.
587 * @param in The Message to be added
588 * @return A SetResponse that reflects what would be returned from a cache
589 * lookup, or null if nothing useful could be cached from the message.
590 * @see Message
591 */
592public SetResponse
593addMessage(Message in) {
594	boolean isAuth = in.getHeader().getFlag(Flags.AA);
595	Record question = in.getQuestion();
596	Name qname;
597	Name curname;
598	int qtype;
599	int qclass;
600	int cred;
601	int rcode = in.getHeader().getRcode();
602	boolean completed = false;
603	RRset [] answers, auth, addl;
604	SetResponse response = null;
605	boolean verbose = Options.check("verbosecache");
606	HashSet additionalNames;
607
608	if ((rcode != Rcode.NOERROR && rcode != Rcode.NXDOMAIN) ||
609	    question == null)
610		return null;
611
612	qname = question.getName();
613	qtype = question.getType();
614	qclass = question.getDClass();
615
616	curname = qname;
617
618	additionalNames = new HashSet();
619
620	answers = in.getSectionRRsets(Section.ANSWER);
621	for (int i = 0; i < answers.length; i++) {
622		if (answers[i].getDClass() != qclass)
623			continue;
624		int type = answers[i].getType();
625		Name name = answers[i].getName();
626		cred = getCred(Section.ANSWER, isAuth);
627		if ((type == qtype || qtype == Type.ANY) &&
628		    name.equals(curname))
629		{
630			addRRset(answers[i], cred);
631			completed = true;
632			if (curname == qname) {
633				if (response == null)
634					response = new SetResponse(
635							SetResponse.SUCCESSFUL);
636				response.addRRset(answers[i]);
637			}
638			markAdditional(answers[i], additionalNames);
639		} else if (type == Type.CNAME && name.equals(curname)) {
640			CNAMERecord cname;
641			addRRset(answers[i], cred);
642			if (curname == qname)
643				response = new SetResponse(SetResponse.CNAME,
644							   answers[i]);
645			cname = (CNAMERecord) answers[i].first();
646			curname = cname.getTarget();
647		} else if (type == Type.DNAME && curname.subdomain(name)) {
648			DNAMERecord dname;
649			addRRset(answers[i], cred);
650			if (curname == qname)
651				response = new SetResponse(SetResponse.DNAME,
652							   answers[i]);
653			dname = (DNAMERecord) answers[i].first();
654			try {
655				curname = curname.fromDNAME(dname);
656			}
657			catch (NameTooLongException e) {
658				break;
659			}
660		}
661	}
662
663	auth = in.getSectionRRsets(Section.AUTHORITY);
664	RRset soa = null, ns = null;
665	for (int i = 0; i < auth.length; i++) {
666		if (auth[i].getType() == Type.SOA &&
667		    curname.subdomain(auth[i].getName()))
668			soa = auth[i];
669		else if (auth[i].getType() == Type.NS &&
670			 curname.subdomain(auth[i].getName()))
671			ns = auth[i];
672	}
673	if (!completed) {
674		/* This is a negative response or a referral. */
675		int cachetype = (rcode == Rcode.NXDOMAIN) ? 0 : qtype;
676		if (rcode == Rcode.NXDOMAIN || soa != null || ns == null) {
677			/* Negative response */
678			cred = getCred(Section.AUTHORITY, isAuth);
679			SOARecord soarec = null;
680			if (soa != null)
681				soarec = (SOARecord) soa.first();
682			addNegative(curname, cachetype, soarec, cred);
683			if (response == null) {
684				int responseType;
685				if (rcode == Rcode.NXDOMAIN)
686					responseType = SetResponse.NXDOMAIN;
687				else
688					responseType = SetResponse.NXRRSET;
689				response = SetResponse.ofType(responseType);
690			}
691			/* DNSSEC records are not cached. */
692		} else {
693			/* Referral response */
694			cred = getCred(Section.AUTHORITY, isAuth);
695			addRRset(ns, cred);
696			markAdditional(ns, additionalNames);
697			if (response == null)
698				response = new SetResponse(
699							SetResponse.DELEGATION,
700							ns);
701		}
702	} else if (rcode == Rcode.NOERROR && ns != null) {
703		/* Cache the NS set from a positive response. */
704		cred = getCred(Section.AUTHORITY, isAuth);
705		addRRset(ns, cred);
706		markAdditional(ns, additionalNames);
707	}
708
709	addl = in.getSectionRRsets(Section.ADDITIONAL);
710	for (int i = 0; i < addl.length; i++) {
711		int type = addl[i].getType();
712		if (type != Type.A && type != Type.AAAA && type != Type.A6)
713			continue;
714		Name name = addl[i].getName();
715		if (!additionalNames.contains(name))
716			continue;
717		cred = getCred(Section.ADDITIONAL, isAuth);
718		addRRset(addl[i], cred);
719	}
720	if (verbose)
721		System.out.println("addMessage: " + response);
722	return (response);
723}
724
725/**
726 * Flushes an RRset from the cache
727 * @param name The name of the records to be flushed
728 * @param type The type of the records to be flushed
729 * @see RRset
730 */
731public void
732flushSet(Name name, int type) {
733	removeElement(name, type);
734}
735
736/**
737 * Flushes all RRsets with a given name from the cache
738 * @param name The name of the records to be flushed
739 * @see RRset
740 */
741public void
742flushName(Name name) {
743	removeName(name);
744}
745
746/**
747 * Sets the maximum length of time that a negative response will be stored
748 * in this Cache.  A negative value disables this feature (that is, sets
749 * no limit).
750 */
751public void
752setMaxNCache(int seconds) {
753	maxncache = seconds;
754}
755
756/**
757 * Gets the maximum length of time that a negative response will be stored
758 * in this Cache.  A negative value indicates no limit.
759 */
760public int
761getMaxNCache() {
762	return maxncache;
763}
764
765/**
766 * Sets the maximum length of time that records will be stored in this
767 * Cache.  A negative value disables this feature (that is, sets no limit).
768 */
769public void
770setMaxCache(int seconds) {
771	maxcache = seconds;
772}
773
774/**
775 * Gets the maximum length of time that records will be stored
776 * in this Cache.  A negative value indicates no limit.
777 */
778public int
779getMaxCache() {
780	return maxcache;
781}
782
783/**
784 * Gets the current number of entries in the Cache, where an entry consists
785 * of all records with a specific Name.
786 */
787public int
788getSize() {
789	return data.size();
790}
791
792/**
793 * Gets the maximum number of entries in the Cache, where an entry consists
794 * of all records with a specific Name.  A negative value is treated as an
795 * infinite limit.
796 */
797public int
798getMaxEntries() {
799	return data.getMaxSize();
800}
801
802/**
803 * Sets the maximum number of entries in the Cache, where an entry consists
804 * of all records with a specific Name.  A negative value is treated as an
805 * infinite limit.
806 *
807 * Note that setting this to a value lower than the current number
808 * of entries will not cause the Cache to shrink immediately.
809 *
810 * The default maximum number of entries is 50000.
811 *
812 * @param entries The maximum number of entries in the Cache.
813 */
814public void
815setMaxEntries(int entries) {
816	data.setMaxSize(entries);
817}
818
819/**
820 * Returns the DNS class of this cache.
821 */
822public int
823getDClass() {
824	return dclass;
825}
826
827/**
828 * Returns the contents of the Cache as a string.
829 */
830public String
831toString() {
832	StringBuffer sb = new StringBuffer();
833	synchronized (this) {
834		Iterator it = data.values().iterator();
835		while (it.hasNext()) {
836			Element [] elements = allElements(it.next());
837			for (int i = 0; i < elements.length; i++) {
838				sb.append(elements[i]);
839				sb.append("\n");
840			}
841		}
842	}
843	return sb.toString();
844}
845
846}
847