View Javadoc

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