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 
  62         public ThreadContext(DirContext dirCtxt, List<String> nsList) {
  63             this.dirCtxt = dirCtxt;
  64             this.nsList = nsList;
  65         }
  66 
  67         public DirContext dirContext() {
  68             return dirCtxt;
  69         }
  70 
  71         public List<String> nameservers() {
  72             return nsList;
  73         }
  74     }
  75 
  76     // Returns a per-thread DirContext
  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      */
 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 
 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 
 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      */
 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 
 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      */
 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      */
 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 }