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
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
134
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
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
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
239
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
264
265
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
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
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
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
408
409
410
411
412
413
414
415
416
417
418
419
420
421
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
430
431
432
433
434
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
443
444
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
484
485
486
487
488
489
490
491
492
493
494
495
496
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 }