001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2013-2017 ForgeRock AS.
015 */
016package org.forgerock.openam.ldap;
017
018import static org.forgerock.openam.ldap.LDAPConstants.TLS_PROTOCOL_VERSION_LIST;
019import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
020
021import com.sun.identity.shared.Constants;
022import com.sun.identity.shared.configuration.SystemPropertiesManager;
023import com.sun.identity.shared.debug.Debug;
024
025import java.security.GeneralSecurityException;
026import java.util.ArrayList;
027import java.util.Collections;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.LinkedHashSet;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034import java.util.StringTokenizer;
035import java.util.concurrent.TimeUnit;
036
037import javax.naming.InvalidNameException;
038
039import org.forgerock.openam.utils.CollectionUtils;
040import org.forgerock.i18n.LocalizedIllegalArgumentException;
041import org.forgerock.opendj.ldap.Attribute;
042import org.forgerock.opendj.ldap.ByteString;
043import org.forgerock.opendj.ldap.Connection;
044import org.forgerock.opendj.ldap.ConnectionFactory;
045import org.forgerock.opendj.ldap.Connections;
046import org.forgerock.opendj.ldap.DN;
047import org.forgerock.opendj.ldap.Filter;
048import org.forgerock.opendj.ldap.LDAPConnectionFactory;
049import org.forgerock.opendj.ldap.LdapException;
050import org.forgerock.opendj.ldap.LoadBalancerEventListener;
051import org.forgerock.opendj.ldap.RDN;
052import org.forgerock.opendj.ldap.SSLContextBuilder;
053import org.forgerock.opendj.ldap.SearchResultReferenceIOException;
054import org.forgerock.opendj.ldap.SearchScope;
055import org.forgerock.opendj.ldap.TrustManagers;
056import org.forgerock.opendj.ldif.ConnectionEntryReader;
057import org.forgerock.util.Option;
058import org.forgerock.util.Options;
059import org.forgerock.util.Reject;
060import org.forgerock.util.time.Duration;
061
062/**
063 * Utility methods to help interaction with the OpenDJ LDAP SDK.
064 * There are two main ways currently to create connection pools/factories:
065 * <ul>
066 *  <li>Providing a set of servers in the format specified in {@link
067 * #prioritizeServers(java.util.Set, java.lang.String, java.lang.String)}, which will be prioritized based on the
068 * current server's server ID/site ID.</li>
069 *  <li>Providing a set of LDAPURLs, which are already considered as "prioritized".</li>
070 * </ul>
071 * In case the configuration provides the possibility to assign LDAP servers to OpenAM servers/sites, then either you
072 * can prioritize manually (if the logic differs from this implementation) and create the corresponding {@link LDAPURL}
073 * objects, or you can pass in the list to the newPrioritized* methods.
074 *
075 * @supported.all.api
076 */
077public final class LDAPUtils {
078
079    /**
080     * An {@link Option} that tells whether affinity based load balancing is enabled for the connections.
081     */
082    public static final Option<Boolean> AFFINITY_ENABLED = Option.withDefault(false);
083    private static final String LDAP_SCOPE_BASE = "SCOPE_BASE";
084    private static final String LDAP_SCOPE_ONE = "SCOPE_ONE";
085    private static final String LDAP_SCOPE_SUB = "SCOPE_SUB";
086    private static final Map<String, SearchScope> SCOPES;
087    private static final Debug DEBUG = Debug.getInstance("LDAPUtils");
088    private static final int DEFAULT_HEARTBEAT_TIMEOUT = 3;
089    private static final List<String> LDAP_SECURE_PROTOCOLS = getEnabledProtocols();
090
091    static {
092        Map<String, SearchScope> mappings = new HashMap<String, SearchScope>(3);
093        mappings.put(LDAP_SCOPE_BASE, SearchScope.BASE_OBJECT);
094        mappings.put(LDAP_SCOPE_ONE, SearchScope.SINGLE_LEVEL);
095        mappings.put(LDAP_SCOPE_SUB, SearchScope.WHOLE_SUBTREE);
096        SCOPES = Collections.unmodifiableMap(mappings);
097    }
098
099    private LDAPUtils() {
100    }
101
102    /**
103     * Based on the incoming parameters prioritizes the LDAP server list, then creates a connection pool that is
104     * capable to failover to the servers defined in case there is an error.
105     *
106     * @param servers The set of servers in the format defined in {@link
107     * #prioritizeServers(java.util.Set, java.lang.String, java.lang.String)}.
108     * @param hostServerId The server ID for this OpenAM server.
109     * @param hostSiteId The site ID for this OpenAM server.
110     * @param username The directory user's DN. May be null if this is an anonymous connection.
111     * @param password The directory user's password.
112     * @param maxSize The max size of the created pool.
113     * @param heartBeatInterval The interval for sending out heartbeat requests.
114     * @param heartBeatTimeUnit The timeunit for the heartbeat interval.
115     * @param useStartTLS Use StartTLS to connect to the LDAP Server(s).
116     * @param sslTrustAll trust all certs to connect to the LDAPS Server(s).
117     * @param ldapOptions Additional LDAP settings used to create the pool.
118     * @return A failover loadbalanced authenticated/anonymous connection pool, which may also send heartbeat requests.
119     */
120    public static ConnectionFactory newPrioritizedFailoverConnectionPool(Set<String> servers,
121            String hostServerId,
122            String hostSiteId,
123            String username,
124            char[] password,
125            int maxSize,
126            int heartBeatInterval,
127            String heartBeatTimeUnit,
128            boolean useStartTLS,
129            boolean sslTrustAll,
130            Options ldapOptions) {
131        return newFailoverConnectionPool(prioritizeServers(servers, hostServerId, hostSiteId),
132                username, password, maxSize, heartBeatInterval, heartBeatTimeUnit, useStartTLS, sslTrustAll,
133                ldapOptions);
134    }
135
136    /**
137     * Creates a new connection pool that is capable to failover to the servers defined in case there is an error.
138     *
139     * @param servers The set of LDAP URLs that will be used to set up the connection factory.
140     * @param username The directory user's DN. May be null if this is an anonymous connection.
141     * @param password The directory user's password.
142     * @param maxSize The max size of the created pool.
143     * @param heartBeatInterval The interval for sending out heartbeat requests.
144     * @param heartBeatTimeUnit The timeunit for the heartbeat interval.
145     * @param useStartTLS Use StartTLS to connect to the LDAP Server(s).
146     * @param sslTrustAll trust all certs to connect to the LDAPS Server(s).
147     * @param ldapOptions Additional LDAP settings used to create the pool
148     * @return A failover loadbalanced authenticated/anonymous connection pool, which may also send heartbeat requests.
149     */
150    public static ConnectionFactory newFailoverConnectionPool(Set<LDAPURL> servers,
151            String username,
152            char[] password,
153            int maxSize,
154            int heartBeatInterval,
155            String heartBeatTimeUnit,
156            boolean useStartTLS,
157            boolean sslTrustAll,
158            Options ldapOptions) {
159        List<ConnectionFactory> factories = new ArrayList<ConnectionFactory>(servers.size());
160        for (LDAPURL ldapurl : servers) {
161            ConnectionFactory cf = Connections.newFixedConnectionPool(
162                    newConnectionFactory(ldapurl, username, password, heartBeatInterval, heartBeatTimeUnit,
163                            useStartTLS, sslTrustAll, ldapOptions), maxSize);
164            factories.add(cf);
165        }
166
167        return loadBalanceFactories(factories, ldapOptions);
168    }
169
170    /**
171     * Based on the incoming parameters prioritizes the LDAP server list, then creates a connection factory that is
172     * capable to failover to the servers defined in case there is an error.
173     *
174     * @param servers The set of servers in the format defined in {@link
175     * #prioritizeServers(java.util.Set, java.lang.String, java.lang.String)}.
176     * @param hostServerId The server ID for this OpenAM server.
177     * @param hostSiteId The site ID for this OpenAM server.
178     * @param username The directory user's DN. May be null if this is an anonymous connection.
179     * @param password The directory user's password.
180     * @param heartBeatInterval The interval for sending out heartbeat requests.
181     * @param heartBeatTimeUnit The timeunit for the heartbeat interval.
182     * @param useStartTLS Use StartTLS to connect to the LDAP Server(s).
183     * @param sslTrustAll trust all certs to connect to the LDAPS Server(s).
184     * @param options Additional LDAP settings used to create the connection factory.
185     * @return A failover loadbalanced authenticated/anonymous connection factory, which may also send heartbeat
186     * requests.
187     */
188    public static ConnectionFactory newPrioritizedFailoverConnectionFactory(Set<String> servers,
189            String hostServerId,
190            String hostSiteId,
191            String username,
192            char[] password,
193            int heartBeatInterval,
194            String heartBeatTimeUnit,
195            boolean useStartTLS,
196            boolean sslTrustAll,
197            Options options) {
198        return newFailoverConnectionFactory(prioritizeServers(servers, hostServerId, hostSiteId),
199                username, password, heartBeatInterval, heartBeatTimeUnit, useStartTLS, sslTrustAll, options);
200    }
201
202    /**
203     * Creates a new connection factory that is capable to failover to the servers defined in case there is an error.
204     *
205     * @param servers The set of LDAP URLs that will be used to set up the connection factory.
206     * @param username The directory user's DN. May be null if this is an anonymous connection.
207     * @param password The directory user's password.
208     * @param heartBeatInterval The interval for sending out heartbeat requests.
209     * @param heartBeatTimeUnit The timeunit for the heartbeat interval.
210     * @param useStartTLS Use StartTLS to connect to the LDAP Server(s).
211     * @param sslTrustAll trust all certs to connect to the LDAPS Server(s).
212     * @param ldapOptions Additional LDAP settings used to create the connection factory.
213     * @return A failover loadbalanced authenticated/anonymous connection factory, which may also send heartbeat
214     * requests.
215     */
216    public static ConnectionFactory newFailoverConnectionFactory(Set<LDAPURL> servers,
217            String username,
218            char[] password,
219            int heartBeatInterval,
220            String heartBeatTimeUnit,
221            boolean useStartTLS,
222            boolean sslTrustAll,
223            Options ldapOptions) {
224        List<ConnectionFactory> factories = new ArrayList<ConnectionFactory>(servers.size());
225        for (LDAPURL ldapurl : servers) {
226            factories.add(newConnectionFactory(ldapurl, username, password, heartBeatInterval, heartBeatTimeUnit,
227                    useStartTLS, sslTrustAll, ldapOptions));
228        }
229        return loadBalanceFactories(factories, ldapOptions);
230    }
231
232    /**
233     * Creates a new connection factory based on the provided parameters.
234     *
235     * @param ldapurl The address of the LDAP server.
236     * @param username The directory user's DN. May be null if this is an anonymous connection.
237     * @param password The directory user's password.
238     * @param heartBeatInterval The interval for sending out heartbeat requests.
239     * @param heartBeatTimeUnit The timeunit for the heartbeat interval.
240     * @param useStartTLS Use StartTLS to connect to the LDAP Server(s).
241     * @param sslTrustAll trust all certs to connect to the LDAPS Server(s).
242     * @param ldapOptions Additional LDAP settings used to create the connection factory.
243     * @return An authenticated/anonymous connection factory, which may also send heartbeat requests.
244     */
245    private static ConnectionFactory newConnectionFactory(LDAPURL ldapurl,
246            String username,
247            char[] password,
248            int heartBeatInterval,
249            String heartBeatTimeUnit,
250            boolean useStartTLS,
251            boolean sslTrustAll,
252            Options ldapOptions) {
253        Boolean ssl = ldapurl.isSSL();
254        int heartBeatTimeout =
255                SystemPropertiesManager.getAsInt(Constants.LDAP_HEARTBEAT_TIMEOUT, DEFAULT_HEARTBEAT_TIMEOUT);
256        if (ssl != null && ssl.booleanValue() || useStartTLS) {
257            try {
258                //Creating a defensive copy of ldapOptions to handle the case when a mixture of SSL/non-SSL connections
259                //needs to be established.
260                ldapOptions = Options.copyOf(ldapOptions);
261                SSLContextBuilder builder = new SSLContextBuilder();
262                if (sslTrustAll) {
263                    builder.setTrustManager(TrustManagers.trustAll());
264                }
265                ldapOptions = ldapOptions.set(SSL_CONTEXT, builder.getSSLContext());
266                if (useStartTLS) {
267                    ldapOptions = ldapOptions.set(SSL_USE_STARTTLS, true);
268                }
269                ldapOptions = ldapOptions.set(SSL_ENABLED_PROTOCOLS, LDAP_SECURE_PROTOCOLS);
270            } catch (GeneralSecurityException gse) {
271                DEBUG.error("An error occurred while creating SSLContext", gse);
272            }
273        }
274
275        // Enable heartbeat
276        if (heartBeatInterval > 0 && heartBeatTimeUnit != null) {
277            TimeUnit unit = TimeUnit.valueOf(heartBeatTimeUnit.toUpperCase());
278            ldapOptions = ldapOptions
279                    .set(HEARTBEAT_ENABLED, true)
280                    .set(HEARTBEAT_INTERVAL, new Duration(unit.toSeconds(heartBeatInterval), TimeUnit.SECONDS))
281                    .set(HEARTBEAT_TIMEOUT, new Duration(unit.toSeconds(heartBeatTimeout), TimeUnit.SECONDS));
282        }
283
284        // Enable Authenticated connection
285        if (username != null) {
286            ldapOptions = ldapOptions.set(AUTHN_BIND_REQUEST, LDAPRequests.newSimpleBindRequest(username, password));
287        }
288
289        return new LDAPConnectionFactory(ldapurl.getHost(), ldapurl.getPort(), ldapOptions);
290    }
291
292    private static ConnectionFactory loadBalanceFactories(List<ConnectionFactory> factories, Options options) {
293        if (options.get(AFFINITY_ENABLED)) {
294            return Connections.newShardedRequestLoadBalancer(factories, options);
295        } else {
296            return Connections.newFailoverLoadBalancer(factories, options);
297        }
298    }
299
300    /**
301     * Prioritizes the incoming LDAP servers based on their assigned servers/sites.
302     * The format of the server list can be either one of the followings:
303     * <ul>
304     *  <li><code>host:port</code> - The LDAP server has no preferred
305     * server/site</li>
306     *  <li><code>host:port|serverid</code> - The LDAP server should be mainly
307     * used by an OpenAM instance with the same serverid</li>
308     *  <li><code>host:port|serverid|siteid</code> - The LDAP server should be
309     * mainly used by an OpenAM instance with the same serverid or with the same
310     * siteid</li>
311     * </ul>
312     * The resulting priority list will have the following order:
313     * <ul>
314     *  <li>servers that are linked with this server</li>
315     *  <li>servers that are linked with the current site</li>
316     *  <li>any other server that did not match in the same order as they were defined</li>
317     * </ul>
318     *
319     * @param servers The Set of servers that needs to be prioritized in the previously described format.
320     * @param hostServerId This server's ID.
321     * @param hostSiteId This server's site ID.
322     * @return The prioritized Set of LDAP URLs that can be used to create connection factories.
323     */
324    public static Set<LDAPURL> prioritizeServers(Set<String> servers, String hostServerId, String hostSiteId) {
325        Set<LDAPURL> ldapServers = new LinkedHashSet<LDAPURL>(servers.size());
326        Set<LDAPURL> serverDefined = new LinkedHashSet<LDAPURL>(servers.size());
327        Set<LDAPURL> siteDefined = new LinkedHashSet<LDAPURL>(servers.size());
328        Set<LDAPURL> nonMatchingServers = new LinkedHashSet<LDAPURL>(servers.size());
329        for (String server : servers) {
330            StringTokenizer tokenizer = new StringTokenizer(server, "|");
331            String ldapUrl = tokenizer.nextToken();
332            String assignedServerId = "";
333            String assignedSiteId = "";
334
335            if (tokenizer.hasMoreTokens()) {
336                assignedServerId = tokenizer.nextToken();
337            }
338            if (tokenizer.hasMoreTokens()) {
339                assignedSiteId = tokenizer.nextToken();
340            }
341            if (!assignedServerId.isEmpty() && assignedServerId.equals(hostServerId)) {
342                serverDefined.add(LDAPURL.valueOf(ldapUrl));
343            } else if (!assignedSiteId.isEmpty() && assignedSiteId.equals(hostSiteId)) {
344                siteDefined.add(LDAPURL.valueOf(ldapUrl));
345            } else {
346                nonMatchingServers.add(LDAPURL.valueOf(ldapUrl));
347            }
348        }
349        //Let's add them in the order of priority to the ldapServers set, this way the most appropriate servers should
350        //be at the beginning of the list and towards the end of the list are the possibly most remote servers.
351        ldapServers.addAll(serverDefined);
352        ldapServers.addAll(siteDefined);
353        ldapServers.addAll(nonMatchingServers);
354        return ldapServers;
355    }
356
357    /**
358     * Converts string representation of scope (as defined in the configuration) to the corresponding
359     * {@link SearchScope} object.
360     *
361     * @param scope the string representation of the scope.
362     * @param defaultScope in case the coversion fail this default scope should be returned.
363     * @return the corresponding {@link SearchScope} object.
364     */
365    public static SearchScope getSearchScope(String scope, SearchScope defaultScope) {
366        SearchScope searchScope = SCOPES.get(scope);
367        return searchScope == null ? defaultScope : searchScope;
368    }
369
370    /**
371     * Parses the incoming filter, and in case of failure falls back to the default filter.
372     *
373     * @param filter The filter that needs to be parsed.
374     * @param defaultFilter If the parsing fails, this will be returned.
375     * @return The parsed Filter object, or the default Filter, if the parse failed.
376     */
377    public static Filter parseFilter(String filter, Filter defaultFilter) {
378        try {
379            return filter == null ? defaultFilter : Filter.valueOf(filter);
380        } catch (LocalizedIllegalArgumentException liae) {
381            DEBUG.error("Unable to construct Filter from " + filter + " -> " + liae.getMessage()
382                    + "\nFalling back to " + defaultFilter.toString());
383        }
384        return defaultFilter;
385    }
386
387    /**
388     * Returns the RDN without the attribute name from the passed in {@link DN} object, for example:
389     * <code>uid=demo,ou=people,dc=example,dc=com</code> will return <code>demo</code>.
390     *
391     * @param dn The DN that we need the name of.
392     * @return The RDN of the DN without the attribute name.
393     */
394    public static String getName(DN dn) {
395        return dn.rdn().getFirstAVA().getAttributeValue().toString();
396    }
397
398    /**
399     * Converts the Attribute to an attribute name, 2-dimensional byte array map and adds it to the map passed in.
400     * The first dimension of the byte array separates the different values, the second dimension holds the actual
401     * value.
402     *
403     * @param attribute The attribute that needs to be converted.
404     * @param map The map where the converted attribute is added to.
405     */
406    public static void addAttributeToMapAsByteArray(Attribute attribute, Map<String, byte[][]> map) {
407        byte[][] values = new byte[attribute.size()][];
408        int counter = 0;
409        for (ByteString byteString : attribute) {
410            byte[] bytes = byteString.toByteArray();
411            values[counter++] = bytes;
412        }
413        map.put(attribute.getAttributeDescriptionAsString(), values);
414    }
415
416    /**
417     * Converts the Attribute to an attribute name, set of String values map and adds it to the map passed in.
418     *
419     * @param attribute The attribute that needs to be converted.
420     * @param map The map where the converted attribute is added to.
421     */
422    public static void addAttributeToMapAsString(Attribute attribute, Map<String, Set<String>> map) {
423        map.put(attribute.getAttributeDescriptionAsString(), getAttributeValuesAsStringSet(attribute));
424    }
425
426    /**
427     * Converts all the attribute values to a String Set.
428     *
429     * @param attribute the attribute to be converted.
430     * @return A Set of String representations of the Attribute values.
431     */
432    public static Set<String> getAttributeValuesAsStringSet(Attribute attribute) {
433        Set<String> values = new HashSet<String>(attribute.size());
434        for (ByteString byteString : attribute) {
435            values.add(byteString.toString());
436        }
437        return values;
438    }
439
440    /**
441     * Converts the incoming set of URLs to {@link LDAPURL} instances and returns them as a set. The iteration order
442     * of the originally passed in Set is retained.
443     *
444     * @param servers The LDAP server URLs that needs to be converted to {@link LDAPURL} instances.
445     * @return A set of LDAPURLs corresponding to the passed in URLs.
446     */
447    public static Set<LDAPURL> convertToLDAPURLs(Set<String> servers) {
448        if (servers == null) {
449            return new LinkedHashSet<LDAPURL>(0);
450        } else {
451            Set<LDAPURL> ret = new LinkedHashSet<LDAPURL>(servers.size());
452            for (String server : servers) {
453                ret.add(LDAPURL.valueOf(server));
454            }
455            return ret;
456        }
457    }
458
459    /**
460     * When provided a DN, returns the value part of the first RDN.
461     * @param dn A DN.
462     * @return The value part of the first RDN.
463     * @throws IllegalArgumentException When the DN's RDN is multivalued, or when the DN is not a valid name.
464     */
465    public static String rdnValueFromDn(String dn) {
466        return rdnValueFromDn(DN.valueOf(dn));
467    }
468
469    /**
470     * When provided a DN, returns the value part of the first RDN.
471     * @param dn A DN.
472     * @return The value part of the first RDN.
473     * @throws IllegalArgumentException When the DN's RDN is multivalued.
474     */
475    public static String rdnValueFromDn(DN dn) {
476        if (dn.size() > 0) {
477            return rdnValue(dn.rdn());
478        }
479        return "";
480    }
481
482    /**
483     * When provided an RDN, returns the value part.
484     * @param rdn An RDN.
485     * @return The value part.
486     * @throws IllegalArgumentException When the RDN is multivalued.
487     */
488    public static String rdnValue(RDN rdn) {
489        Reject.ifTrue(rdn.isMultiValued(), "Multivalued RDNs not supported");
490        return rdn.getFirstAVA().getAttributeValue().toString();
491    }
492
493    /**
494     * When provided a DN, returns the attribute type name of the first RDN.
495     * @param dn A DN.
496     * @return The attribute type name of the first RDN.
497     * @throws IllegalArgumentException When the DN's RDN is multivalued.
498     */
499    public static String rdnTypeFromDn(String dn) {
500        return rdnTypeFromDn(DN.valueOf(dn));
501    }
502
503    /**
504     * When provided a DN, returns the attribute type name of the first RDN.
505     * @param dn A DN.
506     * @return The attribute type name of the first RDN.
507     * @throws IllegalArgumentException When the DN's RDN is multivalued.
508     */
509    public static String rdnTypeFromDn(DN dn) {
510        if (dn.size() > 0) {
511            return rdnType(dn.rdn());
512        }
513        return "";
514    }
515
516    /**
517     * When provided an RDN, returns the attribute type name.
518     * @param rdn An RDN.
519     * @return The attribute type name.
520     * @throws IllegalArgumentException When the RDN is multivalued.
521     */
522    public static String rdnType(RDN rdn) {
523        Reject.ifTrue(rdn.size() != 1, "Multivalued RDNs not supported");
524        return rdn.getFirstAVA().getAttributeType().getNameOrOID();
525    }
526
527    /**
528     * Returns a set of all the non-root DNs from the collection that are not equal to the {@code compare} parameter.
529     * @param compare The DN to compare against.
530     * @param dns THe DNs to compare.
531     * @return A {@code Set} of non identical DNs.
532     * @throws InvalidNameException If an error occurs.
533     */
534    public static Set<String> collectNonIdenticalValues(DN compare, Set<String> dns) throws InvalidNameException {
535        Set<String> results = new HashSet<>();
536        for (String dnString : dns) {
537            DN dn = DN.valueOf(dnString);
538            if (dn.size() > 0 && compare.compareTo(dn) != 0) {
539                results.add(rdnValueFromDn(dn));
540            }
541        }
542        return results;
543    }
544
545    /**
546     * Gets the DB name.
547     *
548     * @param suffix The suffix.
549     * @param ld The connection.
550     * @return The name of the DB.
551     */
552    public static String getDBName(String suffix, Connection ld) {
553        String filter = "cn=" + suffix;
554
555        try {
556            ConnectionEntryReader results = ld.search(LDAPRequests.newSearchRequest("cn=mapping tree,cn=config",
557                    SearchScope.WHOLE_SUBTREE, filter));
558            while (results.hasNext()) {
559                Attribute dbName = results.readEntry().getAttribute("nsslapd-backend");
560                if (dbName != null) {
561                    return dbName.firstValueAsString();
562                }
563            }
564        } catch (LdapException e) {
565            // If not S1DS, then cn=mapping tree wouldn't exist.
566            // Hence return userRoot as DBNAME.
567        } catch (SearchResultReferenceIOException e) {
568            DEBUG.error("LDAPUtils.getDBName: Did not expect to get a reference", e);
569        }
570        return "userRoot";
571    }
572
573    /**
574     * Tests whether the supplied string is a DN, and is not the root DN.
575     * @param candidateDN The possible DN.
576     * @return {@code true} if the string is a DN.
577     */
578    public static boolean isDN(String candidateDN) {
579        try {
580            return newDN(candidateDN).size() > 0;
581        } catch (LocalizedIllegalArgumentException e) {
582            DEBUG.error("LDAPUtils.isDN: Invalid DN", e);
583        }
584        return false;
585    }
586
587    /**
588     * Escapes characters that should be escaped.
589     *
590     * @param str The string to escape.
591     * @return The escaped string.
592     */
593    public static String escapeValue(String str) {
594        return DN.escapeAttributeValue(str);
595    }
596
597    /**
598     * Escapes the provided assertion value according to the LDAP standard. As a special case this method does not
599     * escape the '*' character, in order to be able to use wildcards in filters.
600     *
601     * @param assertionValue The filter assertionValue that needs to be escaped.
602     * @return The escaped assertionValue.
603     */
604    public static String partiallyEscapeAssertionValue(String assertionValue) {
605        StringBuilder sb = new StringBuilder(assertionValue.length());
606        for (int j = 0; j < assertionValue.length(); j++) {
607            char c = assertionValue.charAt(j);
608            if (c == '*') {
609                sb.append(c);
610            } else {
611                sb.append(Filter.escapeAssertionValue(String.valueOf(c)));
612            }
613        }
614        return sb.toString();
615    }
616
617    /**
618     * Normalizes the DN.
619     *
620     * @param dn The DN to normalize.
621     * @return The normalized DN.
622     */
623    public static String normalizeDN(String dn) {
624        return newDN(dn).toString().toLowerCase();
625    }
626
627    /**
628     * Creates a DN from the specified DN string.
629     *
630     * @param orgName The DN string.
631     * @return A DN.
632     */
633    public static DN newDN(String orgName) {
634        if (orgName == null || orgName.startsWith("/") || !orgName.contains("=")) {
635            return DN.rootDN();
636        } else {
637            return DN.valueOf(orgName);
638        }
639    }
640
641    /**
642     * Converts a DN String to a RFC format and lowers case.
643     *
644     * @param dn
645     *            the DN String to be formated
646     * @return a lowercase RFC fromat DN String
647     */
648    public static String formatToRFC(String dn) {
649        return DN.valueOf(dn).toString().toLowerCase();
650    }
651
652    /**
653     * Determines if the DN's are equal.
654     *
655     * @param dn1 The first DN.
656     * @param dn2 The second DN.
657     * @return {@code true} if the DN's are equal.
658     */
659    public static boolean dnEquals(String dn1, String dn2) {
660        DN dnObj1 = DN.valueOf(dn1);
661        DN dnObj2 = DN.valueOf(dn2);
662        return dnObj1.equals(dnObj2);
663    }
664
665    private static class LoggingLBEventListener implements LoadBalancerEventListener {
666
667        public void handleConnectionFactoryOffline(ConnectionFactory factory, LdapException error) {
668            DEBUG.error("Connection factory became offline: " + factory, error);
669        }
670
671        public void handleConnectionFactoryOnline(ConnectionFactory factory) {
672            DEBUG.error("Connection factory became online: " + factory);
673        }
674    }
675
676    /**
677     * Creates a ConnectionFactory from the host string and associated details. The host string can be any of the
678     * following:
679     * <ul>
680     *     <li>A plain hostname/IP address</li>
681     *     <li>A hostname and port, in the format <code>[host]:[port]</code></li>
682     *     <li>A space-separated list of hostnames in priority order, e.g. <code>host1 host2 host3</code></li>
683     *     <li>
684     *         A space-separated list of hostnames with port numbers in priority order, e.g.
685     *         <code>host1:389 host2:50389</code>
686     *     </li>
687     * </ul>
688     * If a list of hosts is given, a load balanced {@code ConnectionFactory} is returned. All factories are
689     * pre-authenticated using the supplied credentials.
690     * @param host The host/host-port string.
691     * @param defaultPort The port number to use for hosts that do not specify a port in the string.
692     * @param ssl SSL enabled or not.
693     * @param authDN The DN to bind with.
694     * @param authPasswd The password to bind with.
695     * @param options Any additional options.
696     * @return A connection factory.
697     */
698    public static ConnectionFactory createFailoverConnectionFactory(String host, int defaultPort, boolean ssl,
699            String authDN, String authPasswd, Options options) {
700        StringTokenizer st = new StringTokenizer(host);
701        String[] hostList = new String[st.countTokens()];
702        int[] portList = new int[st.countTokens()];
703        int hostCount = 0;
704        while (st.hasMoreTokens()) {
705            String s = st.nextToken();
706            int colon = s.indexOf(':');
707            if (colon > 0) {
708                hostList[hostCount] = s.substring(0, colon);
709                portList[hostCount] = Integer.parseInt(s.substring(colon + 1));
710            } else {
711                hostList[hostCount] = s;
712                portList[hostCount] = defaultPort;
713            }
714            hostCount++;
715        }
716
717        if (hostCount > 1) {
718            List<ConnectionFactory> factories = new ArrayList<>();
719            for (int i = 0; i < hostCount; i++) {
720                factories.add(createSingleHostConnectionFactory(hostList[i], portList[i], ssl, authDN, authPasswd,
721                        options));
722            }
723            return loadBalanceFactories(factories, options);
724        } else {
725            return createSingleHostConnectionFactory(hostList[0], portList[0], ssl, authDN, authPasswd, options);
726        }
727    }
728
729    /**
730     * Converts the serverName, port and ssl into LDAPURL and add it into a Set.
731     *
732     * @param serverName The LDAP server name.
733     * @param port The LDAP server port number.
734     * @param isSSL boolean value of true/false for ssl.
735     * @return A set of LDAPURLs based on the passed serverName, port and ssl.
736     */
737    public static Set<LDAPURL> getLdapUrls(String serverName, int port, boolean isSSL) {
738        return CollectionUtils.asSet(LDAPURL.valueOf(serverName, port, isSSL));
739    }
740
741    /**
742     * Converts the ldapServers and ssl into LDAPURL and add it into a Set.
743     *
744     * @param ldapServers The LDAP servers in the format of serverName:port
745     * @param isSSL boolean value of true/false for ssl.
746     * @return A set of LDAPURLs based on the passed serverName, port and ssl.
747     */
748    public static Set<LDAPURL> getLdapUrls(Set<LDAPURL> ldapServers, boolean isSSL) {
749        Set<LDAPURL> ldapUrls = new LinkedHashSet<>(ldapServers.size());
750        for (LDAPURL url : ldapServers) {
751            ldapUrls.add(LDAPURL.valueOf(url.getHost(), url.getPort(), isSSL));
752        }
753        return ldapUrls;
754    }
755
756    private static ConnectionFactory createSingleHostConnectionFactory(String host, int port, boolean ssl,
757            String authDN, String authPasswd, Options options) {
758        options = options.set(AUTHN_BIND_REQUEST, LDAPRequests.newSimpleBindRequest(authDN, authPasswd.toCharArray()));
759        if (ssl) {
760            try {
761                options = options.set(LDAPConnectionFactory.SSL_CONTEXT, new SSLContextBuilder().getSSLContext());
762                options = options.set(SSL_ENABLED_PROTOCOLS, LDAP_SECURE_PROTOCOLS);
763            } catch (GeneralSecurityException gse) {
764                DEBUG.error("An error occurred while creating SSLContext", gse);
765            }
766
767        }
768        return new LDAPConnectionFactory(host, port, options);
769    }
770
771    /**
772     * Return a list of the available TLS protocols to use.
773     *
774     * <p>
775     * All protocols starting with "SSL" are removed. In particular the SSLv2Hello pseudo protocol is removed which will
776     * prevent connections to a server that doesn't have this configured.
777     * </p>
778     * <p>
779     * To override the defaults, set the <em>org.forgerock.openam.ldap.secure.protocol.version</em> system property
780     * to a comma-separated list of desired protocols.
781     * </p>
782     *
783     * @return A list of valid protocol strings.
784     */
785    private static List<String> getEnabledProtocols() {
786
787        if (TLS_PROTOCOL_VERSION_LIST != null && TLS_PROTOCOL_VERSION_LIST.size() != 0) {
788            if (DEBUG.messageEnabled()) {
789                DEBUG.message("LDAPUtils: LDAPS Protocols specified " + TLS_PROTOCOL_VERSION_LIST.toString());
790            }
791            return TLS_PROTOCOL_VERSION_LIST;
792        }
793
794        List<String> enabled = new ArrayList<String>();
795        List<String> protocols = new ArrayList<String>();
796        try {
797            enabled = CollectionUtils.asList(new SSLContextBuilder().getSSLContext()
798                .createSSLEngine().getEnabledProtocols());
799        } catch (GeneralSecurityException gse) {
800            DEBUG.error("An error occurred while setting the SSLContext", gse);
801        }
802
803        // exclude SSLv2Hello and SSLv3
804        for (String  currentProtocol : enabled) {
805            if (!currentProtocol.startsWith("SSL")) {
806                protocols.add(currentProtocol);
807            }
808        }
809        if (DEBUG.messageEnabled()) {
810            DEBUG.message("LDAPUtils: LDAPS Protocols used " + protocols.toString());
811        }
812        return protocols;
813    }
814}