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