/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla 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/MPL/ * * 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 the Initial Developer are Copyright (C) 1999 * the Initial Developer. All Rights Reserved. * * Contributor(s): * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ package netscape.ldap; import java.util.*; import java.io.*; import java.net.*; /** * Makes a connection to a server from a list using "smart" failover. * Connection attempts can be made serially from the same thread, or * in parallel by creating a separate thread after the specified delay. * Connection setup status is preserved for later attempts, so that servers * that are more likely to be available will be tried first. *

* The total time spent opening a connection can be limited with the * ConnectTimeout property. *

* When a connection is successfully created, a socket is opened. The socket * is passed to the LDAPConnThread. The LDAPConnThread must call * invalidateConnection() if the connection is lost due to a network or * server error, or closeConnection() if the connection is deliberately terminated * by the user. */ class LDAPConnSetupMgr implements java.io.Serializable { static final long serialVersionUID = 1519402748245755307L; /** * Policy for opening a connection when multiple servers are used */ private static final int SERIAL = 0; private static final int PARALLEL = 1; /** * ServerEntry.connSetupStatus possible value. The values also represent * the likelihood that the connection will be setup to a server. Lower * values have higher priority. See sortDsList() method */ private static final int CONNECTED = 0; private static final int DISCONNECTED = 1; private static final int NEVER_USED = 2; private static final int INTERRUPTED = 3; private static final int FAILED = 4; /** * Representation for a server in the server list. */ class ServerEntry { LDAPUrl url; int connSetupStatus; Thread connSetupThread; ServerEntry(LDAPUrl url, int status) { this.url = url; connSetupStatus = status; connSetupThread = null; } public String toString() { return "{" + url + " status="+connSetupStatus+"}"; } } /** * Socket to the connected server */ private Socket m_socket = null; /** * Original, underlying socket to the server, see layerSocket() */ private Socket m_origSocket = null; /** * Last exception occured during connection setup */ private LDAPException m_connException = null; /** * List of server to use for the connection setup */ ServerEntry[] m_dsList; /** * Index of the last connected server */ private int m_dsIdx = -1; /** * Socket factory for SSL connections */ LDAPSocketFactory m_factory; /** * Connection setup policy (PARALLEL or SERIAL) */ int m_policy = SERIAL; /** * Delay in ms before another connection setup thread is started. */ int m_connSetupDelay = -1; /** * The maximum time to wait to established the connection */ int m_connectTimeout = 0; /** * During connection setup, the current count of servers to which * connection attmpt has been made */ private transient int m_attemptCnt = 0; /** * Constructor * @param host list of host names to which to connect * @param port list of port numbers corresponding to the host list * @param factory socket factory for SSL connections */ LDAPConnSetupMgr(String[] hosts, int[] ports, LDAPSocketFactory factory) throws LDAPException{ m_dsList = new ServerEntry[hosts.length]; boolean secure = (factory != null); for (int i=0; i < hosts.length; i++) { String url = secure ? "ldaps://" : "ldap://"; url += hosts[i] + ":" + ports[i]; try { m_dsList[i] = new ServerEntry(new LDAPUrl(url), NEVER_USED); } catch (MalformedURLException ex) { throw new LDAPException("Invalid host:port " + hosts[i]+":"+ports[i], LDAPException.PARAM_ERROR); } } m_factory = factory; } LDAPConnSetupMgr(String[] urls, LDAPSocketFactory factory) throws LDAPException{ m_dsList = new ServerEntry[urls.length]; for (int i=0; i < urls.length; i++) { try { LDAPUrl url = new LDAPUrl(urls[i]); m_dsList[i] = new ServerEntry(url, NEVER_USED); } catch (MalformedURLException ex) { throw new LDAPException("Malformed LDAP URL " + urls[i], LDAPException.PARAM_ERROR); } } m_factory = factory; } LDAPConnSetupMgr(LDAPUrl[] urls, LDAPSocketFactory factory) throws LDAPException{ m_dsList = new ServerEntry[urls.length]; for (int i=0; i < urls.length; i++) { m_dsList[i] = new ServerEntry(urls[i], NEVER_USED); } m_factory = factory; } /** * Try to open the connection to any of the servers in the list, limiting * the time waiting for the connection to be established * @return connection socket */ synchronized Socket openConnection() throws LDAPException{ long tcur=0, tmax = Long.MAX_VALUE; Thread th = null; reset(); // If reconnecting, sort dsList so that servers more likly to // be available are tried first sortDsList(); if (m_connectTimeout == 0) { // No need for a separate thread, connect time not limited connect(); } else { // Wait for connection at most m_connectTimeout milliseconds // Run connection setup in a separate thread to monitor the time tmax = System.currentTimeMillis() + m_connectTimeout; th = new Thread (new Runnable() { public void run() { connect(); } }, "ConnSetupMgr"); th.setDaemon(true); th.start(); while (m_socket==null && (m_attemptCnt < m_dsList.length) && (tcur = System.currentTimeMillis()) < tmax) { try { wait(tmax - tcur); } catch (InterruptedException e) { th.interrupt(); cleanup(); throw new LDAPInterruptedException("Interrupted connect operation"); } } } if (m_socket != null) { return m_socket; } if ( th != null && (tcur = System.currentTimeMillis()) >= tmax) { // We have timed out th.interrupt(); cleanup(); throw new LDAPException( "Connect timeout, " + getServerList() + " might be unreachable", LDAPException.CONNECT_ERROR); } if (m_connException != null && m_dsList.length == 1) { throw m_connException; } throw new LDAPException( "Failed to connect to server " + getServerList(), LDAPException.CONNECT_ERROR); } private void reset() { m_socket = null; m_origSocket = null; m_connException = null; m_attemptCnt = 0; for (int i=0; i < m_dsList.length; i++) { m_dsList[i].connSetupThread = null; } } private String getServerList() { StringBuffer sb = new StringBuffer(); for (int i=0; i < m_dsList.length; i++) { sb.append(i==0 ? "" : " "); sb.append(m_dsList[i].url.getHost()); sb.append(":"); sb.append(m_dsList[i].url.getPort()); } return sb.toString(); } private void connect() { if (m_policy == SERIAL || m_dsList.length == 1) { openSerial(); } else { openParallel(); } } /** * Called when the current connection is lost. * Put the connected server at the end of the server list for * the next connect attempt. */ synchronized void invalidateConnection() { if (m_socket != null) { m_dsList[m_dsIdx].connSetupStatus = FAILED; // Move the entry to the end of the list int srvCnt = m_dsList.length, j=0; ServerEntry[] newDsList = new ServerEntry[m_dsList.length]; for (int i=0; i < srvCnt; i++) { if (i != m_dsIdx) { newDsList[j++] = m_dsList[i]; } } newDsList[j] = m_dsList[m_dsIdx]; m_dsList = newDsList; m_dsIdx = j; try { m_socket.close(); } catch (Exception e) { } finally { m_socket = null; } } if (m_origSocket != null) { try { m_origSocket.close(); } catch (Exception e) { } finally { m_origSocket = null; } } } /** * Called when the current connection is terminated by the user. * Mark the connected server status as DISCONNECTED. This will * put it at top of the server list for the next connect attempt. */ void closeConnection() { if (m_socket != null) { m_dsList[m_dsIdx].connSetupStatus = DISCONNECTED; try { m_socket.close(); } catch (Exception e) { } finally { m_socket = null; } } if (m_origSocket != null) { try { m_origSocket.close(); } catch (Exception e) { } finally { m_origSocket = null; } } } Socket getSocket() { return m_socket; } /** * Layer a new socket over the existing one (used by startTLS) */ void layerSocket(LDAPTLSSocketFactory factory) throws LDAPException{ Socket s = factory.makeSocket(m_socket); m_origSocket = m_socket; m_socket = s; } String getHost() { if (m_dsIdx >= 0) { return m_dsList[m_dsIdx].url.getHost(); } return m_dsList[0].url.getHost(); } int getPort() { if (m_dsIdx >= 0) { return m_dsList[m_dsIdx].url.getPort(); } return m_dsList[0].url.getPort(); } boolean isSecure() { if (m_dsIdx >= 0) { return m_dsList[m_dsIdx].url.isSecure(); } return m_dsList[0].url.isSecure(); } LDAPUrl getLDAPUrl() { if (m_dsIdx >= 0) { return m_dsList[m_dsIdx].url; } return m_dsList[0].url; } int getConnSetupDelay() { return m_connSetupDelay/1000; } /** * Selects the connection failover policy * @param delay in seconds for the parallel connection setup policy. * Possible values are:
(delay=-1) use serial policy,
* (delay=0) start immediately concurrent threads to each specified server *
(delay>0) create a new connection setup thread after delay seconds */ void setConnSetupDelay(int delay) { m_policy = (delay < 0) ? SERIAL : PARALLEL; m_connSetupDelay = delay*1000; } int getConnectTimeout() { return m_connectTimeout/1000; } /** * Sets the maximum time to spend in the openConnection() call * @param timeout in seconds to wait for the connection to be established */ void setConnectTimeout(int timeout) { m_connectTimeout = timeout*1000; } /** * Check if the user has voluntarily closed the connection */ boolean isUserDisconnected() { return (m_dsIdx >=0 && m_dsList[m_dsIdx].connSetupStatus == DISCONNECTED); } /** * Try sequentially to open a new connection to a server. */ private void openSerial() { for (int i=0; i < m_dsList.length; i++) { m_dsList[i].connSetupThread = Thread.currentThread(); connectServer(i); if (m_socket != null) { return; } } } /** * Try concurrently to open a new connection a server. Create a separate * thread for each connection attempt. */ private synchronized void openParallel() { for (int i=0; m_socket==null && i < m_dsList.length; i++) { //Create a Thread to execute connectSetver() final int dsIdx = i; String threadName = "ConnSetupMgr " + m_dsList[dsIdx].url; Thread t = new Thread(new Runnable() { public void run() { connectServer(dsIdx); } }, threadName); m_dsList[dsIdx].connSetupThread = t; t.setDaemon(true); t.start(); // Wait before starting another thread if the delay is not zero if (m_connSetupDelay != 0 && i < (m_dsList.length-1)) { try { wait(m_connSetupDelay); } catch (InterruptedException e) { return; } } } // At this point all threads are started. Wait until first thread // succeeds to connect or all threads terminate while (m_socket == null && (m_attemptCnt < m_dsList.length)) { // Wait for a thread to terminate try { wait(); } catch (InterruptedException e) {} } } /** * Connect to the server at the given index */ void connectServer(int idx) { ServerEntry entry = m_dsList[idx]; Thread currThread = Thread.currentThread(); Socket sock = null; LDAPException conex = null; try { /* If we are to create a socket ourselves, make sure it has sufficient privileges to connect to the desired host */ if (!entry.url.isSecure()) { sock = new Socket (entry.url.getHost(), entry.url.getPort()); } else { LDAPSocketFactory factory = m_factory; if (factory == null) { factory = entry.url.getSocketFactory(); } if (factory == null) { throw new LDAPException("Can not connect, no socket factory " + entry.url, LDAPException.OTHER); } sock = factory.makeSocket(entry.url.getHost(), entry.url.getPort()); } sock.setTcpNoDelay( true ); } catch (IOException e) { conex = new LDAPException("failed to connect to server " + entry.url, LDAPException.CONNECT_ERROR); } catch (LDAPException e) { conex = e; } if (currThread.isInterrupted()) { return; } synchronized (this) { if (m_socket == null && entry.connSetupThread == currThread) { entry.connSetupThread = null; if (sock != null) { entry.connSetupStatus = CONNECTED; m_socket = sock; m_dsIdx = idx; cleanup(); // Signal other concurrent threads to terminate } else { entry.connSetupStatus = FAILED; m_connException = conex; } m_attemptCnt++; notifyAll(); } } } /** * Terminate all concurrently running connection setup threads */ private synchronized void cleanup() { Thread currThread = Thread.currentThread(); for (int i=0; i < m_dsList.length; i++) { ServerEntry entry = m_dsList[i]; if (entry.connSetupThread != null && entry.connSetupThread != currThread) { entry.connSetupStatus = INTERRUPTED; //Thread.stop() is considered to be dangerous, use Thread.interrupt(). //interrupt() will however not work if the thread is blocked in the //socket library native connect() call, but the connect() will //eventually timeout and the thread will die. entry.connSetupThread.interrupt(); entry.connSetupThread = null; } } } /** * Sorts Server List so that servers which are more likely to be available * are tried first. The likelihood of making a successful connection * is determined by the connSetupStatus. Lower values have higher * likelihood. Thus, the order of server access is (1) disconnected by * the user (2) never used (3) interrupted connection attempt * (4) connection setup failed/connection lost */ private void sortDsList() { int srvCnt = m_dsList.length; for (int i=1; i < srvCnt; i++) { for (int j=0; j < i; j++) { if (m_dsList[i].connSetupStatus < m_dsList[j].connSetupStatus) { // swap entries ServerEntry entry = m_dsList[j]; m_dsList[j] = m_dsList[i]; m_dsList[i] = entry; } } } } /** * This is used only by the ldapjdk test libaray to simulate a * server problem and to test fail-over and rebind * @return A flag whether the connection was closed */ boolean breakConnection() { try { m_socket.close(); return true; } catch (Exception e) { return false; } } public String toString() { String str = "dsIdx="+m_dsIdx+ " dsList="; for (int i=0; i < m_dsList.length; i++) { str += m_dsList[i]+ " "; } return str; } }