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 2008-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2014-2016 ForgeRock AS.
016 */
017package org.opends.guitools.controlpanel.browser;
018
019import java.util.ArrayList;
020import java.util.HashMap;
021
022import javax.naming.NamingException;
023import javax.naming.ldap.Control;
024import javax.naming.ldap.InitialLdapContext;
025import javax.net.ssl.KeyManager;
026
027import org.opends.admin.ads.util.ApplicationTrustManager;
028import org.opends.admin.ads.util.ConnectionUtils;
029import org.opends.guitools.controlpanel.event.ReferralAuthenticationListener;
030import org.forgerock.opendj.ldap.DN;
031import org.opends.server.types.LDAPURL;
032import org.forgerock.opendj.ldap.SearchScope;
033
034import com.forgerock.opendj.cli.CliConstants;
035
036import static org.opends.admin.ads.util.ConnectionUtils.*;
037
038/**
039 * An LDAPConnectionPool is a pool of LDAPConnection.
040 * <BR><BR>
041 * When a client class needs to access an LDAPUrl, it simply passes
042 * this URL to getConnection() and gets an LDAPConnection back.
043 * When the client has finished with this LDAPConnection, it *must*
044 * pass it releaseConnection() which will take care of its disconnection
045 * or caching.
046 * <BR><BR>
047 * LDAPConnectionPool maintains a pool of authentications. This pool
048 * is populated using registerAuth(). When getConnection() has created
049 * a new connection for accessing a host:port, it looks in the authentication
050 * pool if any authentication is available for this host:port and, if yes,
051 * tries to bind the connection. If no authentication is available, the
052 * returned connection is simply connected (ie anonymous bind).
053 * <BR><BR>
054 * LDAPConnectionPool shares connections and maintains a usage counter
055 * for each connection: two calls to getConnection() with the same URL
056 * will return the same connection. Two calls to releaseConnection() will
057 * be needed to make the connection 'potentially disconnectable'.
058 * <BR><BR>
059 * releaseConnection() does not disconnect systematically a connection
060 * whose usage counter is null. It keeps it connected a while (TODO:
061 * to be implemented).
062 * <BR><BR>
063 * TODO: synchronization is a bit simplistic...
064 */
065public class LDAPConnectionPool {
066
067  private final HashMap<String, AuthRecord> authTable = new HashMap<>();
068  private final HashMap<String, ConnectionRecord> connectionTable = new HashMap<>();
069
070  private ArrayList<ReferralAuthenticationListener> listeners;
071
072  private Control[] requestControls = new Control[] {};
073  private ApplicationTrustManager trustManager;
074  private int connectTimeout = CliConstants.DEFAULT_LDAP_CONNECT_TIMEOUT;
075
076  /**
077   * Returns <CODE>true</CODE> if the connection passed is registered in the
078   * connection pool, <CODE>false</CODE> otherwise.
079   * @param ctx the connection.
080   * @return <CODE>true</CODE> if the connection passed is registered in the
081   * connection pool, <CODE>false</CODE> otherwise.
082   */
083  public boolean isConnectionRegistered(InitialLdapContext ctx) {
084    for (String key : connectionTable.keySet())
085    {
086      ConnectionRecord cr = connectionTable.get(key);
087      if (cr.ctx != null
088          && getHostName(cr.ctx).equals(getHostName(ctx))
089          && getPort(cr.ctx) == getPort(ctx)
090          && getBindDN(cr.ctx).equals(getBindDN(ctx))
091          && getBindPassword(cr.ctx).equals(getBindPassword(ctx))
092          && isSSL(cr.ctx) == isSSL(ctx)
093          && isStartTLS(cr.ctx) == isStartTLS(ctx)) {
094        return true;
095      }
096    }
097    return false;
098  }
099
100  /**
101   * Registers a connection in this connection pool.
102   * @param ctx the connection to be registered.
103   */
104  public void registerConnection(InitialLdapContext ctx) {
105    registerAuth(ctx);
106    LDAPURL url = makeLDAPUrl(ctx);
107    String key = makeKeyFromLDAPUrl(url);
108    ConnectionRecord cr = new ConnectionRecord();
109    cr.ctx = ctx;
110    cr.counter = 1;
111    cr.disconnectAfterUse = false;
112    connectionTable.put(key, cr);
113  }
114
115  /**
116   * Unregisters a connection from this connection pool.
117   * @param ctx the connection to be unregistered.
118   * @throws NamingException if there is a problem unregistering the connection.
119   */
120  public void unregisterConnection(InitialLdapContext ctx)
121  throws NamingException
122  {
123    LDAPURL url = makeLDAPUrl(ctx);
124    unRegisterAuth(url);
125    String key = makeKeyFromLDAPUrl(url);
126    connectionTable.remove(key);
127  }
128
129  /**
130   * Adds a referral authentication listener.
131   * @param listener the referral authentication listener.
132   */
133  public void addReferralAuthenticationListener(
134      ReferralAuthenticationListener listener) {
135    if (listeners == null) {
136      listeners = new ArrayList<>();
137    }
138    listeners.add(listener);
139  }
140
141  /**
142   * Returns an LDAPConnection for accessing the specified url.
143   * If no connection are available for the protocol/host/port
144   * of the URL, getConnection() makes a new one and call connect().
145   * If authentication data available for this protocol/host/port,
146   * getConnection() call bind() on the new connection.
147   * If connect() or bind() failed, getConnection() forward the
148   * NamingException.
149   * When getConnection() succeeds, the returned connection must
150   * be passed to releaseConnection() after use.
151   * @param ldapUrl the LDAP URL to which the connection must connect.
152   * @return a connection to the provided LDAP URL.
153   * @throws NamingException if there was an error connecting.
154   */
155  public InitialLdapContext getConnection(LDAPURL ldapUrl)
156  throws NamingException {
157    String key = makeKeyFromLDAPUrl(ldapUrl);
158    ConnectionRecord cr;
159
160    synchronized(this) {
161      cr = connectionTable.get(key);
162      if (cr == null) {
163        cr = new ConnectionRecord();
164        cr.ctx = null;
165        cr.counter = 1;
166        cr.disconnectAfterUse = false;
167        connectionTable.put(key, cr);
168      }
169      else {
170        cr.counter++;
171      }
172    }
173
174    synchronized(cr) {
175      try {
176        if (cr.ctx == null) {
177          boolean registerAuth = false;
178          AuthRecord authRecord = authTable.get(key);
179          if (authRecord == null)
180          {
181            // Best-effort: try with an already registered authentication
182            authRecord = authTable.values().iterator().next();
183            registerAuth = true;
184          }
185          cr.ctx = createLDAPConnection(ldapUrl, authRecord);
186          cr.ctx.setRequestControls(requestControls);
187          if (registerAuth)
188          {
189            authTable.put(key, authRecord);
190          }
191        }
192      }
193      catch(NamingException x) {
194        synchronized (this) {
195          cr.counter--;
196          if (cr.counter == 0) {
197            connectionTable.remove(key);
198          }
199        }
200        throw x;
201      }
202    }
203
204    return cr.ctx;
205  }
206
207  /**
208   * Sets the request controls to be used by the connections of this connection
209   * pool.
210   * @param ctls the request controls.
211   * @throws NamingException if an error occurs updating the connections.
212   */
213  public synchronized void setRequestControls(Control[] ctls)
214  throws NamingException
215  {
216    requestControls = ctls;
217    for (ConnectionRecord cr : connectionTable.values())
218    {
219      if (cr.ctx != null)
220      {
221        cr.ctx.setRequestControls(requestControls);
222      }
223    }
224  }
225
226
227  /**
228   * Release an LDAPConnection created by getConnection().
229   * The connection should be considered as virtually disconnected
230   * and not be used anymore.
231   * @param ctx the connection to be released.
232   */
233  public synchronized void releaseConnection(InitialLdapContext ctx) {
234
235    String targetKey = null;
236    ConnectionRecord targetRecord = null;
237    synchronized(this) {
238      for (String key : connectionTable.keySet()) {
239        ConnectionRecord cr = connectionTable.get(key);
240        if (cr.ctx == ctx) {
241          targetKey = key;
242          targetRecord = cr;
243          if (targetKey != null)
244          {
245            break;
246          }
247        }
248      }
249    }
250
251    if (targetRecord == null) { // ldc is not in _connectionTable -> bug
252      throw new IllegalArgumentException("Invalid LDAP connection");
253    }
254
255    synchronized (targetRecord)
256    {
257      targetRecord.counter--;
258      if (targetRecord.counter == 0 && targetRecord.disconnectAfterUse)
259      {
260        disconnectAndRemove(targetRecord);
261      }
262    }
263  }
264
265  /**
266   * Register authentication data.
267   * If authentication data are already available for the protocol/host/port
268   * specified in the LDAPURl, they are replaced by the new data.
269   * If true is passed as 'connect' parameter, registerAuth() creates the
270   * connection and attempts to connect() and bind() . If connect() or bind()
271   * fail, registerAuth() forwards the NamingException and does not register
272   * the authentication data.
273   * @param ldapUrl the LDAP URL of the server.
274   * @param dn the bind DN.
275   * @param pw the password.
276   * @param connect whether to connect or not to the server with the
277   * provided authentication (for testing purposes).
278   * @throws NamingException if an error occurs connecting.
279   */
280  private void registerAuth(LDAPURL ldapUrl, String dn, String pw,
281      boolean connect) throws NamingException {
282
283    String key = makeKeyFromLDAPUrl(ldapUrl);
284    final AuthRecord ar = new AuthRecord();
285    ar.dn       = dn;
286    ar.password = pw;
287
288    if (connect) {
289      InitialLdapContext ctx = createLDAPConnection(ldapUrl, ar);
290      ctx.close();
291    }
292
293    synchronized(this) {
294      authTable.put(key, ar);
295      ConnectionRecord cr = connectionTable.get(key);
296      if (cr != null) {
297        if (cr.counter <= 0) {
298          disconnectAndRemove(cr);
299        }
300        else {
301          cr.disconnectAfterUse = true;
302        }
303      }
304    }
305    notifyListeners();
306
307  }
308
309
310  /**
311   * Register authentication data from an existing connection.
312   * This routine recreates the LDAP URL corresponding to
313   * the connection and passes it to registerAuth(LDAPURL).
314   * @param ctx the connection that we retrieve the authentication information
315   * from.
316   */
317  private void registerAuth(InitialLdapContext ctx) {
318    LDAPURL url = makeLDAPUrl(ctx);
319    try {
320      registerAuth(url, getBindDN(ctx), getBindPassword(ctx), false);
321    }
322    catch (NamingException x) {
323      throw new RuntimeException("Bug");
324    }
325  }
326
327
328  /**
329   * Unregister authentication data.
330   * If for the given url there's a connection, try to bind as anonymous.
331   * If unbind fails throw NamingException.
332   * @param ldapUrl the url associated with the authentication to be
333   * unregistered.
334   * @throws NamingException if the unbind fails.
335   */
336  private void unRegisterAuth(LDAPURL ldapUrl) throws NamingException {
337    String key = makeKeyFromLDAPUrl(ldapUrl);
338
339    authTable.remove(key);
340    notifyListeners();
341  }
342
343  /**
344   * Disconnect the connection associated to a record
345   * and remove the record from connectionTable.
346   * @param cr the ConnectionRecord to remove.
347   */
348  private void disconnectAndRemove(ConnectionRecord cr)
349  {
350    String key = makeKeyFromRecord(cr);
351    connectionTable.remove(key);
352    try
353    {
354      cr.ctx.close();
355    }
356    catch (NamingException x)
357    {
358      // Bizarre. However it's not really a problem here.
359    }
360  }
361
362  /**
363   * Notifies the listeners that a referral authentication change happened.
364   *
365   */
366  private void notifyListeners()
367  {
368    for (ReferralAuthenticationListener listener : listeners)
369    {
370      listener.notifyAuthDataChanged();
371    }
372  }
373
374  /**
375   * Make the key string for an LDAP URL.
376   * @param url the LDAP URL.
377   * @return the key to be used in Maps for the provided LDAP URL.
378   */
379  private static String makeKeyFromLDAPUrl(LDAPURL url) {
380    String protocol = isSecureLDAPUrl(url) ? "LDAPS" : "LDAP";
381    return protocol + ":" + url.getHost() + ":" + url.getPort();
382  }
383
384
385  /**
386   * Make the key string for an connection record.
387   * @param rec the connection record.
388   * @return the key to be used in Maps for the provided connection record.
389   */
390  private static String makeKeyFromRecord(ConnectionRecord rec) {
391    String protocol = ConnectionUtils.isSSL(rec.ctx) ? "LDAPS" : "LDAP";
392    return protocol + ":" + getHostName(rec.ctx) + ":" + getPort(rec.ctx);
393  }
394
395  /**
396   * Creates an LDAP Connection for a given LDAP URL and using the
397   * authentication of a AuthRecord.
398   * @param ldapUrl the LDAP URL.
399   * @param ar the authentication information.
400   * @return a connection.
401   * @throws NamingException if an error occurs when connecting.
402   */
403  private InitialLdapContext createLDAPConnection(LDAPURL ldapUrl,
404      AuthRecord ar) throws NamingException
405  {
406    // Take the base DN out of the URL and only keep the protocol, host and port
407    ldapUrl = new LDAPURL(ldapUrl.getScheme(), ldapUrl.getHost(),
408          ldapUrl.getPort(), (DN)null, null, null, null, null);
409
410    if (isSecureLDAPUrl(ldapUrl))
411    {
412      return ConnectionUtils.createLdapsContext(ldapUrl.toString(), ar.dn,
413          ar.password, getConnectTimeout(), null,
414          getTrustManager(), getKeyManager());
415    }
416    return ConnectionUtils.createLdapContext(ldapUrl.toString(), ar.dn,
417        ar.password, getConnectTimeout(), null);
418  }
419
420  /**
421   * Sets the ApplicationTrustManager used by the connection pool to
422   * connect to servers.
423   * @param trustManager the ApplicationTrustManager.
424   */
425  public void setTrustManager(ApplicationTrustManager trustManager)
426  {
427    this.trustManager = trustManager;
428  }
429
430  /**
431   * Returns the ApplicationTrustManager used by the connection pool to
432   * connect to servers.
433   * @return the ApplicationTrustManager used by the connection pool to
434   * connect to servers.
435   */
436  public ApplicationTrustManager getTrustManager()
437  {
438    return trustManager;
439  }
440
441  /**
442   * Returns the timeout to establish the connection in milliseconds.
443   * @return the timeout to establish the connection in milliseconds.
444   */
445  public int getConnectTimeout()
446  {
447    return connectTimeout;
448  }
449
450  /**
451   * Sets the timeout to establish the connection in milliseconds.
452   * Use {@code 0} to express no timeout.
453   * @param connectTimeout the timeout to establish the connection in
454   * milliseconds.
455   * Use {@code 0} to express no timeout.
456   */
457  public void setConnectTimeout(int connectTimeout)
458  {
459    this.connectTimeout = connectTimeout;
460  }
461
462  private KeyManager getKeyManager()
463  {
464//  TODO: we should get it from ControlPanelInfo
465    return null;
466  }
467
468  /**
469   * Returns whether the URL is ldaps URL or not.
470   * @param url the URL.
471   * @return <CODE>true</CODE> if the LDAP URL is secure and <CODE>false</CODE>
472   * otherwise.
473   */
474  private static boolean isSecureLDAPUrl(LDAPURL url) {
475    return !LDAPURL.DEFAULT_SCHEME.equalsIgnoreCase(url.getScheme());
476  }
477
478  private LDAPURL makeLDAPUrl(InitialLdapContext ctx) {
479    return new LDAPURL(
480        isSSL(ctx) ? "ldaps" : LDAPURL.DEFAULT_SCHEME,
481            getHostName(ctx),
482            getPort(ctx),
483            "",
484            null, // no attributes
485            SearchScope.BASE_OBJECT,
486            null, // No filter
487            null); // No extensions
488  }
489
490
491  /**
492   * Make an url from the specified arguments.
493   * @param ctx the connection to the server.
494   * @param dn the base DN of the URL.
495   * @return an LDAP URL from the specified arguments.
496   */
497  public static LDAPURL makeLDAPUrl(InitialLdapContext ctx, String dn) {
498    return new LDAPURL(
499        ConnectionUtils.isSSL(ctx) ? "ldaps" : LDAPURL.DEFAULT_SCHEME,
500               ConnectionUtils.getHostName(ctx),
501               ConnectionUtils.getPort(ctx),
502               dn,
503               null, // No attributes
504               SearchScope.BASE_OBJECT,
505               null,
506               null); // No filter
507  }
508
509
510  /**
511   * Make an url from the specified arguments.
512   * @param url an LDAP URL to use as base of the new LDAP URL.
513   * @param dn the base DN for the new LDAP URL.
514   * @return an LDAP URL from the specified arguments.
515   */
516  public static LDAPURL makeLDAPUrl(LDAPURL url, String dn) {
517    return new LDAPURL(
518        url.getScheme(),
519        url.getHost(),
520        url.getPort(),
521        dn,
522        null, // no attributes
523        SearchScope.BASE_OBJECT,
524        null, // No filter
525        null); // No extensions
526  }
527
528}
529
530/**
531 * A structure representing authentication data.
532 */
533class AuthRecord {
534  String dn;
535  String password;
536}
537
538/**
539 * A structure representing an active connection.
540 */
541class ConnectionRecord {
542  InitialLdapContext ctx;
543  int counter;
544  boolean disconnectAfterUse;
545}