1 /*
   2  * Copyright (c) 2002, 2003, 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 com.sun.jndi.ldap;
  27 
  28 import java.util.Arrays;
  29 import java.util.Enumeration;
  30 import java.util.Hashtable;
  31 import java.util.NoSuchElementException;
  32 import java.util.Random;
  33 import java.util.StringTokenizer;
  34 import java.util.List;
  35 
  36 import javax.naming.*;
  37 import javax.naming.directory.*;
  38 import javax.naming.spi.NamingManager;
  39 import javax.naming.ldap.LdapName;
  40 import javax.naming.ldap.Rdn;
  41 
  42 import com.sun.jndi.ldap.LdapURL;
  43 
  44 /**
  45  * This class discovers the location of LDAP services by querying DNS.
  46  * See http://www.ietf.org/internet-drafts/draft-ietf-ldapext-locate-07.txt
  47  */
  48 
  49 class ServiceLocator {
  50 
  51     private static final String SRV_RR = "SRV";
  52 
  53     private static final String[] SRV_RR_ATTR = new String[]{SRV_RR};
  54 
  55     private static final Random random = new Random();
  56 
  57     private ServiceLocator() {
  58     }
  59 
  60     /**
  61      * Maps a distinguished name (RFC 2253) to a fully qualified domain name.
  62      * Processes a sequence of RDNs having a DC attribute.
  63      * The special RDN "DC=." denotes the root of the domain tree.
  64      * Multi-valued RDNs, non-DC attributes, binary-valued attributes and the
  65      * RDN "DC=." all reset the domain name and processing continues.
  66      *
  67      * @param dn A string distinguished name (RFC 2253).
  68      * @return A domain name or null if none can be derived.
  69      * @throw InvalidNameException If the distinugished name is invalid.
  70      */
  71     static String mapDnToDomainName(String dn) throws InvalidNameException {
  72         if (dn == null) {
  73             return null;
  74         }
  75         StringBuffer domain = new StringBuffer();
  76         LdapName ldapName = new LdapName(dn);
  77 
  78         // process RDNs left-to-right
  79         //List<Rdn> rdnList = ldapName.getRdns();
  80 
  81         List rdnList = ldapName.getRdns();
  82         for (int i = rdnList.size() - 1; i >= 0; i--) {
  83             //Rdn rdn = rdnList.get(i);
  84             Rdn rdn = (Rdn) rdnList.get(i);
  85 
  86             // single-valued RDN with a DC attribute
  87             if ((rdn.size() == 1) &&
  88                 ("dc".equalsIgnoreCase(rdn.getType()) )) {
  89                 Object attrval = rdn.getValue();
  90                 if (attrval instanceof String) {
  91                     if (attrval.equals(".") ||
  92                         (domain.length() == 1 && domain.charAt(0) == '.')) {
  93                         domain.setLength(0); // reset (when current or previous
  94                                              //        RDN value is "DC=.")
  95                     }
  96                     if (domain.length() > 0) {
  97                         domain.append('.');
  98                     }
  99                     domain.append(attrval);
 100                 } else {
 101                     domain.setLength(0); // reset (when binary-valued attribute)
 102                 }
 103             } else {
 104                 domain.setLength(0); // reset (when multi-valued RDN or non-DC)
 105             }
 106         }
 107         return (domain.length() != 0) ? domain.toString() : null;
 108     }
 109 
 110     /**
 111      * Locates the LDAP service for a given domain.
 112      * Queries DNS for a list of LDAP Service Location Records (SRV) for a
 113      * given domain name.
 114      *
 115      * @param domainName A string domain name.
 116      * @param environment The possibly null environment of the context.
 117      * @return An ordered list of hostports for the LDAP service or null if
 118      *         the service has not been located.
 119      */
 120     static String[] getLdapService(String domainName, Hashtable environment) {
 121 
 122         if (domainName == null || domainName.length() == 0) {
 123             return null;
 124         }
 125 
 126         String dnsUrl = "dns:///_ldap._tcp." + domainName;
 127         String[] hostports = null;
 128 
 129         try {
 130             // Create the DNS context using NamingManager rather than using
 131             // the initial context constructor. This avoids having the initial
 132             // context constructor call itself (when processing the URL
 133             // argument in the getAttributes call).
 134             Context ctx = NamingManager.getURLContext("dns", environment);
 135             if (!(ctx instanceof DirContext)) {
 136                 return null; // cannot create a DNS context
 137             }
 138             Attributes attrs =
 139                 ((DirContext)ctx).getAttributes(dnsUrl, SRV_RR_ATTR);
 140             Attribute attr;
 141 
 142             if (attrs != null && ((attr = attrs.get(SRV_RR)) != null)) {
 143                 int numValues = attr.size();
 144                 int numRecords = 0;
 145                 SrvRecord[] srvRecords = new SrvRecord[numValues];
 146 
 147                 // create the service records
 148                 int i = 0;
 149                 int j = 0;
 150                 while (i < numValues) {
 151                     try {
 152                         srvRecords[j] = new SrvRecord((String) attr.get(i));
 153                         j++;
 154                     } catch (Exception e) {
 155                         // ignore bad value
 156                     }
 157                     i++;
 158                 }
 159                 numRecords = j;
 160 
 161                 // trim
 162                 if (numRecords < numValues) {
 163                     SrvRecord[] trimmed = new SrvRecord[numRecords];
 164                     System.arraycopy(srvRecords, 0, trimmed, 0, numRecords);
 165                     srvRecords = trimmed;
 166                 }
 167 
 168                 // Sort the service records in ascending order of their
 169                 // priority value. For records with equal priority, move
 170                 // those with weight 0 to the top of the list.
 171                 if (numRecords > 1) {
 172                     Arrays.sort(srvRecords);
 173                 }
 174 
 175                 // extract the host and port number from each service record
 176                 hostports = extractHostports(srvRecords);
 177             }
 178         } catch (NamingException e) {
 179             // ignore
 180         }
 181         return hostports;
 182     }
 183 
 184     /**
 185      * Extract hosts and port numbers from a list of SRV records.
 186      * An array of hostports is returned or null if none were found.
 187      */
 188     private static String[] extractHostports(SrvRecord[] srvRecords) {
 189         String[] hostports = null;
 190 
 191         int head = 0;
 192         int tail = 0;
 193         int sublistLength = 0;
 194         int k = 0;
 195         for (int i = 0; i < srvRecords.length; i++) {
 196             if (hostports == null) {
 197                 hostports = new String[srvRecords.length];
 198             }
 199             // find the head and tail of the list of records having the same
 200             // priority value.
 201             head = i;
 202             while (i < srvRecords.length - 1 &&
 203                 srvRecords[i].priority == srvRecords[i + 1].priority) {
 204                 i++;
 205             }
 206             tail = i;
 207 
 208             // select hostports from the sublist
 209             sublistLength = (tail - head) + 1;
 210             for (int j = 0; j < sublistLength; j++) {
 211                 hostports[k++] = selectHostport(srvRecords, head, tail);
 212             }
 213         }
 214         return hostports;
 215     }
 216 
 217     /*
 218      * Randomly select a service record in the range [head, tail] and return
 219      * its hostport value. Follows the algorithm in RFC 2782.
 220      */
 221     private static String selectHostport(SrvRecord[] srvRecords, int head,
 222             int tail) {
 223         if (head == tail) {
 224             return srvRecords[head].hostport;
 225         }
 226 
 227         // compute the running sum for records between head and tail
 228         int sum = 0;
 229         for (int i = head; i <= tail; i++) {
 230             if (srvRecords[i] != null) {
 231                 sum += srvRecords[i].weight;
 232                 srvRecords[i].sum = sum;
 233             }
 234         }
 235         String hostport = null;
 236 
 237         // If all records have zero weight, select first available one;
 238         // otherwise, randomly select a record according to its weight
 239         int target = (sum == 0 ? 0 : random.nextInt(sum + 1));
 240         for (int i = head; i <= tail; i++) {
 241             if (srvRecords[i] != null && srvRecords[i].sum >= target) {
 242                 hostport = srvRecords[i].hostport;
 243                 srvRecords[i] = null; // make this record unavailable
 244                 break;
 245             }
 246         }
 247         return hostport;
 248     }
 249 
 250 /**
 251  * This class holds a DNS service (SRV) record.
 252  * See http://www.ietf.org/rfc/rfc2782.txt
 253  */
 254 
 255 static class SrvRecord implements Comparable {
 256 
 257     int priority;
 258     int weight;
 259     int sum;
 260     String hostport;
 261 
 262     /**
 263      * Creates a service record object from a string record.
 264      * DNS supplies the string record in the following format:
 265      * <pre>
 266      *     <Priority> " " <Weight> " " <Port> " " <Host>
 267      * </pre>
 268      */
 269     SrvRecord(String srvRecord) throws Exception {
 270         StringTokenizer tokenizer = new StringTokenizer(srvRecord, " ");
 271         String port;
 272 
 273         if (tokenizer.countTokens() == 4) {
 274             priority = Integer.parseInt(tokenizer.nextToken());
 275             weight = Integer.parseInt(tokenizer.nextToken());
 276             port = tokenizer.nextToken();
 277             hostport = tokenizer.nextToken() + ":" + port;
 278         } else {
 279             throw new IllegalArgumentException();
 280         }
 281     }
 282 
 283     /*
 284      * Sort records in ascending order of priority value. For records with
 285      * equal priority move those with weight 0 to the top of the list.
 286      */
 287     public int compareTo(Object o) {
 288         SrvRecord that = (SrvRecord) o;
 289         if (priority > that.priority) {
 290             return 1; // this > that
 291         } else if (priority < that.priority) {
 292             return -1; // this < that
 293         } else if (weight == 0 && that.weight != 0) {
 294             return -1; // this < that
 295         } else if (weight != 0 && that.weight == 0) {
 296             return 1; // this > that
 297         } else {
 298             return 0; // this == that
 299         }
 300     }
 301 }
 302 }