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
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
136
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
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
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
241
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
266
267
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
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
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
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
410
411
412
413
414
415
416
417
418
419
420
421
422
423
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
432
433
434
435
436
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
445
446
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
486
487
488
489
490
491
492
493
494
495
496
497
498
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 }