001    /****************************************************************
002     * Licensed to the Apache Software Foundation (ASF) under one   *
003     * or more contributor license agreements.  See the NOTICE file *
004     * distributed with this work for additional information        *
005     * regarding copyright ownership.  The ASF licenses this file   *
006     * to you under the Apache License, Version 2.0 (the            *
007     * "License"); you may not use this file except in compliance   *
008     * with the License.  You may obtain a copy of the License at   *
009     *                                                              *
010     *   http://www.apache.org/licenses/LICENSE-2.0                 *
011     *                                                              *
012     * Unless required by applicable law or agreed to in writing,   *
013     * software distributed under the License is distributed on an  *
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015     * KIND, either express or implied.  See the License for the    *
016     * specific language governing permissions and limitations      *
017     * under the License.                                           *
018     ****************************************************************/
019    
020    package com.hs.mail.dns;
021    
022    import java.io.IOException;
023    import java.net.InetAddress;
024    import java.net.UnknownHostException;
025    import java.util.ArrayList;
026    import java.util.Arrays;
027    import java.util.Collection;
028    import java.util.Collections;
029    import java.util.Comparator;
030    import java.util.Iterator;
031    import java.util.List;
032    import java.util.Random;
033    
034    import org.apache.log4j.Logger;
035    import org.springframework.beans.factory.InitializingBean;
036    import org.xbill.DNS.CNAMERecord;
037    import org.xbill.DNS.Cache;
038    import org.xbill.DNS.Credibility;
039    import org.xbill.DNS.DClass;
040    import org.xbill.DNS.ExtendedResolver;
041    import org.xbill.DNS.Lookup;
042    import org.xbill.DNS.MXRecord;
043    import org.xbill.DNS.Message;
044    import org.xbill.DNS.Name;
045    import org.xbill.DNS.RRset;
046    import org.xbill.DNS.Rcode;
047    import org.xbill.DNS.Record;
048    import org.xbill.DNS.Resolver;
049    import org.xbill.DNS.ResolverConfig;
050    import org.xbill.DNS.SetResponse;
051    import org.xbill.DNS.TextParseException;
052    import org.xbill.DNS.Type;
053    
054    import com.hs.mail.smtp.message.HostAddress;
055    
056    /**
057     * Provides DNS client functionality.
058     *
059     */
060    public class DnsServer implements InitializingBean {
061    
062            static Logger logger = Logger.getLogger(DnsServer.class);
063            
064            /**
065             * A resolver instance used to retrieve DNS records.
066             */
067            protected Resolver resolver;
068            
069            /**
070             * A TTL cache of results received from the DNS server.
071             */
072            private Cache cache;
073    
074        /**
075         * Maximum number of RR to cache.
076         */
077        private int maxCacheSize = 50000;
078            
079        /**
080         * Whether the DNS response is required to be authoritative
081         */
082        private int dnsCredibility = Credibility.NONAUTH_ANSWER;
083    
084        /**
085         * The DNS servers to be used by this service
086         */
087        private List<String> dnsServers;
088    
089            /**
090             * The MX Comparator used in the MX sort.
091             */
092            private Comparator<MXRecord> mxComparator = new MXRecordComparator();
093    
094        public String[] getDnsServers() {
095                    return dnsServers.toArray(new String[0]);
096            }
097    
098            public void setDnsServers(List<String> dnsServers) {
099                    this.dnsServers = dnsServers;
100            }
101    
102            public void afterPropertiesSet() throws Exception {
103                    if (dnsServers == null) {
104                            dnsServers = new ArrayList<String>();
105                    }
106                    
107                    // Trying to discover system's DNS servers
108                    String[] servers = ResolverConfig.getCurrentConfig().servers();
109                    if (servers != null) {
110                            for (String server : servers) {
111                                    dnsServers.add(server);
112                            }
113                    }
114                    if (dnsServers.isEmpty()) {
115                            logger.info("No DNS servers have been specified or found - adding localhost");
116                            dnsServers.add("127.0.0.1");
117                    }
118                    
119                    try {
120                            resolver = new ExtendedResolver(getDnsServers());
121                            Lookup.setDefaultResolver(resolver);
122                    } catch (UnknownHostException e) {
123                            logger.fatal("DNS server counld not be initialized. The DNS servers specified are not recognized hosts.", e);
124                            throw e;
125                    }
126                    
127                    cache = new Cache(DClass.IN);
128                    cache.setMaxEntries(maxCacheSize);
129                    Lookup.setDefaultCache(cache, DClass.IN);
130            }
131            
132            /**
133             * <p>
134             * Return a prioritized unmodifiable list of MX records obtained from the
135             * server.
136             * </p>
137             * 
138             * @param hostname
139             *            domain name to look up
140             * 
141             * @return a list of MX records corresponding to this mail domain
142             */
143            public List<String> findMXRecordsRaw(String hostname) {
144            Record answers[] = lookup(hostname, Type.MX);
145                    List<String> servers = new ArrayList<String>();
146                    if (answers == null) {
147                            return servers;
148                    }
149            
150                    MXRecord mxAnswers[] = new MXRecord[answers.length];
151                    for (int i = 0; i < answers.length; i++) {
152                            mxAnswers[i] = (MXRecord) answers[i];
153                    }
154                    
155                    Arrays.sort(mxAnswers, mxComparator);
156            
157                    for (int i = 0; i < mxAnswers.length; i++) {
158                            servers.add(mxAnswers[i].getTarget().toString());
159                    }
160                    return servers;
161            }
162            
163            /**
164             * <p>
165             * Return a prioritized unmodifiable list of host handling mail for the
166             * domain.
167             * </p>
168             * 
169             * <p>
170             * First lookup MX hosts, then MX hosts of the CNAME adress, and if no
171             * server is found return the IP of the hostname
172             * </p>
173             * 
174             * @param hostname
175             *            domain name to look up
176             * 
177             * @return a unmodifiable list of handling servers corresponding to this
178             *         mail domain name
179             */
180            public Collection<String> findMXRecords(String hostname) {
181                    List<String> servers = new ArrayList<String>();
182                    try {
183                            servers = findMXRecordsRaw(hostname);
184                            return Collections.unmodifiableCollection(servers);
185                    } finally {
186                            // If we found no results, we'll add the original domain name if
187                            // it's a valid DNS entry
188                            if (servers.size() == 0) {
189                    StringBuffer logBuffer =
190                        new StringBuffer(128)
191                                .append("Couldn't resolve MX records for domain ")
192                                .append(hostname)
193                                .append(".");
194                    logger.info(logBuffer.toString());
195                    Record cnames[] = lookup(hostname, Type.CNAME);
196                    Collection<String> cnameMXrecords = null;
197                    if (cnames != null && cnames.length > 0) {
198                        cnameMXrecords = findMXRecordsRaw(((CNAMERecord) cnames[0]).getTarget().toString());
199                    } else {
200                        logBuffer = new StringBuffer(128)
201                                        .append("Couldn't find CNAME records for domain ")
202                                        .append(hostname)
203                                        .append(".");
204                        logger.info(logBuffer.toString());
205                    }
206                                    if (cnameMXrecords == null) {
207                        try {
208                            getByName(hostname);
209                            servers.add(hostname);
210                        } catch (UnknownHostException uhe) {
211                            // The original domain name is not a valid host,
212                            // so we can't add it to the server list.  In this
213                            // case we return an empty list of servers
214                            logBuffer = new StringBuffer(128)
215                                            .append("Couldn't resolve IP address for host ")
216                                            .append(hostname)
217                                            .append(".");
218                            logger.error(logBuffer.toString());
219                        }
220                                    } else {
221                                             servers.addAll(cnameMXrecords);
222                                    }
223                            }
224                    }
225            }
226            
227            /**
228             * Looks up DNS records of the specified type for the specified name.
229             * 
230             * This method is a public wrapper for the private implementation method
231             * 
232             * @param name
233             *            the name of the host to be looked up
234             * @param type
235             *            the type of record desired
236             */
237            public Record[] lookup(String name, int type) {
238                    return rawDNSLookup(name, false, type);
239            }
240    
241            /**
242             * Looks up DNS records of the specified type for the specified name
243             * 
244             * @param namestr
245             *            the name of the host to be looked up
246             * @param querysent
247             *            whether the query has already been sent to the DNS servers
248             * @param type
249             *            the type of record desired
250             */
251            private Record[] rawDNSLookup(String namestr, boolean querysent, int type) {
252            Name name = null;
253            try {
254                            name = Name.fromString(namestr, Name.root);
255                    } catch (TextParseException e) {
256                            logger.error("Couldn't parse name " + namestr, e);
257                            return null;
258                    }
259                    int dclass = DClass.IN;
260                    
261                    SetResponse cached = cache.lookupRecords(name, type, dnsCredibility);
262                    if (cached.isSuccessful()) {
263                            if (logger.isDebugEnabled())
264                                    logger.debug(new StringBuffer(256)
265                                        .append("Retrieving MX record for ")
266                                        .append(name).append(" from cache")
267                                        .toString());
268                            return processSetResponse(cached);
269                    } else if (cached.isNXDOMAIN() || cached.isNXRRSET()) {
270                            return null;
271                    } else if (querysent) {
272                            return null;
273                    } else {
274                            if (logger.isDebugEnabled())
275                                    logger.debug(new StringBuffer(256)
276                                            .append("Looking up MX record for ")
277                                            .append(name)
278                                            .toString());
279                            Record question = Record.newRecord(name, type, dclass);
280                            Message query = Message.newQuery(question);
281                            Message response = null;
282    
283                            try {
284                                    response = resolver.send(query);
285                            } catch (IOException e) {
286                                    logger.warn("Query error!", e);
287                                    return null;
288                            }
289                            
290                            int rcode = response.getHeader().getRcode();
291                            if (rcode == Rcode.NOERROR || rcode == Rcode.NXDOMAIN) {
292                                    cached = cache.addMessage(response);
293                                    if (cached != null && cached.isSuccessful()) {
294                                            return processSetResponse(cached);
295                                    }
296                            }
297    
298                            if (rcode != Rcode.NOERROR) {
299                                    return null;
300                            }
301    
302                            return rawDNSLookup(namestr, true, type);
303                    }
304            }
305            
306            protected Record[] processSetResponse(SetResponse sr) {
307                    Record[] answers;
308                    int answerCount = 0, n = 0;
309    
310                    RRset[] rrsets = sr.answers();
311                    answerCount = 0;
312                    for (int i = 0; i < rrsets.length; i++) {
313                            answerCount += rrsets[i].size();
314                    }
315    
316                    answers = new Record[answerCount];
317    
318                    for (int i = 0; i < rrsets.length; i++) {
319                            Iterator iter = rrsets[i].rrs();
320                            while (iter.hasNext()) {
321                                    Record r = (Record) iter.next();
322                                    answers[n++] = r;
323                            }
324                    }
325                    return answers;
326            }
327        
328            /*
329             * RFC 2821 section 5 requires that we sort the MX records by their
330             * preference, and introduce a randomization. This Comparator does
331             * comparisons as normal unless the values are equal, in which case it
332             * "tosses a coin", randomly speaking.
333             * 
334             * This way MX record w/preference 0 appears before MX record w/preference
335             * 1, but a bunch of MX records with the same preference would appear in
336             * different orders each time.
337             * 
338             * Reminder for maintainers: the return value on a Comparator can be
339             * counter-intuitive for those who aren't used to the old C strcmp function:
340             * 
341             * < 0 ==> a < b = 0 ==> a = b > 0 ==> a > b
342             */
343            private static class MXRecordComparator implements Comparator<MXRecord> {
344                    private final static Random random = new Random();
345                    public int compare(MXRecord o1, MXRecord o2) {
346                            int p1 = o1.getPriority();
347                            int p2 = o2.getPriority();
348                            return (p1 == p2) ? (512 - random.nextInt(1024)) : p1 - p2;
349                    }
350            }
351    
352            /*
353             * Returns an Iterator over org.apache.mailet.HostAddress, a specialized
354             * subclass of javax.mail.URLName, which provides location information for
355             * servers that are specified as mail handlers for the given hostname. This
356             * is done using MX records, and the HostAddress instances are returned
357             * sorted by MX priority. If no host is found for domainName, the Iterator
358             * returned will be empty and the first call to hasNext() will return false.
359             * The Iterator is a nested iterator: the outer iteration is over the
360             * results of the MX record lookup, and the inner iteration is over
361             * potentially multiple A records for each MX record. DNS lookups are
362             * deferred until actually needed.
363             * 
364             * @since v2.2.0a16-unstable
365             * 
366             * @param domainName - the domain for which to find mail servers
367             * 
368             * @return an Iterator over HostAddress instances, sorted by priority
369             */
370            public Iterator<HostAddress> getSmtpHostAddress(final String domainName) {
371                    return new Iterator<HostAddress>() {
372                            private Iterator<String> mxHosts = findMXRecords(domainName).iterator();
373                            private Iterator<HostAddress> addresses = null;
374                            
375                            public boolean hasNext() {
376                                    /*
377                                     * Make sure that when next() is called, that we can provide a
378                                     * HostAddress. This means that we need to have an inner
379                                     * iterator, and verify that it has addresses. We could, for
380                                     * example, run into a situation where the next mxHost didn't
381                                     * have any valid addresses.
382                                     */
383                                    if ((addresses == null || !addresses.hasNext()) && mxHosts.hasNext()) {
384                                            do {
385                                final String nextHostname = (String)mxHosts.next();
386                                InetAddress[] addrs = null;
387                                try {
388                                                            addrs = getAllByName(nextHostname);
389                                                    } catch (UnknownHostException e) {
390                                                            // this should never happen, since we just got
391                                                            // this host from mxHosts, which should have
392                                                            // already done this check.
393                                                            StringBuffer logBuffer = new StringBuffer(128)
394                                                                            .append("Couldn't resolve IP address for discovered host ")
395                                                                            .append(nextHostname)
396                                                                            .append(".");
397                                                            logger.error(logBuffer.toString());
398                                                    }
399                                final InetAddress[] ipAddresses = addrs;
400    
401                                addresses = new Iterator<HostAddress>() {
402                                    int i = 0;
403    
404                                                            public boolean hasNext() {
405                                                                    return ipAddresses != null && i < ipAddresses.length;
406                                                            }
407    
408                                                            public HostAddress next() {
409                                                                    return new HostAddress(nextHostname, "smtp://" + ipAddresses[i++].getHostAddress());
410                                                            }
411    
412                                                            public void remove() {
413                                                                    throw new UnsupportedOperationException ("remove not supported by this iterator");
414                                                            }
415                                };
416                                            } while (!addresses.hasNext() && mxHosts.hasNext());
417                                    }
418                                    return addresses != null && addresses.hasNext();
419                            }
420    
421                            public HostAddress next() {
422                                    return addresses != null ? addresses.next() : null;
423                            }
424    
425                            public void remove() {
426                                    throw new UnsupportedOperationException ("remove not supported by this iterator");
427                            }
428                    };
429            }
430            
431            /*
432             * java.net.InetAddress.get[All]ByName(String) allows an IP literal to be
433             * passed, and will recognize it even with a trailing '.'. However,
434             * org.xbill.DNS.Address does not recognize an IP literal with a trailing
435             * '.' character. The problem is that when we lookup an MX record for some
436             * domains, we may find an IP address, which will have had the trailing '.'
437             * appended by the time we get it back from dnsjava. An MX record is not
438             * allowed to have an IP address as the right-hand-side, but there are still
439             * plenty of such records on the Internet. Since java.net.InetAddress can
440             * handle them, for the time being we've decided to support them.
441             * 
442             * These methods are NOT intended for use outside of James, and are NOT
443             * declared by the org.apache.james.services.DNSServer. This is currently a
444             * stopgap measure to be revisited for the next release.
445             */
446            private static String allowIPLiteral(String host) {
447                    if ((host.charAt(host.length() - 1) == '.')) {
448                            String possible_ip_literal = host.substring(0, host.length() - 1);
449                            if (org.xbill.DNS.Address.isDottedQuad(possible_ip_literal)) {
450                                    host = possible_ip_literal;
451                            }
452                    }
453                    return host;
454            }
455            
456        /**
457         * @see java.net.InetAddress#getByName(String)
458         */
459            public static InetAddress getByName(String host)
460                            throws UnknownHostException {
461                    return org.xbill.DNS.Address.getByName(allowIPLiteral(host));
462            }
463    
464        /**
465         * @see java.net.InetAddress#getByAllName(String)
466         */
467            public static InetAddress[] getAllByName(String host)
468                            throws UnknownHostException {
469                    return org.xbill.DNS.Address.getAllByName(allowIPLiteral(host));
470            }
471    
472    }