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