1 /*
2  * Copyright (c) 2000, 2011, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package sun.net.spi.nameservice.dns;
27 
28 import java.lang.ref.SoftReference;
29 import java.net.InetAddress;
30 import java.net.UnknownHostException;
31 import javax.naming.*;
32 import javax.naming.directory.*;
33 import javax.naming.spi.NamingManager;
34 import java.util.*;
35 import sun.net.util.IPAddressUtil;
36 import sun.net.dns.ResolverConfiguration;
37 import sun.net.spi.nameservice.*;
38 import java.security.AccessController;
39 import sun.security.action.*;
40 
41 /*
42  * A name service provider based on JNDI-DNS.
43  */
44 
45 public final class DNSNameService implements NameService {
46 
47     // List of domains specified by property
48     private LinkedList<String> domainList = null;
49 
50     // JNDI-DNS URL for name servers specified via property
51     private String nameProviderUrl = null;
52 
53     // Per-thread soft cache of the last temporary context
54     private static ThreadLocal<SoftReference<ThreadContext>> contextRef =
55             new ThreadLocal<>();
56 
57     // Simple class to encapsulate the temporary context
58     private static class ThreadContext {
59         private DirContext dirCtxt;
60         private List<String> nsList;
61 
ThreadContext(DirContext dirCtxt, List<String> nsList)62         public ThreadContext(DirContext dirCtxt, List<String> nsList) {
63             this.dirCtxt = dirCtxt;
64             this.nsList = nsList;
65         }
66 
dirContext()67         public DirContext dirContext() {
68             return dirCtxt;
69         }
70 
nameservers()71         public List<String> nameservers() {
72             return nsList;
73         }
74     }
75 
76     // Returns a per-thread DirContext
getTemporaryContext()77     private DirContext getTemporaryContext() throws NamingException {
78         SoftReference<ThreadContext> ref = contextRef.get();
79         ThreadContext thrCtxt = null;
80         List<String> nsList = null;
81 
82         // if no property specified we need to obtain the list of servers
83         //
84         if (nameProviderUrl == null)
85             nsList = ResolverConfiguration.open().nameservers();
86 
87         // if soft reference hasn't been gc'ed no property has been
88         // specified then we need to check if the DNS configuration
89         // has changed.
90         //
91         if ((ref != null) && ((thrCtxt = ref.get()) != null)) {
92             if (nameProviderUrl == null) {
93                 if (!thrCtxt.nameservers().equals(nsList)) {
94                     // DNS configuration has changed
95                     thrCtxt = null;
96                 }
97             }
98         }
99 
100         // new thread context needs to be created
101         if (thrCtxt == null) {
102             final Hashtable<String,Object> env = new Hashtable<>();
103             env.put("java.naming.factory.initial",
104                     "com.sun.jndi.dns.DnsContextFactory");
105 
106             // If no nameservers property specified we create provider URL
107             // based on system configured name servers
108             //
109             String provUrl = nameProviderUrl;
110             if (provUrl == null) {
111                 provUrl = createProviderURL(nsList);
112                 if (provUrl.length() == 0) {
113                     throw new RuntimeException("bad nameserver configuration");
114                 }
115             }
116             env.put("java.naming.provider.url", provUrl);
117 
118             // Need to create directory context in privileged block
119             // as JNDI-DNS needs to resolve the name servers.
120             //
121             DirContext dirCtxt;
122             try {
123                 dirCtxt = java.security.AccessController.doPrivileged(
124                         new java.security.PrivilegedExceptionAction<DirContext>() {
125                             public DirContext run() throws NamingException {
126                                 // Create the DNS context using NamingManager rather than using
127                                 // the initial context constructor. This avoids having the initial
128                                 // context constructor call itself.
129                                 Context ctx = NamingManager.getInitialContext(env);
130                                 if (!(ctx instanceof DirContext)) {
131                                     return null; // cannot create a DNS context
132                                 }
133                                 return (DirContext)ctx;
134                             }
135                     });
136             } catch (java.security.PrivilegedActionException pae) {
137                 throw (NamingException)pae.getException();
138             }
139 
140             // create new soft reference to our thread context
141             //
142             thrCtxt = new ThreadContext(dirCtxt, nsList);
143             contextRef.set(new SoftReference<ThreadContext>(thrCtxt));
144         }
145 
146         return thrCtxt.dirContext();
147     }
148 
149     /**
150      * Resolves the specified entry in DNS.
151      *
152      * Canonical name records are recursively resolved (to a maximum
153      * of 5 to avoid performance hit and potential CNAME loops).
154      *
155      * @param   ctx     JNDI directory context
156      * @param   name    name to resolve
157      * @param   ids     record types to search
158      * @param   depth   call depth - pass as 0.
159      *
160      * @return  array list with results (will have at least on entry)
161      *
162      * @throws  UnknownHostException if lookup fails or other error.
163      */
resolve(final DirContext ctx, final String name, final String[] ids, int depth)164     private ArrayList<String> resolve(final DirContext ctx, final String name,
165                                       final String[] ids, int depth)
166             throws UnknownHostException
167     {
168         ArrayList<String> results = new ArrayList<>();
169         Attributes attrs;
170 
171         // do the query
172         try {
173             attrs = java.security.AccessController.doPrivileged(
174                     new java.security.PrivilegedExceptionAction<Attributes>() {
175                         public Attributes run() throws NamingException {
176                             return ctx.getAttributes(name, ids);
177                         }
178                 });
179         } catch (java.security.PrivilegedActionException pae) {
180             throw new UnknownHostException(pae.getException().getMessage());
181         }
182 
183         // non-requested type returned so enumeration is empty
184         NamingEnumeration<? extends Attribute> ne = attrs.getAll();
185         if (!ne.hasMoreElements()) {
186             throw new UnknownHostException("DNS record not found");
187         }
188 
189         // iterate through the returned attributes
190         UnknownHostException uhe = null;
191         try {
192             while (ne.hasMoreElements()) {
193                 Attribute attr = ne.next();
194                 String attrID = attr.getID();
195 
196                 for (NamingEnumeration<?> e = attr.getAll(); e.hasMoreElements();) {
197                     String addr = (String)e.next();
198 
199                     // for canoncical name records do recursive lookup
200                     // - also check for CNAME loops to avoid stack overflow
201 
202                     if (attrID.equals("CNAME")) {
203                         if (depth > 4) {
204                             throw new UnknownHostException(name + ": possible CNAME loop");
205                         }
206                         try {
207                             results.addAll(resolve(ctx, addr, ids, depth+1));
208                         } catch (UnknownHostException x) {
209                             // canonical name can't be resolved.
210                             if (uhe == null)
211                                 uhe = x;
212                         }
213                     } else {
214                         results.add(addr);
215                     }
216                 }
217             }
218         } catch (NamingException nx) {
219             throw new UnknownHostException(nx.getMessage());
220         }
221 
222         // pending exception as canonical name could not be resolved.
223         if (results.isEmpty() && uhe != null) {
224             throw uhe;
225         }
226 
227         return results;
228     }
229 
DNSNameService()230     public DNSNameService() throws Exception {
231 
232         // default domain
233         String domain = AccessController.doPrivileged(
234             new GetPropertyAction("sun.net.spi.nameservice.domain"));
235         if (domain != null && domain.length() > 0) {
236             domainList = new LinkedList<String>();
237             domainList.add(domain);
238         }
239 
240         // name servers
241         String nameservers = AccessController.doPrivileged(
242             new GetPropertyAction("sun.net.spi.nameservice.nameservers"));
243         if (nameservers != null && nameservers.length() > 0) {
244             nameProviderUrl = createProviderURL(nameservers);
245             if (nameProviderUrl.length() == 0) {
246                 throw new RuntimeException("malformed nameservers property");
247             }
248 
249         } else {
250 
251             // no property specified so check host DNS resolver configured
252             // with at least one nameserver in dotted notation.
253             //
254             List<String> nsList = ResolverConfiguration.open().nameservers();
255             if (nsList.isEmpty()) {
256                 throw new RuntimeException("no nameservers provided");
257             }
258             boolean found = false;
259             for (String addr: nsList) {
260                 if (IPAddressUtil.isIPv4LiteralAddress(addr) ||
261                     IPAddressUtil.isIPv6LiteralAddress(addr)) {
262                     found = true;
263                     break;
264                 }
265             }
266             if (!found) {
267                 throw new RuntimeException("bad nameserver configuration");
268             }
269         }
270     }
271 
lookupAllHostAddr(String host)272     public InetAddress[] lookupAllHostAddr(String host) throws UnknownHostException {
273 
274         // DNS records that we search for
275         String[] ids = {"A", "AAAA", "CNAME"};
276 
277         // first get directory context
278         DirContext ctx;
279         try {
280             ctx = getTemporaryContext();
281         } catch (NamingException nx) {
282             throw new Error(nx);
283         }
284 
285         ArrayList<String> results = null;
286         UnknownHostException uhe = null;
287 
288         // If host already contains a domain name then just look it up
289         if (host.indexOf('.') >= 0) {
290             try {
291                 results = resolve(ctx, host, ids, 0);
292             } catch (UnknownHostException x) {
293                 uhe = x;
294             }
295         }
296 
297         // Here we try to resolve the host using the domain suffix or
298         // the domain suffix search list. If the host cannot be resolved
299         // using the domain suffix then we attempt devolution of
300         // the suffix - eg: if we are searching for "foo" and our
301         // domain suffix is "eng.sun.com" we will try to resolve
302         // "foo.eng.sun.com" and "foo.sun.com".
303         // It's not normal to attempt devolation with domains on the
304         // domain suffix search list - however as ResolverConfiguration
305         // doesn't distinguish domain or search list in the list it
306         // returns we approximate by doing devolution on the domain
307         // suffix if the list has one entry.
308 
309         if (results == null) {
310             List<String> searchList = null;
311             Iterator<String> i;
312             boolean usingSearchList = false;
313 
314             if (domainList != null) {
315                 i = domainList.iterator();
316             } else {
317                 searchList = ResolverConfiguration.open().searchlist();
318                 if (searchList.size() > 1) {
319                     usingSearchList = true;
320                 }
321                 i = searchList.iterator();
322             }
323 
324             // iterator through each domain suffix
325             while (i.hasNext()) {
326                 String parentDomain = i.next();
327                 int start = 0;
328                 while ((start = parentDomain.indexOf(".")) != -1
329                        && start < parentDomain.length() -1) {
330                     try {
331                         results = resolve(ctx, host+"."+parentDomain, ids, 0);
332                         break;
333                     } catch (UnknownHostException x) {
334                         uhe = x;
335                         if (usingSearchList) {
336                             break;
337                         }
338 
339                         // devolve
340                         parentDomain = parentDomain.substring(start+1);
341                     }
342                 }
343                 if (results != null) {
344                     break;
345                 }
346             }
347         }
348 
349         // finally try the host if it doesn't have a domain name
350         if (results == null && (host.indexOf('.') < 0)) {
351             results = resolve(ctx, host, ids, 0);
352         }
353 
354         // if not found then throw the (last) exception thrown.
355         if (results == null) {
356             assert uhe != null;
357             throw uhe;
358         }
359 
360         /**
361          * Convert the array list into a byte aray list - this
362          * filters out any invalid IPv4/IPv6 addresses.
363          */
364         assert results.size() > 0;
365         InetAddress[] addrs = new InetAddress[results.size()];
366         int count = 0;
367         for (int i=0; i<results.size(); i++) {
368             String addrString = results.get(i);
369             byte addr[] = IPAddressUtil.textToNumericFormatV4(addrString);
370             if (addr == null) {
371                 addr = IPAddressUtil.textToNumericFormatV6(addrString);
372             }
373             if (addr != null) {
374                 addrs[count++] = InetAddress.getByAddress(host, addr);
375             }
376         }
377 
378         /**
379          * If addresses are filtered then we need to resize the
380          * array. Additionally if all addresses are filtered then
381          * we throw an exception.
382          */
383         if (count == 0) {
384             throw new UnknownHostException(host + ": no valid DNS records");
385         }
386         if (count < results.size()) {
387             InetAddress[] tmp = new InetAddress[count];
388             for (int i=0; i<count; i++) {
389                 tmp[i] = addrs[i];
390             }
391             addrs = tmp;
392         }
393 
394         return addrs;
395     }
396 
397     /**
398      * Reverse lookup code. I.E: find a host name from an IP address.
399      * IPv4 addresses are mapped in the IN-ADDR.ARPA. top domain, while
400      * IPv6 addresses can be in IP6.ARPA or IP6.INT.
401      * In both cases the address has to be converted into a dotted form.
402      */
getHostByAddr(byte[] addr)403     public String getHostByAddr(byte[] addr) throws UnknownHostException {
404         String host = null;
405         try {
406             String literalip = "";
407             String[] ids = { "PTR" };
408             DirContext ctx;
409             ArrayList<String> results = null;
410             try {
411                 ctx = getTemporaryContext();
412             } catch (NamingException nx) {
413                 throw new Error(nx);
414             }
415             if (addr.length == 4) { // IPv4 Address
416                 for (int i = addr.length-1; i >= 0; i--) {
417                     literalip += (addr[i] & 0xff) +".";
418                 }
419                 literalip += "IN-ADDR.ARPA.";
420 
421                 results = resolve(ctx, literalip, ids, 0);
422                 host = results.get(0);
423             } else if (addr.length == 16) { // IPv6 Address
424                 /**
425                  * Because RFC 3152 changed the root domain name for reverse
426                  * lookups from IP6.INT. to IP6.ARPA., we need to check
427                  * both. I.E. first the new one, IP6.ARPA, then if it fails
428                  * the older one, IP6.INT
429                  */
430 
431                 for (int i = addr.length-1; i >= 0; i--) {
432                     literalip += Integer.toHexString((addr[i] & 0x0f)) +"."
433                         +Integer.toHexString((addr[i] & 0xf0) >> 4) +".";
434                 }
435                 String ip6lit = literalip + "IP6.ARPA.";
436 
437                 try {
438                     results = resolve(ctx, ip6lit, ids, 0);
439                     host = results.get(0);
440                 } catch (UnknownHostException e) {
441                     host = null;
442                 }
443                 if (host == null) {
444                     // IP6.ARPA lookup failed, let's try the older IP6.INT
445                     ip6lit = literalip + "IP6.INT.";
446                     results = resolve(ctx, ip6lit, ids, 0);
447                     host = results.get(0);
448                 }
449             }
450         } catch (Exception e) {
451             throw new UnknownHostException(e.getMessage());
452         }
453         // Either we couldn't find it or the address was neither IPv4 or IPv6
454         if (host == null)
455             throw new UnknownHostException();
456         // remove trailing dot
457         if (host.endsWith(".")) {
458             host = host.substring(0, host.length() - 1);
459         }
460         return host;
461     }
462 
463 
464     // ---------
465 
appendIfLiteralAddress(String addr, StringBuffer sb)466     private static void appendIfLiteralAddress(String addr, StringBuffer sb) {
467         if (IPAddressUtil.isIPv4LiteralAddress(addr)) {
468             sb.append("dns://" + addr + " ");
469         } else {
470             if (IPAddressUtil.isIPv6LiteralAddress(addr)) {
471                 sb.append("dns://[" + addr + "] ");
472             }
473         }
474     }
475 
476     /*
477      * @return String containing the JNDI-DNS provider URL
478      *         corresponding to the supplied List of nameservers.
479      */
createProviderURL(List<String> nsList)480     private static String createProviderURL(List<String> nsList) {
481         StringBuffer sb = new StringBuffer();
482         for (String s: nsList) {
483             appendIfLiteralAddress(s, sb);
484         }
485         return sb.toString();
486     }
487 
488     /*
489      * @return String containing the JNDI-DNS provider URL
490      *         corresponding to the list of nameservers
491      *         contained in the provided str.
492      */
createProviderURL(String str)493     private static String createProviderURL(String str) {
494         StringBuffer sb = new StringBuffer();
495         StringTokenizer st = new StringTokenizer(str, ",");
496         while (st.hasMoreTokens()) {
497             appendIfLiteralAddress(st.nextToken(), sb);
498         }
499         return sb.toString();
500     }
501 }
502