View Javadoc

1   /************************************************************************
2    * Copyright (c) 2000-2006 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.CNAMERecord;
27  import org.xbill.DNS.Cache;
28  import org.xbill.DNS.Credibility;
29  import org.xbill.DNS.DClass;
30  import org.xbill.DNS.ExtendedResolver;
31  import org.xbill.DNS.Lookup;
32  import org.xbill.DNS.Message;
33  import org.xbill.DNS.MXRecord;
34  import org.xbill.DNS.Name;
35  import org.xbill.DNS.Rcode;
36  import org.xbill.DNS.Record;
37  import org.xbill.DNS.Resolver;
38  import org.xbill.DNS.RRset;
39  import org.xbill.DNS.ResolverConfig;
40  import org.xbill.DNS.SetResponse;
41  import org.xbill.DNS.TextParseException;
42  import org.xbill.DNS.Type;
43  
44  import java.net.InetAddress;
45  import java.net.UnknownHostException;
46  import java.util.ArrayList;
47  import java.util.Arrays;
48  import java.util.Collection;
49  import java.util.Collections;
50  import java.util.Comparator;
51  import java.util.Iterator;
52  import java.util.List;
53  import java.util.Random;
54  
55  /***
56   * Provides DNS client functionality to services running
57   * inside James
58   */
59  public class DNSServer
60      extends AbstractLogEnabled
61      implements Configurable, Initializable, Disposable,
62      org.apache.james.services.DNSServer, DNSServerMBean {
63  
64      /***
65       * A resolver instance used to retrieve DNS records.  This
66       * is a reference to a third party library object.
67       */
68      protected Resolver resolver;
69  
70      /***
71       * A TTL cache of results received from the DNS server.  This
72       * is a reference to a third party library object.
73       */
74      private Cache cache;
75  
76      /***
77       * Maximum number of RR to cache.
78       */
79  
80      private int maxCacheSize = 50000;
81  
82      /***
83       * Whether the DNS response is required to be authoritative
84       */
85      private int dnsCredibility;
86  
87      /***
88       * The DNS servers to be used by this service
89       */
90      private List dnsServers = new ArrayList();
91  
92      /***
93       * The MX Comparator used in the MX sort.
94       */
95      private Comparator mxComparator = new MXRecordComparator();
96  
97      /***
98       * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
99       */
100     public void configure( final Configuration configuration )
101         throws ConfigurationException {
102 
103         final boolean autodiscover =
104             configuration.getChild( "autodiscover" ).getValueAsBoolean( true );
105 
106         if (autodiscover) {
107             getLogger().info("Autodiscovery is enabled - trying to discover your system's DNS Servers");
108             String[] serversArray = ResolverConfig.getCurrentConfig().servers();
109             if (serversArray != null) {
110                 for ( int i = 0; i < serversArray.length; i++ ) {
111                     dnsServers.add(serversArray[ i ]);
112                     getLogger().info("Adding autodiscovered server " + serversArray[i]);
113                 }
114             }
115         }
116 
117         // Get the DNS servers that this service will use for lookups
118         final Configuration serversConfiguration = configuration.getChild( "servers" );
119         final Configuration[] serverConfigurations =
120             serversConfiguration.getChildren( "server" );
121 
122         for ( int i = 0; i < serverConfigurations.length; i++ ) {
123             dnsServers.add( serverConfigurations[ i ].getValue() );
124         }
125 
126         if (dnsServers.isEmpty()) {
127             getLogger().info("No DNS servers have been specified or found by autodiscovery - adding 127.0.0.1");
128             dnsServers.add("127.0.0.1");
129         }
130 
131         final boolean authoritative =
132             configuration.getChild( "authoritative" ).getValueAsBoolean( false );
133         // TODO: Check to see if the credibility field is being used correctly.  From the
134         //       docs I don't think so
135         dnsCredibility = authoritative ? Credibility.AUTH_ANSWER : Credibility.NONAUTH_ANSWER;
136 
137         maxCacheSize = (int) configuration.getChild( "maxcachesize" ).getValueAsLong( maxCacheSize );
138     }
139 
140     /***
141      * @see org.apache.avalon.framework.activity.Initializable#initialize()
142      */
143     public void initialize()
144         throws Exception {
145 
146         getLogger().debug("DNSServer init...");
147 
148         // If no DNS servers were configured, default to local host
149         if (dnsServers.isEmpty()) {
150             try {
151                 dnsServers.add( InetAddress.getLocalHost().getHostName() );
152             } catch ( UnknownHostException ue ) {
153                 dnsServers.add( "127.0.0.1" );
154             }
155         }
156 
157         //Create the extended resolver...
158         final String[] serversArray = (String[])dnsServers.toArray(new String[0]);
159 
160         if (getLogger().isInfoEnabled()) {
161             for(int c = 0; c < serversArray.length; c++) {
162                 getLogger().info("DNS Server is: " + serversArray[c]);
163             }
164         }
165 
166         try {
167             resolver = new ExtendedResolver( serversArray );
168             Lookup.setDefaultResolver(resolver);
169         } catch (UnknownHostException uhe) {
170             getLogger().fatalError("DNS service could not be initialized.  The DNS servers specified are not recognized hosts.", uhe);
171             throw uhe;
172         }
173 
174         cache = new Cache (DClass.IN);
175         cache.setMaxEntries(maxCacheSize);
176         Lookup.setDefaultCache(cache, DClass.IN);
177         
178         getLogger().debug("DNSServer ...init end");
179     }
180 
181     /***
182      * <p>Return the list of DNS servers in use by this service</p>
183      *
184      * @return an array of DNS server names
185      */
186     public String[] getDNSServers() {
187         return (String[])dnsServers.toArray(new String[0]);
188     }
189 
190     
191     /***
192      * <p>Return a prioritized unmodifiable list of MX records
193      * obtained from the server.</p>
194      *
195      * @param hostname domain name to look up
196      *
197      * @return a list of MX records corresponding to this mail domain
198      */
199     public List findMXRecordsRaw(String hostname) {
200         Record answers[] = lookup(hostname, Type.MX);
201         List servers = new ArrayList();
202         if (answers == null) {
203             return servers;
204         }
205 
206         MXRecord mxAnswers[] = new MXRecord[answers.length];
207         for (int i = 0; i < answers.length; i++) {
208             mxAnswers[i] = (MXRecord)answers[i];
209         }
210 
211         Arrays.sort(mxAnswers, mxComparator);
212 
213         for (int i = 0; i < mxAnswers.length; i++) {
214             servers.add(mxAnswers[i].getTarget ().toString ());
215             getLogger().debug(new StringBuffer("Found MX record ").append(mxAnswers[i].getTarget ().toString ()).toString());
216         }
217         return servers;
218     }
219     
220     /***
221      * <p>Return a prioritized unmodifiable list of host handling mail
222      * for the domain.</p>
223      * 
224      * <p>First lookup MX hosts, then MX hosts of the CNAME adress, and
225      * if no server is found return the IP of the hostname</p>
226      *
227      * @param hostname domain name to look up
228      *
229      * @return a unmodifiable list of handling servers corresponding to
230      *         this mail domain name
231      */
232     public Collection findMXRecords(String hostname) {
233         List servers = new ArrayList();
234         try {
235             servers = findMXRecordsRaw(hostname);
236             return Collections.unmodifiableCollection(servers);
237         } finally {
238             //If we found no results, we'll add the original domain name if
239             //it's a valid DNS entry
240             if (servers.size () == 0) {
241                 StringBuffer logBuffer =
242                     new StringBuffer(128)
243                             .append("Couldn't resolve MX records for domain ")
244                             .append(hostname)
245                             .append(".");
246                 getLogger().info(logBuffer.toString());
247                 Record cnames[] = lookup(hostname, Type.CNAME);
248                 Collection cnameMXrecords = null;
249                 if (cnames!=null && cnames.length > 0) {
250                     cnameMXrecords = findMXRecordsRaw(((CNAMERecord) cnames[0]).getTarget().toString());
251                 } else {
252                     logBuffer = new StringBuffer(128)
253                             .append("Couldn't find CNAME records for domain ")
254                             .append(hostname)
255                             .append(".");
256                     getLogger().info(logBuffer.toString());
257                 }
258                 if (cnameMXrecords==null) {
259                     try {
260                         getByName(hostname);
261                         servers.add(hostname);
262                     } catch (UnknownHostException uhe) {
263                         // The original domain name is not a valid host,
264                         // so we can't add it to the server list.  In this
265                         // case we return an empty list of servers
266                         logBuffer = new StringBuffer(128)
267                                   .append("Couldn't resolve IP address for host ")
268                                   .append(hostname)
269                                   .append(".");
270                         getLogger().error(logBuffer.toString());
271                     }
272                 } else {
273                     servers.addAll(cnameMXrecords);
274                 }
275             }
276         }
277     }
278 
279     /***
280      * Looks up DNS records of the specified type for the specified name.
281      *
282      * This method is a public wrapper for the private implementation
283      * method
284      *
285      * @param name the name of the host to be looked up
286      * @param type the type of record desired
287      */
288     public Record[] lookup(String name, int type) {
289         return rawDNSLookup(name,false,type);
290     }
291 
292     /***
293      * Looks up DNS records of the specified type for the specified name
294      *
295      * @param namestr the name of the host to be looked up
296      * @param querysent whether the query has already been sent to the DNS servers
297      * @param type the type of record desired
298      */
299     private Record[] rawDNSLookup(String namestr, boolean querysent, int type) {
300         Name name = null;
301         try {
302             name = Name.fromString(namestr, Name.root);
303         } catch (TextParseException tpe) {
304             // TODO: Figure out how to handle this correctly.
305             getLogger().error("Couldn't parse name " + namestr, tpe);
306             return null;
307         }
308         int dclass = DClass.IN;
309 
310         SetResponse cached = cache.lookupRecords(name, type, dnsCredibility);
311         if (cached.isSuccessful()) {
312             getLogger().debug(new StringBuffer(256)
313                              .append("Retrieving MX record for ")
314                              .append(name).append(" from cache")
315                              .toString());
316 
317             return processSetResponse(cached);
318         }
319         else if (cached.isNXDOMAIN() || cached.isNXRRSET()) {
320             return null;
321         }
322         else if (querysent) {
323             return null;
324         }
325         else {
326             getLogger().debug(new StringBuffer(256)
327                              .append("Looking up MX record for ")
328                              .append(name)
329                              .toString());
330             Record question = Record.newRecord(name, type, dclass);
331             Message query = Message.newQuery(question);
332             Message response = null;
333 
334             try {
335                 response = resolver.send(query);
336             }
337             catch (Exception ex) {
338                 getLogger().warn("Query error!", ex);
339                 return null;
340             }
341 
342             int rcode = response.getHeader().getRcode();
343             if (rcode == Rcode.NOERROR || rcode == Rcode.NXDOMAIN) {
344                 cached = cache.addMessage(response);
345                 if (cached != null && cached.isSuccessful()) {
346                     return processSetResponse(cached);
347                 }
348             }
349 
350             if (rcode != Rcode.NOERROR) {
351                 return null;
352             }
353 
354             return rawDNSLookup(namestr, true, type);
355         }
356     }
357     
358     protected Record[] processSetResponse(SetResponse sr) {
359         Record [] answers;
360         int answerCount = 0, n = 0;
361 
362         RRset [] rrsets = sr.answers();
363         answerCount = 0;
364         for (int i = 0; i < rrsets.length; i++) {
365             answerCount += rrsets[i].size();
366         }
367 
368         answers = new Record[answerCount];
369 
370         for (int i = 0; i < rrsets.length; i++) {
371             Iterator iter = rrsets[i].rrs();
372             while (iter.hasNext()) {
373                 Record r = (Record)iter.next();
374                 answers[n++] = r;
375             }
376         }
377         return answers;
378     }
379 
380     /* RFC 2821 section 5 requires that we sort the MX records by their
381      * preference, and introduce a randomization.  This Comparator does
382      * comparisons as normal unless the values are equal, in which case
383      * it "tosses a coin", randomly speaking.
384      *
385      * This way MX record w/preference 0 appears before MX record
386      * w/preference 1, but a bunch of MX records with the same preference
387      * would appear in different orders each time.
388      *
389      * Reminder for maintainers: the return value on a Comparator can
390      * be counter-intuitive for those who aren't used to the old C
391      * strcmp function:
392      *
393      * < 0 ==> a < b
394      * = 0 ==> a = b
395      * > 0 ==> a > b
396      */
397     private static class MXRecordComparator implements Comparator {
398         private final static Random random = new Random();
399         public int compare (Object a, Object b) {
400             int pa = ((MXRecord)a).getPriority();
401             int pb = ((MXRecord)b).getPriority();
402             return (pa == pb) ? (512 - random.nextInt(1024)) : pa - pb;
403         }
404     }
405 
406     /*
407      * Returns an Iterator over org.apache.mailet.HostAddress, a
408      * specialized subclass of javax.mail.URLName, which provides
409      * location information for servers that are specified as mail
410      * handlers for the given hostname.  This is done using MX records,
411      * and the HostAddress instances are returned sorted by MX priority.
412      * If no host is found for domainName, the Iterator returned will be
413      * empty and the first call to hasNext() will return false.  The
414      * Iterator is a nested iterator: the outer iteration is over the
415      * results of the MX record lookup, and the inner iteration is over
416      * potentially multiple A records for each MX record.  DNS lookups
417      * are deferred until actually needed.
418      *
419      * @since v2.2.0a16-unstable
420      * @param domainName - the domain for which to find mail servers
421      * @return an Iterator over HostAddress instances, sorted by priority
422      */
423     public Iterator getSMTPHostAddresses(final String domainName) {
424         return new Iterator() {
425             private Iterator mxHosts = findMXRecords(domainName).iterator();
426             private Iterator addresses = null;
427 
428             public boolean hasNext() {
429                 /* Make sure that when next() is called, that we can
430                  * provide a HostAddress.  This means that we need to
431                  * have an inner iterator, and verify that it has
432                  * addresses.  We could, for example, run into a
433                  * situation where the next mxHost didn't have any valid
434                  * addresses.
435                  */
436                 if ((addresses == null || !addresses.hasNext()) && mxHosts.hasNext()) do {
437                     final String nextHostname = (String)mxHosts.next();
438                     InetAddress[] addrs = null;
439                     try {
440                         addrs = getAllByName(nextHostname);
441                     } catch (UnknownHostException uhe) {
442                         // this should never happen, since we just got
443                         // this host from mxHosts, which should have
444                         // already done this check.
445                         StringBuffer logBuffer = new StringBuffer(128)
446                                                  .append("Couldn't resolve IP address for discovered host ")
447                                                  .append(nextHostname)
448                                                  .append(".");
449                         getLogger().error(logBuffer.toString());
450                     }
451                     final InetAddress[] ipAddresses = addrs;
452 
453                     addresses = new Iterator() {
454                         int i = 0;
455 
456                         public boolean hasNext() {
457                             return ipAddresses != null && i < ipAddresses.length;
458                         }
459 
460                         public Object next() {
461                             return new org.apache.mailet.HostAddress(nextHostname, "smtp://" + ipAddresses[i++].getHostAddress());
462                         }
463 
464                         public void remove() {
465                             throw new UnsupportedOperationException ("remove not supported by this iterator");
466                         }
467                     };
468                 } while (!addresses.hasNext() && mxHosts.hasNext());
469 
470                 return addresses != null && addresses.hasNext();
471             }
472 
473             public Object next() {
474                 return addresses != null ? addresses.next() : null;
475             }
476 
477             public void remove() {
478                 throw new UnsupportedOperationException ("remove not supported by this iterator");
479             }
480         };
481     }
482 
483     /* java.net.InetAddress.get[All]ByName(String) allows an IP literal
484      * to be passed, and will recognize it even with a trailing '.'.
485      * However, org.xbill.DNS.Address does not recognize an IP literal
486      * with a trailing '.' character.  The problem is that when we
487      * lookup an MX record for some domains, we may find an IP address,
488      * which will have had the trailing '.' appended by the time we get
489      * it back from dnsjava.  An MX record is not allowed to have an IP
490      * address as the right-hand-side, but there are still plenty of
491      * such records on the Internet.  Since java.net.InetAddress can
492      * handle them, for the time being we've decided to support them.
493      *
494      * These methods are NOT intended for use outside of James, and are
495      * NOT declared by the org.apache.james.services.DNSServer.  This is
496      * currently a stopgap measure to be revisited for the next release.
497      */
498 
499     private static String allowIPLiteral(String host) {
500         if ((host.charAt(host.length() - 1) == '.')) {
501             String possible_ip_literal = host.substring(0, host.length() - 1);
502             if (org.xbill.DNS.Address.isDottedQuad(possible_ip_literal)) {
503                 host = possible_ip_literal;
504             }
505         }
506         return host;
507     }
508 
509     /***
510      * @see java.net.InetAddress#getByName(String)
511      */
512     public static InetAddress getByName(String host) throws UnknownHostException {
513         return org.xbill.DNS.Address.getByName(allowIPLiteral(host));
514     }
515 
516     /***
517      * @see java.net.InetAddress#getByAllName(String)
518      */
519     public static InetAddress[] getAllByName(String host) throws UnknownHostException {
520         return org.xbill.DNS.Address.getAllByName(allowIPLiteral(host));
521     }
522 
523     /***
524      * The dispose operation is called at the end of a components lifecycle.
525      * Instances of this class use this method to release and destroy any
526      * resources that they own.
527      *
528      * This implementation no longer shuts down org.xbill.DNS.Cache
529      * because dnsjava 2.0.0 removed the need for a cleaner thread! 
530      *
531      * @throws Exception if an error is encountered during shutdown
532      */
533     public void dispose()
534     {
535     } 
536 }