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