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  
21  
22  package org.apache.james.dnsserver;
23  
24  import org.apache.avalon.framework.activity.Initializable;
25  import org.apache.avalon.framework.configuration.Configurable;
26  import org.apache.avalon.framework.configuration.Configuration;
27  import org.apache.avalon.framework.configuration.ConfigurationException;
28  import org.apache.avalon.framework.logger.AbstractLogEnabled;
29  import org.apache.james.api.dnsservice.TemporaryResolutionException;
30  import org.xbill.DNS.ARecord;
31  import org.xbill.DNS.Cache;
32  import org.xbill.DNS.Credibility;
33  import org.xbill.DNS.DClass;
34  import org.xbill.DNS.ExtendedResolver;
35  import org.xbill.DNS.Lookup;
36  import org.xbill.DNS.MXRecord;
37  import org.xbill.DNS.Name;
38  import org.xbill.DNS.PTRRecord;
39  import org.xbill.DNS.Record;
40  import org.xbill.DNS.Resolver;
41  import org.xbill.DNS.ResolverConfig;
42  import org.xbill.DNS.ReverseMap;
43  import org.xbill.DNS.TXTRecord;
44  import org.xbill.DNS.TextParseException;
45  import org.xbill.DNS.Type;
46  
47  import java.net.InetAddress;
48  import java.net.UnknownHostException;
49  import java.util.ArrayList;
50  import java.util.Arrays;
51  import java.util.Collection;
52  import java.util.Collections;
53  import java.util.Comparator;
54  import java.util.Iterator;
55  import java.util.List;
56  import java.util.Random;
57  
58  /**
59   * Provides DNS client functionality to services running
60   * inside James
61   */
62  public class DNSServer
63      extends AbstractLogEnabled
64      implements Configurable, Initializable, org.apache.james.api.dnsservice.DNSService, 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      protected 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 search paths to be used
96       */
97      private Name[] searchPaths = null;
98  
99      /**
100      * The MX Comparator used in the MX sort.
101      */
102     private Comparator mxComparator = new MXRecordComparator();
103 
104     /**
105      * If true than the DNS server will return only a single IP per each MX record
106      * when looking up SMTPServers
107      */
108     private boolean singleIPPerMX;
109     
110     /**
111      * If true register this service as the default resolver/cache for DNSJava static
112      * calls
113      */
114     private boolean setAsDNSJavaDefault;
115     
116     private String localHostName;
117     
118     private String localCanonicalHostName;
119     
120     private String localAddress;
121     
122 
123     /**
124      * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
125      */
126     public void configure( final Configuration configuration )
127         throws ConfigurationException {
128 
129         final boolean autodiscover =
130             configuration.getChild( "autodiscover" ).getValueAsBoolean( true );
131 
132         List sPaths = new ArrayList();
133         if (autodiscover) {
134             getLogger().info("Autodiscovery is enabled - trying to discover your system's DNS Servers");
135             String[] serversArray = ResolverConfig.getCurrentConfig().servers();
136             if (serversArray != null) {
137                 for ( int i = 0; i < serversArray.length; i++ ) {
138                     dnsServers.add(serversArray[ i ]);
139                     getLogger().info("Adding autodiscovered server " + serversArray[i]);
140                 }
141             }
142             Name[] systemSearchPath = ResolverConfig.getCurrentConfig().searchPath();
143             if (systemSearchPath != null && systemSearchPath.length > 0) {
144                 sPaths.addAll(Arrays.asList(systemSearchPath));
145             }
146             if (getLogger().isInfoEnabled()) {
147                 for (Iterator i = sPaths.iterator(); i.hasNext();) {
148                     Name searchPath = (Name) i.next();
149                     getLogger().info("Adding autodiscovered search path " + searchPath.toString());
150                 }
151             }
152         }
153 
154         singleIPPerMX = configuration.getChild( "singleIPperMX" ).getValueAsBoolean( false ); 
155 
156         setAsDNSJavaDefault = configuration.getChild( "setAsDNSJavaDefault" ).getValueAsBoolean( true );
157         
158         // Get the DNS servers that this service will use for lookups
159         final Configuration serversConfiguration = configuration.getChild( "servers" );
160         final Configuration[] serverConfigurations =
161             serversConfiguration.getChildren( "server" );
162 
163         for ( int i = 0; i < serverConfigurations.length; i++ ) {
164             dnsServers.add( serverConfigurations[ i ].getValue() );
165         }
166 
167         // Get the DNS servers that this service will use for lookups
168         final Configuration searchPathsConfiguration = configuration.getChild( "searchpaths" );
169         final Configuration[] searchPathsConfigurations =
170             searchPathsConfiguration.getChildren( "searchpath" );
171 
172         for ( int i = 0; i < searchPathsConfigurations.length; i++ ) {
173             try {
174                 sPaths.add( Name.fromString(searchPathsConfigurations[ i ].getValue()) );
175             } catch (TextParseException e) {
176                 throw new ConfigurationException("Unable to parse searchpath host: "+searchPathsConfigurations[ i ].getValue(),e);
177             }
178         }
179         
180         searchPaths = (Name[]) sPaths.toArray(new Name[0]);
181 
182         if (dnsServers.isEmpty()) {
183             getLogger().info("No DNS servers have been specified or found by autodiscovery - adding 127.0.0.1");
184             dnsServers.add("127.0.0.1");
185         }
186 
187         final boolean authoritative =
188            configuration.getChild( "authoritative" ).getValueAsBoolean( false );
189         // TODO: Check to see if the credibility field is being used correctly.  From the
190         //      docs I don't think so
191         dnsCredibility = authoritative ? Credibility.AUTH_ANSWER : Credibility.NONAUTH_ANSWER;
192 
193         maxCacheSize = (int) configuration.getChild( "maxcachesize" ).getValueAsLong( maxCacheSize );
194     }
195 
196     /**
197      * @see org.apache.avalon.framework.activity.Initializable#initialize()
198      */
199     public void initialize()
200         throws Exception {
201 
202         getLogger().debug("DNSService init...");
203 
204         // If no DNS servers were configured, default to local host
205         if (dnsServers.isEmpty()) {
206             try {
207                 dnsServers.add( InetAddress.getLocalHost().getHostName() );
208             } catch ( UnknownHostException ue ) {
209                 dnsServers.add( "127.0.0.1" );
210             }
211         }
212 
213         //Create the extended resolver...
214         final String[] serversArray = (String[])dnsServers.toArray(new String[0]);
215 
216         if (getLogger().isInfoEnabled()) {
217             for(int c = 0; c < serversArray.length; c++) {
218                 getLogger().info("DNS Server is: " + serversArray[c]);
219             }
220         }
221 
222         try {
223             resolver = new ExtendedResolver( serversArray );
224         } catch (UnknownHostException uhe) {
225             getLogger().fatalError("DNS service could not be initialized.  The DNS servers specified are not recognized hosts.", uhe);
226             throw uhe;
227         }
228 
229         cache = new Cache (DClass.IN);
230         cache.setMaxEntries(maxCacheSize);
231         
232         if (setAsDNSJavaDefault) {
233             Lookup.setDefaultResolver(resolver);
234             Lookup.setDefaultCache(cache, DClass.IN);
235             Lookup.setDefaultSearchPath(searchPaths);
236             getLogger().info("Registered cache, resolver and search paths as DNSJava defaults");
237         }
238         
239         // Cache the local hostname and local address. This is needed because 
240         // the following issues:
241         // JAMES-787
242         // JAMES-302
243         InetAddress addr = getLocalHost();
244         localCanonicalHostName = addr.getCanonicalHostName();
245         localHostName = addr.getHostName();
246         localAddress = addr.getHostAddress();
247         
248         getLogger().debug("DNSService ...init end");
249     }
250 
251     /**
252      * <p>Return the list of DNS servers in use by this service</p>
253      *
254      * @return an array of DNS server names
255      */
256     public String[] getDNSServers() {
257         return (String[])dnsServers.toArray(new String[0]);
258     }
259 
260     /**
261      * <p>Return the list of DNS servers in use by this service</p>
262      *
263      * @return an array of DNS server names
264      */
265     public Name[] getSearchPaths() {
266         return searchPaths;
267     }
268 
269     
270     /**
271      * <p>Return a prioritized unmodifiable list of MX records
272      * obtained from the server.</p>
273      *
274      * @param hostname domain name to look up
275      *
276      * @return a list of MX records corresponding to this mail domain
277      * @throws TemporaryResolutionException get thrown on temporary problems
278      */
279     private List findMXRecordsRaw(String hostname) throws TemporaryResolutionException {
280         Record answers[] = lookup(hostname, Type.MX, "MX");
281         List servers = new ArrayList();
282         if (answers == null) {
283             return servers;
284         }
285 
286         MXRecord mxAnswers[] = new MXRecord[answers.length];
287         for (int i = 0; i < answers.length; i++) {
288             mxAnswers[i] = (MXRecord)answers[i];
289         }
290 
291         Arrays.sort(mxAnswers, mxComparator);
292 
293         for (int i = 0; i < mxAnswers.length; i++) {
294             servers.add(mxAnswers[i].getTarget ().toString ());
295             getLogger().debug(new StringBuffer("Found MX record ").append(mxAnswers[i].getTarget ().toString ()).toString());
296         }
297         return servers;
298     }
299     
300     /**
301      * @see org.apache.james.api.dnsservice.DNSService#findMXRecords(String)
302      */
303     public Collection findMXRecords(String hostname) throws TemporaryResolutionException {
304         List servers = new ArrayList();
305         try {
306             servers = findMXRecordsRaw(hostname);
307             return Collections.unmodifiableCollection(servers);
308         } finally {
309             //If we found no results, we'll add the original domain name if
310             //it's a valid DNS entry
311             if (servers.size () == 0) {
312                 StringBuffer logBuffer =
313                     new StringBuffer(128)
314                             .append("Couldn't resolve MX records for domain ")
315                             .append(hostname)
316                             .append(".");
317                 getLogger().info(logBuffer.toString());
318                 try {
319                     getByName(hostname);
320                     servers.add(hostname);
321                 } catch (UnknownHostException uhe) {
322                     // The original domain name is not a valid host,
323                     // so we can't add it to the server list.  In this
324                     // case we return an empty list of servers
325                     logBuffer = new StringBuffer(128)
326                               .append("Couldn't resolve IP address for host ")
327                               .append(hostname)
328                               .append(".");
329                     getLogger().error(logBuffer.toString());
330                 }
331             }
332         }
333     }
334 
335     /**
336      * Looks up DNS records of the specified type for the specified name.
337      *
338      * This method is a public wrapper for the private implementation
339      * method
340      *
341      * @param namestr the name of the host to be looked up
342      * @param type the type of record desired
343      * @param typeDesc the description of the record type, for debugging purpose
344      */
345     protected Record[] lookup(String namestr, int type, String typeDesc) throws TemporaryResolutionException {
346         // Name name = null;
347         try {
348             // name = Name.fromString(namestr, Name.root);
349             Lookup l = new Lookup(namestr, type);
350             
351             l.setCache(cache);
352             l.setResolver(resolver);
353             l.setCredibility(dnsCredibility);
354             l.setSearchPath(searchPaths);
355             Record[] r = l.run();
356             
357             try {
358                 if (l.getResult() == Lookup.TRY_AGAIN) {
359                     throw new TemporaryResolutionException(
360                             "DNSService is temporary not reachable");
361                 } else {
362                     return r;
363                 }
364             } catch (IllegalStateException ise) {
365                 // This is okay, because it mimics the original behaviour
366                 // TODO find out if it's a bug in DNSJava 
367                 getLogger().debug("Error determining result ", ise);
368                 throw new TemporaryResolutionException(
369                         "DNSService is temporary not reachable");
370             }
371             
372             // return rawDNSLookup(name, false, type, typeDesc);
373         } catch (TextParseException tpe) {
374             // TODO: Figure out how to handle this correctly.
375             getLogger().error("Couldn't parse name " + namestr, tpe);
376             return null;
377         }
378     }
379     
380     protected Record[] lookupNoException(String namestr, int type, String typeDesc) {
381         try {
382             return lookup(namestr, type, typeDesc);
383         } catch (TemporaryResolutionException e) {
384             return null;
385         }
386     }
387     
388     /* RFC 2821 section 5 requires that we sort the MX records by their
389      * preference, and introduce a randomization.  This Comparator does
390      * comparisons as normal unless the values are equal, in which case
391      * it "tosses a coin", randomly speaking.
392      *
393      * This way MX record w/preference 0 appears before MX record
394      * w/preference 1, but a bunch of MX records with the same preference
395      * would appear in different orders each time.
396      *
397      * Reminder for maintainers: the return value on a Comparator can
398      * be counter-intuitive for those who aren't used to the old C
399      * strcmp function:
400      *
401      * < 0 ==> a < b
402      * = 0 ==> a = b
403      * > 0 ==> a > b
404      */
405     private static class MXRecordComparator implements Comparator {
406         private final static Random random = new Random();
407         public int compare (Object a, Object b) {
408             int pa = ((MXRecord)a).getPriority();
409             int pb = ((MXRecord)b).getPriority();
410             return (pa == pb) ? (512 - random.nextInt(1024)) : pa - pb;
411         }
412     }
413 
414     /**
415      * @see org.apache.james.api.dnsservice.DNSService#getSMTPHostAddresses(String)
416      */
417     public Iterator getSMTPHostAddresses(final String domainName) throws TemporaryResolutionException {
418         return new Iterator() {
419             private Iterator mxHosts = findMXRecords(domainName).iterator();
420             private Iterator addresses = null;
421 
422             public boolean hasNext() {
423                 /* Make sure that when next() is called, that we can
424                  * provide a HostAddress.  This means that we need to
425                  * have an inner iterator, and verify that it has
426                  * addresses.  We could, for example, run into a
427                  * situation where the next mxHost didn't have any valid
428                  * addresses.
429                  */
430                 if ((addresses == null || !addresses.hasNext()) && mxHosts.hasNext()) do {
431                     final String nextHostname = (String)mxHosts.next();
432                     InetAddress[] addrs = null;
433                     try {
434                         if (singleIPPerMX) {
435                             addrs = new InetAddress[] {getByName(nextHostname)};
436                         } else {
437                             addrs = getAllByName(nextHostname);
438                         }
439                     } catch (UnknownHostException uhe) {
440                         // this should never happen, since we just got
441                         // this host from mxHosts, which should have
442                         // already done this check.
443                         StringBuffer logBuffer = new StringBuffer(128)
444                                                  .append("Couldn't resolve IP address for discovered host ")
445                                                  .append(nextHostname)
446                                                  .append(".");
447                         getLogger().error(logBuffer.toString());
448                     }
449                     final InetAddress[] ipAddresses = addrs;
450 
451                     addresses = new Iterator() {
452                         int i = 0;
453 
454                         public boolean hasNext() {
455                             return ipAddresses != null && i < ipAddresses.length;
456                         }
457 
458                         public Object next() {
459                             return new org.apache.mailet.HostAddress(nextHostname, "smtp://" + ipAddresses[i++].getHostAddress());
460                         }
461 
462                         public void remove() {
463                             throw new UnsupportedOperationException ("remove not supported by this iterator");
464                         }
465                     };
466                 } while (!addresses.hasNext() && mxHosts.hasNext());
467 
468                 return addresses != null && addresses.hasNext();
469             }
470 
471             public Object next() {
472                 return addresses != null ? addresses.next() : null;
473             }
474 
475             public void remove() {
476                 throw new UnsupportedOperationException ("remove not supported by this iterator");
477             }
478         };
479     }
480 
481     /* java.net.InetAddress.get[All]ByName(String) allows an IP literal
482      * to be passed, and will recognize it even with a trailing '.'.
483      * However, org.xbill.DNS.Address does not recognize an IP literal
484      * with a trailing '.' character.  The problem is that when we
485      * lookup an MX record for some domains, we may find an IP address,
486      * which will have had the trailing '.' appended by the time we get
487      * it back from dnsjava.  An MX record is not allowed to have an IP
488      * address as the right-hand-side, but there are still plenty of
489      * such records on the Internet.  Since java.net.InetAddress can
490      * handle them, for the time being we've decided to support them.
491      *
492      * These methods are NOT intended for use outside of James, and are
493      * NOT declared by the org.apache.james.services.DNSServer.  This is
494      * currently a stopgap measure to be revisited for the next release.
495      */
496 
497     private static String allowIPLiteral(String host) {
498         if ((host.charAt(host.length() - 1) == '.')) {
499             String possible_ip_literal = host.substring(0, host.length() - 1);
500             if (org.xbill.DNS.Address.isDottedQuad(possible_ip_literal)) {
501                 host = possible_ip_literal;
502             }
503         }
504         return host;
505     }
506 
507     /**
508      * @see org.apache.james.api.dnsservice.DNSService#getByName(String)
509      */
510     public InetAddress getByName(String host) throws UnknownHostException {
511         String name = allowIPLiteral(host);
512          
513         try {
514             // Check if its local
515             if (name.equalsIgnoreCase(localHostName) || name.equalsIgnoreCase(localCanonicalHostName) ||name.equals(localAddress)) {
516                 return getLocalHost();
517             }
518             
519             return org.xbill.DNS.Address.getByAddress(name);
520         } catch (UnknownHostException e) {
521             Record[] records = lookupNoException(name, Type.A, "A");
522 
523             if (records != null && records.length >= 1) {
524                 ARecord a = (ARecord) records[0];
525                 return InetAddress.getByAddress(name, a.getAddress().getAddress());
526             } else throw e;
527         }
528     }
529 
530     /**
531      * @see org.apache.james.api.dnsservice.DNSService#getAllByName(String)
532      */
533     public InetAddress[] getAllByName(String host) throws UnknownHostException {
534         String name = allowIPLiteral(host);
535         try {
536             // Check if its local
537             if (name.equalsIgnoreCase(localHostName) || name.equalsIgnoreCase(localCanonicalHostName) ||name.equals(localAddress)) {
538                 return new InetAddress[] {getLocalHost()};
539             }
540             
541             InetAddress addr = org.xbill.DNS.Address.getByAddress(name);
542             return new InetAddress[] {addr};
543         } catch (UnknownHostException e) {
544             Record[] records = lookupNoException(name, Type.A, "A");
545             
546             if (records != null && records.length >= 1) {
547                 InetAddress [] addrs = new InetAddress[records.length];
548                 for (int i = 0; i < records.length; i++) {
549                     ARecord a = (ARecord) records[i];
550                     addrs[i] = InetAddress.getByAddress(name, a.getAddress().getAddress());
551                 }
552                 return addrs;
553             } else throw e;
554         }
555     }
556     
557     /**
558      * @see org.apache.james.api.dnsservice.DNSService#findTXTRecords(String)
559      */
560     public Collection findTXTRecords(String hostname){
561         List txtR = new ArrayList();
562         Record[] records = lookupNoException(hostname, Type.TXT, "TXT");
563     
564         if (records != null) {
565            for (int i = 0; i < records.length; i++) {
566                TXTRecord txt = (TXTRecord) records[i];
567                txtR.add(txt.rdataToString());
568            }
569         
570         }
571         return txtR;
572     }
573 
574     /**
575      * @see org.apache.james.api.dnsservice.DNSService#getHostName(java.net.InetAddress)
576      */
577     public String getHostName(InetAddress addr){
578         String result = null;
579         Name name = ReverseMap.fromAddress(addr);
580         Record[] records = lookupNoException(name.toString(), Type.PTR, "PTR");
581 
582         if (records == null) {
583             result = addr.getHostAddress();
584         } else {
585             PTRRecord ptr = (PTRRecord) records[0];
586             result = ptr.getTarget().toString();
587         }
588         return result;
589     }
590 
591     /**
592      * @see org.apache.james.api.dnsservice.DNSService#getLocalHost()
593      */
594     public InetAddress getLocalHost() throws UnknownHostException {
595         return InetAddress.getLocalHost();
596     }
597 
598 }