KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > apache > james > dnsserver > DNSServer


1 /***********************************************************************
2  * Copyright (c) 2000-2004 The Apache Software Foundation. *
3  * All rights reserved. *
4  * ------------------------------------------------------------------- *
5  * Licensed under the Apache License, Version 2.0 (the "License"); you *
6  * may not use this file except in compliance with the License. You *
7  * may obtain a copy of the License at: *
8  * *
9  * http://www.apache.org/licenses/LICENSE-2.0 *
10  * *
11  * Unless required by applicable law or agreed to in writing, software *
12  * distributed under the License is distributed on an "AS IS" BASIS, *
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or *
14  * implied. See the License for the specific language governing *
15  * permissions and limitations under the License. *
16  ***********************************************************************/

17
18 package org.apache.james.dnsserver;
19
20 import org.apache.avalon.framework.activity.Initializable;
21 import org.apache.avalon.framework.activity.Disposable;
22 import org.apache.avalon.framework.configuration.Configurable;
23 import org.apache.avalon.framework.configuration.Configuration;
24 import org.apache.avalon.framework.configuration.ConfigurationException;
25 import org.apache.avalon.framework.logger.AbstractLogEnabled;
26 import org.xbill.DNS.Cache;
27 import org.xbill.DNS.Credibility;
28 import org.xbill.DNS.DClass;
29 import org.xbill.DNS.ExtendedResolver;
30 import org.xbill.DNS.FindServer;
31 import org.xbill.DNS.Lookup;
32 import org.xbill.DNS.Message;
33 import org.xbill.DNS.MXRecord;
34 import org.xbill.DNS.ARecord;
35 import org.xbill.DNS.Name;
36 import org.xbill.DNS.Rcode;
37 import org.xbill.DNS.Record;
38 import org.xbill.DNS.Resolver;
39 import org.xbill.DNS.RRset;
40 import org.xbill.DNS.SetResponse;
41 import org.xbill.DNS.TextParseException;
42 import org.xbill.DNS.Type;
43
44 import java.net.InetAddress JavaDoc;
45 import java.net.UnknownHostException JavaDoc;
46 import java.util.*;
47
48 /**
49  * Provides DNS client functionality to services running
50  * inside James
51  */

