/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * The contents of this file are subject to the Netscape Public * License Version 1.1 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.mozilla.org/NPL/ * * Software distributed under the License is distributed on an "AS * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or * implied. See the License for the specific language governing * rights and limitations under the License. * * The Original Code is mozilla.org code. * * The Initial Developer of the Original Code is Netscape * Communications Corporation. Portions created by Netscape are * Copyright (C) 1999 Netscape Communications Corporation. All * Rights Reserved. * * Contributor(s): */ package netscape.ldap; import java.util.*; import java.io.*; import java.net.MalformedURLException; import netscape.ldap.factory.*; /** * Represents an LDAP URL. The complete specification for LDAP URLs is in * RFC 1959. In addition, the secure ldap (ldaps://) is also * supported. LDAP URLs have the following format: * *
 * "ldap[s]://" [ hostName [":" portNumber] ] "/"
 *                      distinguishedName
 *          ["?" attributeList ["?" scope
 *                      "?" filterString ] ]
 * 
* where *

*

*

* Note that if scope and filterString * are not specified, an LDAP URL identifies exactly one entry in the * directory.

* 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. * * @version 1.0 */ public class LDAPUrl implements java.io.Serializable { static final long serialVersionUID = -3245440798565713640L; public static String defaultFilter = "(objectClass=*)"; private String m_hostName; private int m_portNumber; private String m_DN; private Vector m_attributes; private int m_scope; private String m_filter; private String m_URL; private boolean m_secure; private static LDAPSocketFactory m_factory; /** * The default port number for secure LDAP connections. * @see netscape.ldap.LDAPUrl#LDAPUrl(java.lang.String, int, java.lang.String, java.lang.String[], int, java.lang.String, boolean) */ public static final int DEFAULT_SECURE_PORT = 636; /** * Constructs a URL object with the specified string as URL. * @param url LDAP search expression in URL form * @exception MalformedURLException failed to parse URL */ public LDAPUrl (String url) throws java.net.MalformedURLException { m_attributes = null; m_scope = LDAPv2.SCOPE_BASE; m_filter = defaultFilter; m_URL = url; parseUrl(url); } /** * Parse URL as defined in RFC 1959. Beyond the RFC, the secure ldap * (ldaps) is also supported. */ private void parseUrl(String url) throws MalformedURLException { StringTokenizer urlParser = new StringTokenizer (url, ":/?", true); String currentToken; String str = null; try { currentToken = urlParser.nextToken(); if (currentToken.equalsIgnoreCase ("LDAPS")) { m_secure = true; } else if (!currentToken.equalsIgnoreCase ("LDAP")) { throw new MalformedURLException (); } currentToken = urlParser.nextToken(); if (!currentToken.equals(":")) { throw new MalformedURLException (); } currentToken = urlParser.nextToken(); if (!currentToken.equals("/")) { throw new MalformedURLException (); } currentToken = urlParser.nextToken(); if (!currentToken.equals("/")) { throw new MalformedURLException (); } currentToken = urlParser.nextToken(); } catch (NoSuchElementException e) { throw new MalformedURLException (); } // host-port if (currentToken.equals ("/")) { m_hostName = null; m_portNumber = m_secure ? DEFAULT_SECURE_PORT : LDAPv2.DEFAULT_PORT; } else if (currentToken.equals (":")) { // port number without host name is not allowed throw new MalformedURLException ("No hostname"); } else if (currentToken.equals ("?")) { throw new MalformedURLException ("No host[:port]"); } else { m_hostName = currentToken; if (urlParser.countTokens() == 0) { m_portNumber = m_secure ? DEFAULT_SECURE_PORT : LDAPv2.DEFAULT_PORT; return; } currentToken = urlParser.nextToken (); // either ":" or "/" if (currentToken.equals (":")) { try { m_portNumber = Integer.parseInt (urlParser.nextToken()); } catch (NumberFormatException nf) { throw new MalformedURLException ("Port not a number"); } catch (NoSuchElementException ex) { throw new MalformedURLException ("No port number"); } if (urlParser.countTokens() == 0) { return; } else if (! urlParser.nextToken().equals("/")) { throw new MalformedURLException (); } } else if (currentToken.equals ("/")) { m_portNumber = m_secure ? DEFAULT_SECURE_PORT : LDAPv2.DEFAULT_PORT; } else { // expecting ":" or "/" throw new MalformedURLException (); } } // DN if (!urlParser.hasMoreTokens ()) { return; } m_DN = decode(readNextConstruct(urlParser)); if (m_DN.equals("?")) { m_DN = ""; } else if (m_DN.equals("/")) { throw new MalformedURLException (); } // attribute if (!urlParser.hasMoreTokens ()) { return; } str = readNextConstruct(urlParser); if (!str.equals("?")) { StringTokenizer attributeParser = new StringTokenizer (decode(str), ", "); m_attributes = new Vector (); while (attributeParser.hasMoreTokens()) { m_attributes.addElement (attributeParser.nextToken()); } } // scope if (!urlParser.hasMoreTokens ()) { return; } str = readNextConstruct(urlParser); if (!str.equals("?")) { m_scope = getScope(str); if (m_scope < 0) { throw new MalformedURLException("Bad scope:" + str); } } // filter if (!urlParser.hasMoreTokens ()) { return; } str = readNextConstruct(urlParser); m_filter = decode(str); checkBalancedParentheses(m_filter); if (!m_filter.startsWith("(") && !m_filter.endsWith(")")) { m_filter = "(" + m_filter + ")"; } // Nothing after the filter is allowed if (urlParser.hasMoreTokens()) { throw new MalformedURLException(); } } private void checkBalancedParentheses(String filter) throws MalformedURLException { int parenCnt =0; StringTokenizer filterParser = new StringTokenizer (filter, "()", true); while (filterParser.hasMoreElements()) { String token = filterParser.nextToken(); if (token.equals("(")) { parenCnt++; } else if (token.equals(")")) { if (--parenCnt < 0) { throw new MalformedURLException("Unbalanced filter parentheses"); } } } if (parenCnt != 0) { throw new MalformedURLException("Unbalanced filter parentheses"); } } /** * Constructs with the specified host, port, and DN. This form is used to * create URL references to a particular object in the directory. * @param host host name of the LDAP server, or null for "nearest X.500/LDAP" * @param port port number of the LDAP server (use LDAPv2.DEFAULT_PORT for * the default port) * @param DN distinguished name of the object */ public LDAPUrl (String host, int port, String DN) { initialize(host, port, DN, null, LDAPv2.SCOPE_BASE, defaultFilter, false); } /** * Constructs a full-blown LDAP URL to specify an LDAP search operation. * @param host host name of the LDAP server, or null for "nearest X.500/LDAP" * @param port port number of the LDAP server (use LDAPv2.DEFAULT_PORT for * the default port) * @param DN distinguished name of the object * @param attributes list of attributes to return. Use null for "all * attributes." * @param scope depth of search (in DN namespace). Use one of the LDAPv2 scopes: * SCOPE_BASE, SCOPE_ONE, or SCOPE_SUB. * @param filter LDAP filter string (as defined in RFC 1558). Use null for * no filter (this effectively makes the URL reference a single object). */ public LDAPUrl (String host, int port, String DN, String attributes[], int scope, String filter) { this(host, port, DN, attributes, scope, filter, false); } /** * Constructs a full-blown LDAP URL to specify an LDAP search operation. * @param host host name of the LDAP server, or null for "nearest X.500/LDAP" * @param port port number of the LDAP server (use LDAPv2.DEFAULT_PORT for * the default port) * @param DN distinguished name of the object * @param attributes list of the attributes to return. Use null for "all * attributes." * @param scope depth of the search (in DN namespace). Use one of the LDAPv2 scopes: * SCOPE_BASE, SCOPE_ONE, or SCOPE_SUB. * @param filter LDAP filter string (as defined in RFC 1558). Use null for * no filter (this effectively makes the URL reference a single object). */ public LDAPUrl (String host, int port, String DN, Enumeration attributes, int scope, String filter) { initialize(host, port, DN, attributes, scope, filter, false); } /** * Constructs a full-blown LDAP URL to specify an LDAP search operation. * @param host host name of the LDAP server, or null for "nearest X.500/LDAP" * @param port port number of the LDAP server (use LDAPv2.DEFAULT_PORT for * the default non-secure port or LDAPUrl.DEFAULT_SECURE_PORT for the default * secure port) * @param DN distinguished name of the object * @param attributes list of the attributes to return. Use null for "all * attributes." * @param scope depth of the search (in DN namespace). Use one of the LDAPv2 scopes: * SCOPE_BASE, SCOPE_ONE, or SCOPE_SUB. * @param filter LDAP filter string (as defined in RFC 1558). Use null for * no filter (this effectively makes the URL reference a single object). * @param secure flag if secure ldap protocol (ldaps) is to be used. */ public LDAPUrl (String host, int port, String DN, String[] attributes, int scope, String filter, boolean secure) { if (attributes != null) { Vector list = new Vector(); for (int k = 0; k < attributes.length; k++) { list.addElement(attributes[k]); } initialize(host, port, DN, list.elements(), scope, filter, secure); } else { initialize(host, port, DN, null, scope, filter, secure); } } /** * Initializes URL object. */ private void initialize (String host, int port, String DN, Enumeration attributes, int scope, String filter, boolean secure) { m_hostName = host; m_DN = DN; m_portNumber = port; m_filter = (filter != null) ? filter : defaultFilter; m_scope = scope; m_secure = secure; if (attributes != null) { m_attributes = new Vector (); while (attributes.hasMoreElements()) { m_attributes.addElement (attributes.nextElement()); } } else m_attributes = null; StringBuffer url = new StringBuffer (secure ? "LDAPS://" :"LDAP://"); if (host != null) { url.append (host); url.append (':'); url.append (String.valueOf (port)); } url.append ('/'); url.append (encode (DN)); if (attributes != null) { url.append ('?'); Enumeration attrList = m_attributes.elements(); boolean firstElement = true; while (attrList.hasMoreElements()) { if (!firstElement) url.append (','); else firstElement = false; url.append ((String)attrList.nextElement()); } } if (filter != null) { if (attributes == null) url.append ('?'); url.append ('?'); switch (scope) { default: case LDAPv2.SCOPE_BASE: url.append ("base"); break; case LDAPv2.SCOPE_ONE: url.append ("one"); break; case LDAPv2.SCOPE_SUB: url.append ("sub"); break; } url.append ('?'); url.append (filter); } m_URL = url.toString(); } /** * Return the host name of the LDAP server * @return LDAP host. */ public String getHost () { return m_hostName; } /** * Return the port number for the LDAP server * @return port number. */ public int getPort () { return m_portNumber; } /** * Return the distinguished name encapsulated in the URL * @return target distinguished name. */ public String getDN () { return m_DN; } /** * Return the collection of attributes specified in the URL, or null * for "every attribute" * @return enumeration of attributes. */ public Enumeration getAttributes () { if (m_attributes == null) return null; else return m_attributes.elements(); } /** * Return the collection of attributes specified in the URL, or null * for "every attribute" * @return string array of attributes. */ public String[] getAttributeArray () { if (m_attributes == null) return null; else { String[] attrNames = new String[m_attributes.size()]; Enumeration attrs = getAttributes(); int i = 0; while (attrs.hasMoreElements()) attrNames[i++] = (String)attrs.nextElement(); return attrNames; } } /** * Returns the scope of the search, according to the values * SCOPE_BASE, SCOPE_ONE, SCOPE_SUB as defined in LDAPv2. This refers * to how deep within the directory namespace the search will look * @return search scope. */ public int getScope () { return m_scope; } /** * Returns the scope of the search. If the scope returned is -1, then * the given string is not for the scope. * @param str the string against which to compare the scope type * @returns the scope of the search, -1 is returned if the given string is * not SUB, ONE or BASE (the acceptable LDAPv2 values for scope). */ private int getScope(String str) { int s = -1; if (str.equalsIgnoreCase("base")) s = LDAPv2.SCOPE_BASE; else if (str.equalsIgnoreCase("one")) s = LDAPv2.SCOPE_ONE; else if (str.equalsIgnoreCase("sub")) s = LDAPv2.SCOPE_SUB; return s; } /** * Returns the search filter (RFC 1558), or the default if none was * specified. * @return the search filter. */ public String getFilter () { return m_filter; } /** * Returns a valid string representation of this LDAP URL. * @return the LDAP search expression in URL form. */ public String getUrl () { return m_URL; } /** * Returns true if the secure ldap protocol is used. * @return true if ldaps is used. */ public boolean isSecure() { return m_secure; } /** * Gets the socket factory to be used for ldaps:// URLs. *

* If the factory is not explicitly specified with * LDAPUrl.setSocketFactory, the method will * attempt the determine the default factory based on the * available factories in the netscape.ldap.factory package. * * @return the socket factory to be used for ldaps:// URLs */ public static LDAPSocketFactory getSocketFactory() { if (m_factory == null) { // No factory explicity set, try to determine // the default one. try { // First try iPlanet JSSSocketFactory m_factory = new JSSSocketFactory(); } catch (Throwable e) { } if (m_factory != null) { return m_factory; } try { // then try Sun JSSESocketFactory m_factory = new JSSESocketFactory(null); } catch (Throwable e) { } } return m_factory; } /** * Sets the socket factory to be used for ldaps:// URLs. * Overrides the default factory assigned by the LDAPUrl * class. * @param the socket factory to be used for ldaps:// URLs * @see netscape.ldap.LDAPUrl#getSocketFactory */ public static void setSocketFactory(LDAPSocketFactory factory) { m_factory = factory; } /** * Reads next construct from the given string parser. * @param parser the string parser * @return the next construct which can be an attribute, scope or filter. * @exception java.net.MalformedURLException Get thrown when the url format * is incorrect. */ private String readNextConstruct(StringTokenizer parser) throws MalformedURLException { try { if (parser.hasMoreTokens()) { String tkn = parser.nextToken(); if (tkn.equals("?")) { // empty construct return tkn; } else if (parser.hasMoreTokens()){ // Remove '?' delimiter String delim = parser.nextToken(); if (!delim.equals("?")) { throw new MalformedURLException(); } } return tkn; } } catch (NoSuchElementException e) { throw new MalformedURLException(); } return null; } /** * Parses hex character into integer. */ private static int hexValue (char hexChar) throws MalformedURLException { if (hexChar >= '0' && hexChar <= '9') return hexChar - '0'; if (hexChar >= 'A' && hexChar <= 'F') return hexChar - 'A' + 10; if (hexChar >= 'a' && hexChar <= 'f') return hexChar - 'a' + 10; throw new MalformedURLException (); } private static char hexChar (int hexValue) { if (hexValue < 0 || hexValue > 0xF) return 'x'; if (hexValue < 10) return (char)(hexValue + '0'); return (char)((hexValue - 10) + 'a'); } /** * Decodes a URL-encoded string. Any occurences of %HH are decoded to the * hex value represented. However, this routine does NOT decode "+" * into " ". See RFC 1738 for full details about URL encoding/decoding. * @param URLEncoded a segment of a URL which was encoded using the URL * encoding rules * @exception MalformedURLException failed to parse URL */ public static String decode (String URLEncoded) throws MalformedURLException { StringBuffer decoded = new StringBuffer (URLEncoded); int srcPos = 0, dstPos = 0; try { while (srcPos < decoded.length()) { if (decoded.charAt (srcPos) != '%') { if (srcPos != dstPos) decoded.setCharAt (dstPos, decoded.charAt (srcPos)); srcPos++; dstPos++; continue; } decoded.setCharAt (dstPos, (char) ((hexValue(decoded.charAt (srcPos+1))<<4) | (hexValue(decoded.charAt (srcPos+2))))); dstPos++; srcPos += 3; } } catch (StringIndexOutOfBoundsException sioob) { // Indicates that a "%" character occured without the following HH throw new MalformedURLException (); } /* 070497 Url problems submitted by Netscape */ /* decoded.setLength (dstPos+1); */ decoded.setLength (dstPos); return decoded.toString (); } /** * Encodes an arbitrary string. Any illegal characters are encoded as * %HH. However, this routine does NOT decode "+" into " " (this is a HTTP * thing, not a general URL thing). Note that, because Sun's URLEncoder * does do this encoding, we can't use it. * See RFC 1738 for full details about URL encoding/decoding. * @param toEncode an arbitrary string to encode for embedding within a URL */ public static String encode (String toEncode) { StringBuffer encoded = new StringBuffer (toEncode.length()+10); for (int currPos = 0; currPos < toEncode.length(); currPos++) { char currChar = toEncode.charAt (currPos); if ((currChar >= 'a' && currChar <= 'z') || (currChar >= 'A' && currChar <= 'Z') || (currChar >= '0' && currChar <= '9') || ("$-_.+!*'(),".indexOf (currChar) > 0)) { // this is the criteria for "doesn't need to be encoded" (whew!) encoded.append (currChar); } else { encoded.append ("%"); encoded.append (hexChar ((currChar & 0xF0) >> 4)); encoded.append (hexChar (currChar & 0x0F)); } } return encoded.toString(); } /** * Returns the URL in String format * * @return the URL in String format */ public String toString() { return getUrl(); } /** * Reports if the two objects represent the same URL * * @param url the object to be compared to * @return true if the two are equivalent */ public boolean equals( LDAPUrl url ) { if ( getHost() == null ) { if ( url.getHost() != null ) { return false; } } else if ( !getHost().equals( url.getHost() ) ) { return false; } if ( getPort() != url.getPort() ) { return false; } if ( getDN() == null ) { if ( url.getDN() != null ) { return false; } } else if ( !getDN().equals( url.getDN() ) ) { return false; } if ( getFilter() == null ) { if ( url.getFilter() != null ) { return false; } } else if ( !getFilter().equals( url.getFilter() ) ) { return false; } if ( getScope() != url.getScope() ) { return false; } if ( m_attributes == null ) { if ( url.m_attributes != null ) { return false; } } else if ( m_attributes.size() != url.m_attributes.size() ) { return false; } else { for( int i = 0; i < m_attributes.size(); i++ ) { if ( m_attributes.elementAt( i ) != url.m_attributes.elementAt( i ) ) { return false; } } } return true; } }