/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2010 Sun Microsystems, Inc. */ package org.opends.sdk; import static com.sun.opends.sdk.messages.Messages.*; import java.util.*; import org.opends.sdk.requests.Requests; import org.opends.sdk.requests.SearchRequest; import org.opends.sdk.schema.Schema; import com.sun.opends.sdk.util.StaticUtils; import com.sun.opends.sdk.util.Validator; /** * An LDAP URL as defined in RFC 4516. In addition, the secure ldap (ldaps://) * is also supported. LDAP URLs have the following format: * *
 * "ldap[s]://" [ hostName [":" portNumber] ]
 *          "/" distinguishedName
 *          ["?" attributeList
 *              ["?" scope "?" filterString ] ]
 * 
* * Where: * * The same encoding rules for other URLs (e.g. HTTP) apply for LDAP URLs. * Specifically, any "illegal" characters are escaped with * %HH, where HH represent the two hex * digits which correspond to the ASCII value of the character. This encoding is * only legal (or necessary) on the DN and filter portions of the URL. *

* Note that this class does not implement extensions. * * @see RFC 4516 - Lightweight * Directory Access Protocol (LDAP): Uniform Resource Locator */ public final class LDAPUrl { /** * The scheme corresponding to an LDAP URL. RFC 4516 mandates only ldap scheme * but we support "ldaps" too. */ private final boolean isSecured; /** * The host name corresponding to an LDAP URL. */ private final String host; /** * The port number corresponding to an LDAP URL. */ private final int port; /** * The distinguished name corresponding to an LDAP URL. */ private final DN name; /** * The search scope corresponding to an LDAP URL. */ private final SearchScope scope; /** * The search filter corresponding to an LDAP URL. */ private final Filter filter; /** * The attributes that need to be searched. */ private final List attributes; /** * The String value of LDAP URL. */ private final String urlString; /** * Normalized ldap URL. */ private String normalizedURL; /** * The default scheme to be used with LDAP URL. */ private static final String DEFAULT_URL_SCHEME = "ldap"; /** * The SSL-based scheme allowed to be used with LDAP URL. */ private static final String SSL_URL_SCHEME = "ldaps"; /** * The default host. */ private static final String DEFAULT_HOST = "localhost"; /** * The default non-SSL port. */ private static final int DEFAULT_PORT = 389; /** * The default SSL port. */ private static final int DEFAULT_SSL_PORT = 636; /** * The default filter. */ private static final Filter DEFAULT_FILTER = Filter .getObjectClassPresentFilter(); /** * The default search scope. */ private static final SearchScope DEFAULT_SCOPE = SearchScope.BASE_OBJECT; /** * The default distinguished name. */ private static final DN DEFAULT_DN = DN.rootDN(); /** * The % encoding character. */ private static final char PERCENT_ENCODING_CHAR = '%'; /** * The ? character. */ private static final char QUESTION_CHAR = '?'; /** * The slash (/) character. */ private static final char SLASH_CHAR = '/'; /** * The comma (,) character. */ private static final char COMMA_CHAR = ','; /** * The colon (:) character. */ private static final char COLON_CHAR = ':'; /** * Set containing characters that do not need to be encoded. */ private static final Set VALID_CHARS = new HashSet(); static { // Refer to RFC 3986 for more details. final char[] delims = { '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', '.', '-', '_', '~' }; for (final char c : delims) { VALID_CHARS.add(c); } for (char c = 'a'; c <= 'z'; c++) { VALID_CHARS.add(c); } for (char c = 'A'; c <= 'Z'; c++) { VALID_CHARS.add(c); } for (char c = '0'; c <= '9'; c++) { VALID_CHARS.add(c); } } /** * Parses the provided LDAP string representation of an LDAP URL using the * default schema. * * @param url * The LDAP string representation of an LDAP URL. * @return The parsed LDAP URL. * @throws LocalizedIllegalArgumentException * If {@code url} is not a valid LDAP string representation of an * LDAP URL. * @throws NullPointerException * If {@code url} was {@code null}. */ public static LDAPUrl valueOf(final String url) throws LocalizedIllegalArgumentException, NullPointerException { return valueOf(url, Schema.getDefaultSchema()); } /** * Parses the provided LDAP string representation of an LDAP URL using the * provided schema. * * @param url * The LDAP string representation of an LDAP URL. * @param schema * The schema to use when parsing the LDAP URL. * @return The parsed LDAP URL. * @throws LocalizedIllegalArgumentException * If {@code url} is not a valid LDAP string representation of an * LDAP URL. * @throws NullPointerException * If {@code url} or {@code schema} was {@code null}. */ public static LDAPUrl valueOf(final String url, final Schema schema) throws LocalizedIllegalArgumentException, NullPointerException { Validator.ensureNotNull(url, schema); return new LDAPUrl(url, schema); } private static int decodeHex(final String url, final int index, final char hexChar) throws LocalizedIllegalArgumentException { if (hexChar >= '0' && hexChar <= '9') { return hexChar - '0'; } else if (hexChar >= 'A' && hexChar <= 'F') { return hexChar - 'A' + 10; } else if (hexChar >= 'a' && hexChar <= 'f') { return hexChar - 'a' + 10; } final LocalizableMessage msg = ERR_LDAPURL_INVALID_HEX_BYTE.get(url, index); throw new LocalizedIllegalArgumentException(msg); } private static void percentDecoder(final String urlString, final int index, final String s, final StringBuilder decoded) throws LocalizedIllegalArgumentException { Validator.ensureNotNull(s, decoded); decoded.append(s); int srcPos = 0, dstPos = 0; while (srcPos < decoded.length()) { if (decoded.charAt(srcPos) != '%') { if (srcPos != dstPos) { decoded.setCharAt(dstPos, decoded.charAt(srcPos)); } srcPos++; dstPos++; continue; } decoded.setCharAt(dstPos, (char) ((decodeHex(urlString, index + srcPos + 1, decoded.charAt(srcPos + 1)) << 4) | (decodeHex(urlString, index + srcPos + 2, decoded.charAt(srcPos + 2))))); dstPos++; srcPos += 3; } decoded.setLength(dstPos); } /** * This method performs the percent-encoding as defined in section 2.1 of RFC * 3986. * * @param urlElement * The element of the URL that needs to be percent encoded. * @param encodedBuffer * The buffer that contains the final percent encoded value. */ private static void percentEncoder(final String urlElement, final StringBuilder encodedBuffer) { Validator.ensureNotNull(urlElement); for (int count = 0; count < urlElement.length(); count++) { final char c = urlElement.charAt(count); if (VALID_CHARS.contains(c)) { encodedBuffer.append(c); } else { encodedBuffer.append(PERCENT_ENCODING_CHAR); encodedBuffer.append(Integer.toHexString(c)); } } } /** * Creates a new LDAP URL referring to a single entry on the specified server. * The LDAP URL with have base object scope and the filter {@code * (objectClass=*)}. * * @param isSecured * {@code true} if this LDAP URL should use LDAPS or {@code false} if * it should use LDAP. * @param host * The name or IP address in dotted format of the LDAP server. For * example, {@code ldap.server1.com} or {@code 192.202.185.90}. Use * {@code null} for the local host. * @param port * The port number of the LDAP server, or {@code null} to use the * default port (389 for LDAP and 636 for LDAPS). * @param name * The distinguished name of the base entry relative to which the * search is to be performed, or {@code null} to specify the root * DSE. * @throws LocalizedIllegalArgumentException * If {@code port} was less than 1 or greater than 65535. */ public LDAPUrl(final boolean isSecured, final String host, final Integer port, final DN name) throws LocalizedIllegalArgumentException { this(isSecured, host, port, name, DEFAULT_SCOPE, DEFAULT_FILTER); } /** * Creates a new LDAP URL including the full set of parameters for a search * request. * * @param isSecured * {@code true} if this LDAP URL should use LDAPS or {@code false} if * it should use LDAP. * @param host * The name or IP address in dotted format of the LDAP server. For * example, {@code ldap.server1.com} or {@code 192.202.185.90}. Use * {@code null} for the local host. * @param port * The port number of the LDAP server, or {@code null} to use the * default port (389 for LDAP and 636 for LDAPS). * @param name * The distinguished name of the base entry relative to which the * search is to be performed, or {@code null} to specify the root * DSE. * @param scope * The search scope, or {@code null} to specify base scope. * @param filter * The search filter, or {@code null} to specify the filter {@code * (objectClass=*)}. * @param attributes * The list of attributes to be included in the search results. * @throws LocalizedIllegalArgumentException * If {@code port} was less than 1 or greater than 65535. */ public LDAPUrl(final boolean isSecured, final String host, final Integer port, final DN name, final SearchScope scope, final Filter filter, final String... attributes) throws LocalizedIllegalArgumentException { // The buffer storing the encoded url. final StringBuilder urlBuffer = new StringBuilder(); // build the scheme. this.isSecured = isSecured; if (this.isSecured) { urlBuffer.append(SSL_URL_SCHEME); } else { urlBuffer.append(DEFAULT_URL_SCHEME); } urlBuffer.append("://"); if (host == null) { this.host = DEFAULT_HOST; } else { this.host = host; urlBuffer.append(this.host); } int listenPort = DEFAULT_PORT; if (port == null) { listenPort = isSecured ? DEFAULT_SSL_PORT : DEFAULT_PORT; } else { listenPort = port.intValue(); if (listenPort < 1 || listenPort > 65535) { final LocalizableMessage msg = ERR_LDAPURL_BAD_PORT.get(listenPort); throw new LocalizedIllegalArgumentException(msg); } urlBuffer.append(COLON_CHAR); urlBuffer.append(listenPort); } this.port = listenPort; // We need a slash irrespective of dn is defined or not. urlBuffer.append(SLASH_CHAR); if (name != null) { this.name = name; percentEncoder(name.toString(), urlBuffer); } else { this.name = DEFAULT_DN; } // Add attributes. urlBuffer.append(QUESTION_CHAR); switch (attributes.length) { case 0: this.attributes = Collections.emptyList(); break; case 1: this.attributes = Collections.singletonList(attributes[0]); urlBuffer.append(attributes[0]); break; default: this.attributes = Collections.unmodifiableList(Arrays.asList(attributes)); urlBuffer.append(attributes[0]); for (int i = 1; i < attributes.length; i++) { urlBuffer.append(COMMA_CHAR); urlBuffer.append(attributes[i]); } break; } // Add the scope. urlBuffer.append(QUESTION_CHAR); if (scope != null) { this.scope = scope; urlBuffer.append(scope); } else { this.scope = DEFAULT_SCOPE; } // Add the search filter. urlBuffer.append(QUESTION_CHAR); if (filter != null) { this.filter = filter; urlBuffer.append(this.filter); } else { this.filter = DEFAULT_FILTER; } urlString = urlBuffer.toString(); } private LDAPUrl(final String urlString, final Schema schema) throws LocalizedIllegalArgumentException { this.urlString = urlString; // Parse the url and build the LDAP URL. final int schemeIdx = urlString.indexOf("://"); if (schemeIdx < 0) { final LocalizableMessage msg = ERR_LDAPURL_NO_SCHEME.get(urlString); throw new LocalizedIllegalArgumentException(msg); } final String scheme = StaticUtils.toLowerCase(urlString.substring(0, schemeIdx)); if (scheme.equalsIgnoreCase(DEFAULT_URL_SCHEME)) { // Default ldap scheme. isSecured = false; } else if (scheme.equalsIgnoreCase(SSL_URL_SCHEME)) { isSecured = true; } else { final LocalizableMessage msg = ERR_LDAPURL_BAD_SCHEME.get(urlString, scheme); throw new LocalizedIllegalArgumentException(msg); } final int urlLength = urlString.length(); final int hostPortIdx = urlString.indexOf(SLASH_CHAR, schemeIdx + 3); final StringBuilder builder = new StringBuilder(); if (hostPortIdx < 0) { // We got anything here like the host and port? if (urlLength > (schemeIdx + 3)) { final String hostAndPort = urlString .substring(schemeIdx + 3, urlLength); port = parseHostPort(urlString, hostAndPort, builder); host = builder.toString(); builder.setLength(0); } else { // Nothing else is specified apart from the scheme. // Use the default settings and return from here. host = DEFAULT_HOST; port = isSecured ? DEFAULT_SSL_PORT : DEFAULT_PORT; } name = DEFAULT_DN; scope = DEFAULT_SCOPE; filter = DEFAULT_FILTER; attributes = Collections.emptyList(); return; } final String hostAndPort = urlString.substring(schemeIdx + 3, hostPortIdx); // assign the host and port. port = parseHostPort(urlString, hostAndPort, builder); host = builder.toString(); builder.setLength(0); // Parse the dn. DN parsedDN = null; final int dnIdx = urlString.indexOf(QUESTION_CHAR, hostPortIdx + 1); if (dnIdx < 0) { // Whatever we have here is the dn. final String dnStr = urlString.substring(hostPortIdx + 1, urlLength); percentDecoder(urlString, hostPortIdx + 1, dnStr, builder); try { parsedDN = DN.valueOf(builder.toString(), schema); } catch (final LocalizedIllegalArgumentException e) { final LocalizableMessage msg = ERR_LDAPURL_INVALID_DN.get(urlString, e .getMessageObject()); throw new LocalizedIllegalArgumentException(msg); } builder.setLength(0); name = parsedDN; scope = DEFAULT_SCOPE; filter = DEFAULT_FILTER; attributes = Collections.emptyList(); return; } final String dnStr = urlString.substring(hostPortIdx + 1, dnIdx); if (dnStr.length() == 0) { parsedDN = DEFAULT_DN; } else { percentDecoder(urlString, hostPortIdx + 1, dnStr, builder); try { parsedDN = DN.valueOf(builder.toString(), schema); } catch (final LocalizedIllegalArgumentException e) { final LocalizableMessage msg = ERR_LDAPURL_INVALID_DN.get(urlString, e .getMessageObject()); throw new LocalizedIllegalArgumentException(msg); } builder.setLength(0); } name = parsedDN; // Find out the attributes. final int attrIdx = urlString.indexOf(QUESTION_CHAR, dnIdx + 1); if (attrIdx < 0) { attributes = Collections.emptyList(); scope = DEFAULT_SCOPE; filter = DEFAULT_FILTER; return; } else { final String attrDesc = urlString.substring(dnIdx + 1, attrIdx); final StringTokenizer token = new StringTokenizer(attrDesc, String .valueOf(COMMA_CHAR)); final List parsedAttrs = new ArrayList(token .countTokens()); while (token.hasMoreElements()) { parsedAttrs.add(token.nextToken()); } attributes = Collections.unmodifiableList(parsedAttrs); } // Find the scope. final int scopeIdx = urlString.indexOf(QUESTION_CHAR, attrIdx + 1); SearchScope parsedScope = SearchScope.BASE_OBJECT; if (scopeIdx < 0) { scope = DEFAULT_SCOPE; filter = DEFAULT_FILTER; return; } else { String scopeDef = urlString.substring(attrIdx + 1, scopeIdx); scopeDef = StaticUtils.toLowerCase(scopeDef); for (final SearchScope sscope : SearchScope.values()) { if (sscope.toString().equals(scopeDef)) { parsedScope = sscope; break; } } } scope = parsedScope; // Last one is filter. final String parsedFilter = urlString.substring(scopeIdx + 1, urlLength); if (parsedFilter.length() > 0) { // Clear what we already have. builder.setLength(0); percentDecoder(urlString, scopeIdx + 1, parsedFilter, builder); try { this.filter = Filter.valueOf(builder.toString()); } catch (final LocalizedIllegalArgumentException e) { final LocalizableMessage msg = ERR_LDAPURL_INVALID_FILTER.get( urlString, e.getMessageObject()); throw new LocalizedIllegalArgumentException(msg); } } else { this.filter = DEFAULT_FILTER; } } /** * Creates a new search request containing the parameters of this LDAP URL. * * @return A new search request containing the parameters of this LDAP URL. */ public SearchRequest asSearchRequest() { final SearchRequest request = Requests .newSearchRequest(name, scope, filter); for (final String a : attributes) { request.addAttribute(a); } return request; } /** * {@inheritDoc} */ @Override() public boolean equals(final Object o) { if (o == this) { return true; } else if (o instanceof LDAPUrl) { final String s1 = toNormalizedString(); final String s2 = ((LDAPUrl) o).toNormalizedString(); return s1.equals(s2); } else { return false; } } /** * Returns an unmodifiable list containing the attributes to be included with * each entry that matches the search criteria. Attributes that are sub-types * of listed attributes are implicitly included. If the returned list is empty * then all user attributes will be included by default. * * @return An unmodifiable list containing the attributes to be included with * each entry that matches the search criteria. */ public List getAttributes() { return attributes; } /** * Returns the search filter associated with this LDAP URL. * * @return The search filter associated with this LDAP URL. */ public Filter getFilter() { return filter; } /** * Returns the name or IP address in dotted format of the LDAP server * referenced by this LDAP URL. For example, {@code ldap.server1.com} or * {@code 192.202.185.90}. Use {@code null} for the local host. * * @return A name or IP address in dotted format of the LDAP server referenced * by this LDAP URL. */ public String getHost() { return host; } /** * Returns the distinguished name of the base entry relative to which the * search is to be performed. * * @return The distinguished name of the base entry relative to which the * search is to be performed. */ public DN getName() { return name; } /** * Returns the port number of the LDAP server referenced by this LDAP URL. * * @return The port number of the LDAP server referenced by this LDAP URL. */ public int getPort() { return port; } /** * Returns the search scope associated with this LDAP URL. * * @return The search scope associated with this LDAP URL. */ public SearchScope getScope() { return scope; } /** * {@inheritDoc} */ @Override public int hashCode() { final String s = toNormalizedString(); return s.hashCode(); } /** * Returns {@code true} if this LDAP URL should use LDAPS or {@code false} if * it should use LDAP. * * @return {@code true} if this LDAP URL should use LDAPS or {@code false} if * it should use LDAP. */ public boolean isSecure() { return isSecured; } /** * {@inheritDoc} */ @Override public String toString() { return urlString; } private int parseHostPort(final String urlString, final String hostAndPort, final StringBuilder host) throws LocalizedIllegalArgumentException { Validator.ensureNotNull(hostAndPort, port, host); int urlPort = isSecured ? DEFAULT_SSL_PORT : DEFAULT_PORT; if (hostAndPort.length() == 0) { host.append(DEFAULT_HOST); return urlPort; } final int colonIdx = hostAndPort.indexOf(':'); if (colonIdx < 0) { // port is not specified. host.append(hostAndPort); return urlPort; } else { String s = hostAndPort.substring(0, colonIdx); if (s.length() == 0) { // Use the default host as we allow only the port to be specified. host.append(DEFAULT_HOST); } else { host.append(s); } s = hostAndPort.substring(colonIdx + 1, hostAndPort.length()); try { urlPort = Integer.parseInt(s); } catch (final NumberFormatException e) { final LocalizableMessage msg = ERR_LDAPURL_CANNOT_DECODE_PORT.get( urlString, s); throw new LocalizedIllegalArgumentException(msg); } // Check the validity of the port. if (urlPort < 1 || urlPort > 65535) { final LocalizableMessage msg = ERR_LDAPURL_INVALID_PORT.get(urlString, urlPort); throw new LocalizedIllegalArgumentException(msg); } } return urlPort; } private String toNormalizedString() { if (normalizedURL == null) { final StringBuilder builder = new StringBuilder(); if (this.isSecured) { builder.append(SSL_URL_SCHEME); } else { builder.append(DEFAULT_URL_SCHEME); } builder.append("://"); builder.append(host); builder.append(COLON_CHAR); builder.append(port); builder.append(SLASH_CHAR); percentEncoder(name.toString(), builder); builder.append(QUESTION_CHAR); final int sz = attributes.size(); for (int i = 0; i < sz; i++) { if (i > 0) { builder.append(COMMA_CHAR); } builder.append(attributes.get(i)); } builder.append(QUESTION_CHAR); builder.append(scope); builder.append(QUESTION_CHAR); percentEncoder(filter.toString(), builder); normalizedURL = builder.toString(); } return normalizedURL; } }