52 public class DNSServer
53     extends AbstractLogEnabled
54     implements Configurable, Initializable, Disposable,
55     org.apache.james.services.DNSServer, DNSServerMBean {
56
57     /**
58      * A resolver instance used to retrieve DNS records. This
59      * is a reference to a third party library object.
60      */

61     private Resolver resolver;
62
63     /**
64      * A TTL cache of results received from the DNS server. This
65      * is a reference to a third party library object.
66      */

67     private Cache cache;
68
69     /**
70      * Whether the DNS response is required to be authoritative
71      */

72     private int dnsCredibility;
73
74     /**
75      * The DNS servers to be used by this service
76      */

77     private List dnsServers = new ArrayList();
78
79     /**
80      * The MX Comparator used in the MX sort.
81      */

82     private Comparator mxComparator = new MXRecordComparator();
83
84     /**
85      * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
86      */

87     public void configure( final Configuration configuration )
88         throws ConfigurationException {
89
90         final boolean autodiscover =
91             configuration.getChild( "autodiscover" ).getValueAsBoolean( true );
92
93         if (autodiscover) {
94             getLogger().info("Autodiscovery is enabled - trying to discover your system's DNS Servers");
95             String JavaDoc[] serversArray = FindServer.servers();
96             if (serversArray != null) {
97                 for ( int i = 0; i < serversArray.length; i++ ) {
98                     dnsServers.add(serversArray[ i ]);
99                     getLogger().info("Adding autodiscovered server " + serversArray[i]);
100                 }
101             }
102         }
103
104         // Get the DNS servers that this service will use for lookups
105
final Configuration serversConfiguration = configuration.getChild( "servers" );
106         final Configuration[] serverConfigurations =
107             serversConfiguration.getChildren( "server" );
108
109         for ( int i = 0; i < serverConfigurations.length; i++ ) {
110             dnsServers.add( serverConfigurations[ i ].getValue() );
111         }
112
113         if (dnsServers.isEmpty()) {
114             getLogger().info("No DNS servers have been specified or found by autodiscovery - adding 127.0.0.1");
115             dnsServers.add("127.0.0.1");
116         }
117
118         final boolean authoritative =
119             configuration.getChild( "authoritative" ).getValueAsBoolean( false );
120         // TODO: Check to see if the credibility field is being used correctly. From the
121
// docs I don't think so
122
dnsCredibility = authoritative ? Credibility.AUTH_ANSWER : Credibility.NONAUTH_ANSWER;
123     }
124
125     /**
126      * @see org.apache.avalon.framework.activity.Initializable#initialize()
127      */

128     public void initialize()
129         throws Exception JavaDoc {
130
131         getLogger().debug("DNSServer init...");
132
133         // If no DNS servers were configured, default to local host
134
if (dnsServers.isEmpty()) {
135             try {
136                 dnsServers.add( InetAddress.getLocalHost().getHostName() );
137             } catch ( UnknownHostException JavaDoc ue ) {
138                 dnsServers.add( "127.0.0.1" );
139             }
140         }
141
142         //Create the extended resolver...
143
final String JavaDoc[] serversArray = (String JavaDoc[])dnsServers.toArray(new String JavaDoc[0]);
144
145         if (getLogger().isInfoEnabled()) {
146             for(int c = 0; c < serversArray.length; c++) {
147                 getLogger().info("DNS Server is: " + serversArray[c]);
148             }
149         }
150
151         try {
152             resolver = new ExtendedResolver( serversArray );
153             Lookup.setDefaultResolver(resolver);
154         } catch (UnknownHostException JavaDoc uhe) {
155             getLogger().fatalError("DNS service could not be initialized. The DNS servers specified are not recognized hosts.", uhe);
156             throw uhe;
157         }
158
159         cache = new Cache (DClass.IN);
160
161         getLogger().debug("DNSServer ...init end");
162     }
163
164     /**
165      * <p>Return the list of DNS servers in use by this service</p>
166      *
167      * @return an array of DNS server names
168      */

169     public String JavaDoc[] getDNSServers() {
170         return (String JavaDoc[])dnsServers.toArray(new String JavaDoc[0]);
171     }
172     
173     /**
174      * <p>Return a prioritized unmodifiable list of MX records
175      * obtained from the server.</p>
176      *
177      * @param hostname domain name to look up
178      *
179      * @return a unmodifiable list of MX records corresponding to
180      * this mail domain name
181      */

182     public Collection findMXRecords(String JavaDoc hostname) {
183         Record answers[] = lookup(hostname, Type.MX);
184         List servers = new ArrayList();
185         try {
186             if (answers == null) {
187                 return servers;
188             }
189
190             MXRecord mxAnswers[] = new MXRecord[answers.length];
191             for (int i = 0; i < answers.length; i++) {
192                 mxAnswers[i] = (MXRecord)answers[i];
193             }
194
195             Arrays.sort(mxAnswers, mxComparator);
196
197             for (int i = 0; i < mxAnswers.length; i++) {
198                 servers.add(mxAnswers[i].getTarget ().toString ());
199                 getLogger().debug(new StringBuffer JavaDoc("Found MX record ").append(mxAnswers[i].getTarget ().toString ()).toString());
200             }
201             return Collections.unmodifiableCollection(servers);
202         } finally {
203             //If we found no results, we'll add the original domain name if
204
//it's a valid DNS entry
205
if (servers.size () == 0) {
206                 StringBuffer JavaDoc logBuffer =
207                     new StringBuffer JavaDoc(128)
208                             .append("Couldn't resolve MX records for domain ")
209                             .append(hostname)
210                             .append(".");
211                 getLogger().info(logBuffer.toString());
212                 try {
213                     getByName(hostname);
214                     servers.add(hostname);
215                 } catch (UnknownHostException JavaDoc uhe) {
216                     // The original domain name is not a valid host,
217
// so we can't add it to the server list. In this
218
// case we return an empty list of servers
219
logBuffer = new StringBuffer JavaDoc(128)
220                                 .append("Couldn't resolve IP address for host ")
221                                 .append(hostname)
222                                 .append(".");
223                     getLogger().error(logBuffer.toString());
224                 }
225             }
226         }
227     }
228
229     /**
230      * Looks up DNS records of the specified type for the specified name.
231      *
232      * This method is a public wrapper for the private implementation
233      * method
234      *
235      * @param name the name of the host to be looked up
236      * @param type the type of record desired
237      */

238     public Record[] lookup(String JavaDoc name, int type) {
239         return rawDNSLookup(name,false,type);
240     }
241
242     /**
243      * Looks up DNS records of the specified type for the specified name
244      *
245      * @param namestr the name of the host to be looked up
246      * @param querysent whether the query has already been sent to the DNS servers
247      * @param type the type of record desired
248      */

249     private Record[] rawDNSLookup(String JavaDoc namestr, boolean querysent, int type) {
250         Name name = null;
251         try {
252             name = Name.fromString(namestr, Name.root);
253         } catch (TextParseException tpe) {
254             // TODO: Figure out how to handle this correctly.
255
getLogger().error("Couldn't parse name " + namestr, tpe);
256             return null;
257         }
258         int dclass = DClass.IN;
259
260         SetResponse cached = cache.lookupRecords(name, type, dnsCredibility);
261         if (cached.isSuccessful()) {
262             getLogger().debug(new StringBuffer JavaDoc(256)
263                              .append("Retrieving MX record for ")
264                              .append(name).append(" from cache")
265                              .toString());
266
267             return processSetResponse(cached);
268         }
269         else if (cached.isNXDOMAIN() || cached.isNXRRSET()) {
270             return null;
271         }
272         else if (querysent) {
273             return null;
274         }
275         else {
276             getLogger().debug(new StringBuffer JavaDoc(256)
277                              .append("Looking up MX record for ")
278                              .append(name)
279                              .toString());
280             Record question = Record.newRecord(name, type, dclass);
281             Message query = Message.newQuery(question);
282             Message response = null;
283
284             try {
285                 response = resolver.send(query);
286             }
287             catch (Exception JavaDoc ex) {
288                 getLogger().warn("Query error!", ex);
289                 return null;
290             }
291
292             int rcode = response.getHeader().getRcode();
293             if (rcode == Rcode.NOERROR || rcode == Rcode.NXDOMAIN) {
294                 cached = cache.addMessage(response);
295                 if (cached != null && cached.isSuccessful()) {
296                     return processSetResponse(cached);
297                 }
298             }
299
300             if (rcode != Rcode.NOERROR) {
301                 return null;
302             }
303
304             return rawDNSLookup(namestr, true, type);
305         }
306     }
307     
308     private Record[] processSetResponse(SetResponse sr) {
309         Record [] answers;
310         int answerCount = 0, n = 0;
311
312         RRset [] rrsets = sr.answers();
313         answerCount = 0;
314         for (int i = 0; i < rrsets.length; i++) {
315             answerCount += rrsets[i].size();
316         }
317
318         answers = new Record[answerCount];
319
320         for (int i = 0; i < rrsets.length; i++) {
321             Iterator iter = rrsets[i].rrs();
322             while (iter.hasNext()) {
323                 Record r = (Record)iter.next();
324                 answers[n++] = r;
325             }
326         }
327         return answers;
328     }
329
330     /* RFC 2821 section 5 requires that we sort the MX records by their
331      * preference, and introduce a randomization. This Comparator does
332      * comparisons as normal unless the values are equal, in which case
333      * it "tosses a coin", randomly speaking.
334      *
335      * This way MX record w/preference 0 appears before MX record
336      * w/preference 1, but a bunch of MX records with the same preference
337      * would appear in different orders each time.
338      *
339      * Reminder for maintainers: the return value on a Comparator can
340      * be counter-intuitive for those who aren't used to the old C
341      * strcmp function:
342      *
343      * < 0 ==> a < b
344      * = 0 ==> a = b
345      * > 0 ==> a > b
346      */

347     private static class MXRecordComparator implements Comparator {
348         private final static Random random = new Random();
349         public int compare (Object JavaDoc a, Object JavaDoc b) {
350             int pa = ((MXRecord)a).getPriority();
351             int pb = ((MXRecord)b).getPriority();
352             return (pa == pb) ? (512 - random.nextInt(1024)) : pa - pb;
353         }
354     }
355
356     /*
357      * Returns an Iterator over org.apache.mailet.HostAddress, a
358      * specialized subclass of javax.mail.URLName, which provides
359      * location information for servers that are specified as mail
360      * handlers for the given hostname. This is done using MX records,
361      * and the HostAddress instances are returned sorted by MX priority.
362      * If no host is found for domainName, the Iterator returned will be
363      * empty and the first call to hasNext() will return false. The
364      * Iterator is a nested iterator: the outer iteration is over the
365      * results of the MX record lookup, and the inner iteration is over
366      * potentially multiple A records for each MX record. DNS lookups
367      * are deferred until actually needed.
368      *
369      * @since v2.2.0a16-unstable
370      * @param domainName - the domain for which to find mail servers
371      * @return an Iterator over HostAddress instances, sorted by priority
372      */

373     public Iterator getSMTPHostAddresses(final String JavaDoc domainName) {
374         return new Iterator() {
375             private Iterator mxHosts = findMXRecords(domainName).iterator();
376             private Iterator addresses = null;
377
378             public boolean hasNext() {
379                 /* Make sure that when next() is called, that we can
380                  * provide a HostAddress. This means that we need to
381                  * have an inner iterator, and verify that it has
382                  * addresses. We could, for example, run into a
383                  * situation where the next mxHost didn't have any valid
384                  * addresses.
385                  */

386                 if ((addresses == null || !addresses.hasNext()) && mxHosts.hasNext()) do {
387                     final String JavaDoc nextHostname = (String JavaDoc)mxHosts.next();
388                     InetAddress JavaDoc[] addrs = null;
389                     try {
390                         addrs = getAllByName(nextHostname);
391                     } catch (UnknownHostException JavaDoc uhe) {
392                         // this should never happen, since we just got
393
// this host from mxHosts, which should have
394
// already done this check.
395
StringBuffer JavaDoc logBuffer = new StringBuffer JavaDoc(128)
396                                                  .append("Couldn't resolve IP address for discovered host ")
397                                                  .append(nextHostname)
398                                                  .append(".");
399                         getLogger().error(logBuffer.toString());
400                     }
401                     final InetAddress JavaDoc[] ipAddresses = addrs;
402
403                     addresses = new Iterator() {
404                         int i = 0;
405
406                         public boolean hasNext() {
407                             return ipAddresses != null && i < ipAddresses.length;
408                         }
409
410                         public Object JavaDoc next() {
411                             return new org.apache.mailet.HostAddress(nextHostname, "smtp://" + ipAddresses[i++].getHostAddress());
412                         }
413
414                         public void remove() {
415                             throw new UnsupportedOperationException JavaDoc ("remove not supported by this iterator");
416                         }
417                     };
418                 } while (!addresses.hasNext() && mxHosts.hasNext());
419
420                 return addresses != null && addresses.hasNext();
421             }
422
423             public Object JavaDoc next() {
424                 return addresses != null ? addresses.next() : null;
425             }
426
427             public void remove() {
428                 throw new UnsupportedOperationException JavaDoc ("remove not supported by this iterator");
429             }
430         };
431     }
432
433     /* java.net.InetAddress.get[All]ByName(String) allows an IP literal
434      * to be passed, and will recognize it even with a trailing '.'.
435      * However, org.xbill.DNS.Address does not recognize an IP literal
436      * with a trailing '.' character. The problem is that when we
437      * lookup an MX record for some domains, we may find an IP address,
438      * which will have had the trailing '.' appended by the time we get
439      * it back from dnsjava. An MX record is not allowed to have an IP
440      * address as the right-hand-side, but there are still plenty of
441      * such records on the Internet. Since java.net.InetAddress can
442      * handle them, for the time being we've decided to support them.
443      *
444      * These methods are NOT intended for use outside of James, and are
445      * NOT declared by the org.apache.james.services.DNSServer. This is
446      * currently a stopgap measure to be revisited for the next release.
447      */

448
449     private static String JavaDoc allowIPLiteral(String JavaDoc host) {
450         if ((host.charAt(host.length() - 1) == '.')) {
451             String JavaDoc possible_ip_literal = host.substring(0, host.length() - 1);
452             if (org.xbill.DNS.Address.isDottedQuad(possible_ip_literal)) {
453                 host = possible_ip_literal;
454             }
455         }
456         return host;
457     }
458
459     /**
460      * @see java.net.InetAddress#getByName(String)
461      */

462     public static InetAddress JavaDoc getByName(String JavaDoc host) throws UnknownHostException JavaDoc {
463         return org.xbill.DNS.Address.getByName(allowIPLiteral(host));
464     }
465
466     /**
467      * @see java.net.InetAddress#getByAllName(String)
468      */

469     public static InetAddress JavaDoc[] getAllByName(String JavaDoc host) throws UnknownHostException JavaDoc {
470         return org.xbill.DNS.Address.getAllByName(allowIPLiteral(host));
471     }
472
473     /**
474      * A way to get mail hosts to try. If any MX hosts are found for the
475      * domain name with which this is constructed, then these MX hostnames
476      * are returned in priority sorted order, lowest priority numbers coming
477      * first. And, whenever multiple hosts have the same priority then these
478      * are returned in a randomized order within that priority group, as
479      * specified in RFC 2821, Section 5.
480      *
481      * If no MX hosts are found for the domain name, then a DNS search is
482      * performed for an A record. If an A record is found then domainName itself
483      * will be returned by the Iterator, and it will be the only object in
484      * the Iterator. If however no A record is found (in addition to no MX
485      * record) then the Iterator constructed will be empty; the first call to
486      * its hasNext() will return false.
487      *
488      * This behavior attempts to satisfy the requirements of RFC 2821, Section 5.
489      * @since v2.2.0a16-unstable
490      */

491
492     /**** THIS CODE IS BROKEN AND UNUSED ****/
493
494     /* this code was used in getSMTPHostAddresses as:
495        private Iterator mxHosts = new MxSorter(domainName);
496
497        This class effectively replaces findMXRecords. If
498        it is to be kept, it should replace the body of that
499        method. The fixes would be to either implement a
500        more robust DNS lookup, or to replace the Type.A
501        lookup with InetAddress.getByName(), which is what
502        findMXRecords uses. */

503
504     private class MxSorter implements Iterator {
505         private int priorListPriority = Integer.MIN_VALUE;
506         private ArrayList equiPriorityList = new ArrayList();
507         private Record[] mxRecords;
508         private Random rnd = new Random ();
509
510         /* The implementation of this class attempts to achieve efficiency by
511          * performing no more sorting of the rawMxRecords than necessary. In the
512          * large majority of cases the first attempt, made by a client of this class
513          * to connect to an SMTP server for a given domain, will succeed. As such,
514          * in most cases only one call will be made to this Iterator's
515          * next(), and in that majority of cases there will have been no need
516          * to sort the array of MX Records. This implementation would, however, be
517          * relatively inefficient in the case where all hosts fail, when every
518          * Object is called out of a long Iterator.
519          */

520
521         private MxSorter(String JavaDoc domainName) {
522             mxRecords = lookup(domainName, Type.MX);
523             if (mxRecords == null || mxRecords.length == 0) {
524                 //no MX records were found, so try to use the domainName
525
Record[] aRecords = lookup(domainName, Type.A);
526                 if(aRecords != null && aRecords.length > 0) {
527                     equiPriorityList.add(domainName);
528                 }
529             }
530         }
531
532         /**
533          * Sets presentPriorityList to contain all hosts
534          * which have the least priority greater than pastPriority.
535          * When this is called, both (rawMxRecords.length > 0) and
536          * (presentPriorityList.size() == 0), by contract.
537          * In the case where this is called repeatedly, so that priorListPriority
538          * has already become the highest of the priorities in the rawMxRecords,
539          * then this returns without having added any elements to
540          * presentPriorityList; presentPriorityList.size remains zero.
541          */

542         private void createPriorityList(){
543             int leastPriorityFound = Integer.MAX_VALUE;
544             /* We loop once through the rawMxRecords, finding the lowest priority
545              * greater than priorListPriority, and collecting all the hostnames
546              * with that priority into equiPriorityList.
547              */

548             for (int i = 0; i < mxRecords.length; i++) {
549                 MXRecord thisRecord = (MXRecord)mxRecords[i];
550                 int thisRecordPriority = thisRecord.getPriority();
551                 if (thisRecordPriority > priorListPriority) {
552                     if (thisRecordPriority < leastPriorityFound) {
553                         equiPriorityList.clear();
554                         leastPriorityFound = thisRecordPriority;
555                         equiPriorityList.add(thisRecord.getTarget().toString());
556                     } else if (thisRecordPriority == leastPriorityFound) {
557                         equiPriorityList.add(thisRecord.getTarget().toString());
558                     }
559                 }
560             }
561             priorListPriority = leastPriorityFound;
562         }
563
564         public boolean hasNext(){
565             if (equiPriorityList.size() > 0){
566                 return true;
567             }else if (mxRecords != null && mxRecords.length > 0){
568                 createPriorityList();
569                 return equiPriorityList.size() > 0;
570             } else{
571                 return false;
572             }
573         }
574
575         public Object JavaDoc next(){
576             if (hasNext()){
577                 /* this randomization is done to comply with RFC-2821 */
578                 /* Note: java.util.Random.nextInt(limit) is about twice as fast as (int)(Math.random()*limit) */
579                 int getIndex = rnd.nextInt(equiPriorityList.size());
580                 Object JavaDoc returnElement = equiPriorityList.get(getIndex);
581                 equiPriorityList.remove(getIndex);
582                 return returnElement;
583             }else{
584                 throw new NoSuchElementException();
585             }
586         }
587
588         public void remove () {
589             throw new UnsupportedOperationException JavaDoc ("remove not supported by this iterator");
590         }
591     }
592
593     
594     /**
595      * The dispose operation is called at the end of a components lifecycle.
596      * Instances of this class use this method to release and destroy any
597      * resources that they own.
598      *
599      * This implementation shuts down org.xbill.DNS.Cache
600      *
601      * @throws Exception if an error is encountered during shutdown
602      */

603     public void dispose()
604     {
605         //setting the clean interval to a negative value, will terminate
606
//the Cache cleaner thread.
607
cache.setCleanInterval (-1);
608     }
609 }
610
Popular Tags