/*
* Copyright 2008-2010 UnboundID Corp.
* All Rights Reserved.
*/
/*
* Sun Public License
*
* The contents of this file are subject to the Sun Public License Version
* 1.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is available at http://www.sun.com/
*
* The Original Code is the SLAMD Distributed Load Generation Engine.
* The Initial Developer of the Original Code is Neil A. Wilson.
* Portions created by Neil A. Wilson are Copyright (C) 2008-2010.
* All Rights Reserved.
*
* Contributor(s): Neil A. Wilson
*/
package com.slamd.jobs;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import com.slamd.job.JobClass;
import com.slamd.job.UnableToRunException;
import com.slamd.parameter.InvalidValueException;
import com.slamd.parameter.MultiChoiceParameter;
import com.slamd.parameter.MultiLineTextParameter;
import com.slamd.parameter.Parameter;
import com.slamd.parameter.ParameterList;
import com.slamd.parameter.PasswordParameter;
import com.slamd.parameter.PlaceholderParameter;
import com.slamd.parameter.StringParameter;
import com.unboundid.ldap.sdk.BindRequest;
import com.unboundid.ldap.sdk.ExtendedResult;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.LDAPConnectionPool;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.RoundRobinServerSet;
import com.unboundid.ldap.sdk.SimpleBindRequest;
import com.unboundid.ldap.sdk.StartTLSPostConnectProcessor;
import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
import com.unboundid.util.ssl.SSLUtil;
import com.unboundid.util.ssl.TrustAllTrustManager;
/**
* This class provides an API that should be extended by jobs which communicate
* with one or more LDAP directory server instances. It supports load-balancing
* requests against multiple Directory Server addresses/ports, SSL or StartTLS
* communication, and authentication, and it provides the ability to establish
* connections using the specified properties.
*/
public abstract class LDAPJobClass
extends JobClass
{
/**
* The security method string that indicates that no security should be used.
*/
private static final String SECURITY_METHOD_NONE = "None";
/**
* The security method string that indicates that communication should be
* protected with SSL.
*/
private static final String SECURITY_METHOD_SSL = "SSL";
/**
* The security method string that indicates that communication should be
* protected with StartTLS.
*/
private static final String SECURITY_METHOD_STARTTLS = "StartTLS";
/**
* The array of possible security methods.
*/
private static final String[] SECURITY_METHODS =
{
SECURITY_METHOD_NONE,
SECURITY_METHOD_SSL,
SECURITY_METHOD_STARTTLS
};
// Variables used to connect to the target server.
private static boolean useStartTLS;
private static SSLContext sslContext;
private static String bindDN;
private static String bindPW;
// The server set that will be used to create the connections.
private static RoundRobinServerSet serverSet;
// The parameters used to provide information about the connections.
private MultiLineTextParameter addressesParameter =
new MultiLineTextParameter("addresses", "Server Addresses",
"The addresses (or addresses followed by colons and and port " +
"numbers) of the directory servers to which connections " +
"should be established. If multiple addresses are " +
"given, then they should be provided on separate lines, " +
"and clients will load-balance between those servers on " +
"a round-robin manner.",
null, true);
private MultiChoiceParameter securityMethodParameter =
new MultiChoiceParameter("securityMethod", "Security Method",
"The type of security (if any) that should be used to " +
"protect communication with the directory server.",
SECURITY_METHODS, SECURITY_METHOD_NONE);
private StringParameter bindDNParameter =
new StringParameter("bindDN", "Bind DN",
"The DN to use to authenticate to the directory server. If " +
"no value is provided then no authentication will be " +
"performed.",
false, "");
private PasswordParameter bindPWParameter =
new PasswordParameter("bindPW", "Bind Password",
"The password to use to authenticate to the directory " +
"server. If no value is provided then no authentication " +
"will be performed.",
false, "");
/**
* Creates a new instance of this job class.
*/
protected LDAPJobClass()
{
super();
}
/**
* {@inheritDoc}
*/
@Override()
public String getJobCategoryName()
{
return "LDAP";
}
/**
* {@inheritDoc}
*/
@Override()
public final ParameterList getParameterStubs()
{
ArrayList<Parameter> params = new ArrayList<Parameter>();
params.add(new PlaceholderParameter());
params.add(addressesParameter);
params.add(securityMethodParameter);
params.add(bindDNParameter);
params.add(bindPWParameter);
List<Parameter> nonLDAPStubs = getNonLDAPParameterStubs();
if (nonLDAPStubs != null)
{
params.addAll(nonLDAPStubs);
}
Parameter[] paramArray = new Parameter[params.size()];
return new ParameterList(params.toArray(paramArray));
}
/**
* Retrieves the list of parameters needed by this job that are not needed to
* connect or authenticate to the target server.
*
* @return The list of parameters needed by this job that are not needed to
* connect or authenticate to the target server.
*/
protected List<Parameter> getNonLDAPParameterStubs()
{
// No additional parameters are required by default.
return Collections.emptyList();
}
/**
* {@inheritDoc}
*/
@Override()
public void validateJobInfo(int numClients, int threadsPerClient,
int threadStartupDelay, Date startTime,
Date stopTime, int duration,
int collectionInterval, ParameterList parameters)
throws InvalidValueException
{
String[] addrs;
MultiLineTextParameter addressesParam =
parameters.getMultiLineTextParameter(addressesParameter.getName());
if ((addressesParam == null) || (! addressesParam.hasValue()) ||
((addrs = addressesParam.getNonBlankLines()).length == 0))
{
throw new InvalidValueException("No addresses were provided.");
}
for (String a : addrs)
{
int colonPos = a.indexOf(':');
if (colonPos == 0)
{
throw new InvalidValueException("Address '" + a +
"' does not have a hostname");
}
else if (colonPos > 0)
{
try
{
int portNumber = Integer.parseInt(a.substring(colonPos+1));
if ((portNumber < 1) || (portNumber > 65535))
{
throw new InvalidValueException("Address '" + a +
"' has an invalid port number (it must be between 1 and " +
"65535)");
}
}
catch (Exception e)
{
throw new InvalidValueException("The value after the colon in " +
"address '" + a + "' cannot be parsed as an integer port number",
e);
}
}
}
StringParameter bindDNParam =
parameters.getStringParameter(bindDNParameter.getName());
boolean bindDNProvided = ((bindDNParam != null) && bindDNParam.hasValue());
PasswordParameter bindPWParam =
parameters.getPasswordParameter(bindPWParameter.getName());
boolean bindPWProvided = ((bindPWParam != null) && bindPWParam.hasValue());
if (bindDNProvided && (! bindPWProvided))
{
throw new InvalidValueException("If a bind DN is provided, then a " +
"bind password must also be provided.");
}
if (bindPWProvided && (! bindDNProvided))
{
throw new InvalidValueException("If a bind password is provided, then " +
"a bind DN must also be provided.");
}
validateNonLDAPJobInfo(numClients, threadsPerClient, threadStartupDelay,
startTime, stopTime, duration, collectionInterval, parameters);
}
/**
* Performs any validation that may be necessary for the provided job
* information.
*
* @param numClients The number of clients on which the job should
* run.
* @param threadsPerClient The number of threads per client.
* @param threadStartupDelay The length of time in milliseconds between
* each thread to be started.
* @param startTime The time that the job should start running, or
* {@code null} if none was specified.
* @param stopTime The time that the job should stop running, or
* {@code null} if none was specified.
* @param duration The maximum length of time in seconds that the
* job should run.
* @param collectionInterval The statistics collection interval in seconds.
* @param parameters The set of parameters for the job.
*
* @throws InvalidValueException If any of the provided information is
* invalid for the job.
*/
protected void validateNonLDAPJobInfo(final int numClients,
final int threadsPerClient,
final int threadStartupDelay,
final Date startTime,
final Date stopTime,
final int duration,
final int collectionInterval,
final ParameterList parameters)
throws InvalidValueException
{
// No validation is required by default.
}
/**
* {@inheritDoc}
*/
@Override()
public final boolean providesParameterTest()
{
return true;
}
/**
* {@inheritDoc}
*/
@Override()
public final boolean testJobParameters(final ParameterList parameters,
final ArrayList<String> outputMessages)
{
MultiLineTextParameter addressesParam =
parameters.getMultiLineTextParameter(addressesParameter.getName());
ArrayList<String> addrList = new ArrayList<String>();
ArrayList<Integer> portList = new ArrayList<Integer>();
for (String a : addressesParam.getNonBlankLines())
{
int colonPos = a.indexOf(':');
if (colonPos > 0)
{
addrList.add(a.substring(0, colonPos));
portList.add(Integer.parseInt(a.substring(colonPos+1)));
}
else
{
addrList.add(a);
portList.add(389);
}
}
boolean ssl = false;
boolean startTLS = false;
MultiChoiceParameter secMethodParam =
parameters.getMultiChoiceParameter(securityMethodParameter.getName());
if (secMethodParam.getValueString().equals(SECURITY_METHOD_SSL))
{
ssl = true;
}
else if (secMethodParam.getValueString().equals(SECURITY_METHOD_STARTTLS))
{
startTLS = true;
}
String dn = null;
String pw = null;
StringParameter bindDNParam =
parameters.getStringParameter(bindDNParameter.getName());
PasswordParameter bindPWParam =
parameters.getPasswordParameter(bindPWParameter.getName());
if ((bindDNParam != null) && bindDNParam.hasValue())
{
dn = bindDNParam.getStringValue();
pw = bindPWParam.getStringValue();
}
SocketFactory socketFactory;
SSLContext sslCtx;
if (ssl)
{
sslCtx = null;
SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
try
{
socketFactory = sslUtil.createSSLSocketFactory();
}
catch (Exception e)
{
outputMessages.add("Unable to create an SSL socket factory: " +
stackTraceToString(e));
return false;
}
}
else if (startTLS)
{
socketFactory = SocketFactory.getDefault();
SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
try
{
sslCtx = sslUtil.createSSLContext();
}
catch (Exception e)
{
outputMessages.add("Unable to create an SSL context for StartTLS " +
"communication: " + stackTraceToString(e));
return false;
}
}
else
{
socketFactory = SocketFactory.getDefault();
sslCtx = null;
}
boolean successful = true;
for (int i=0; i < addrList.size(); i++)
{
String addr = addrList.get(i);
int port = portList.get(i);
LDAPConnection c;
if (ssl)
{
outputMessages.add("Attempting to create an SSL-based connection to " +
addr + ':' + port + "....");
}
else
{
outputMessages.add("Attempting to create an insecure connection to " +
addr + ':' + port + "....");
}
try
{
c = new LDAPConnection(socketFactory, addr, port);
outputMessages.add("Connected successfully.");
outputMessages.add("");
}
catch (LDAPException le)
{
successful = false;
outputMessages.add("The connection attempt failed: " +
le.getExceptionMessage());
continue;
}
try
{
if (startTLS)
{
outputMessages.add("Attempting to perform StartTLS negotation....");
ExtendedResult r = c.processExtendedOperation(
new StartTLSExtendedRequest(sslCtx));
if (r.getResultCode().equals(ResultCode.SUCCESS))
{
outputMessages.add("StartTLS negotiation successful.");
outputMessages.add("");
}
else
{
successful = false;
outputMessages.add("StartTLS negotiation failed: " +
r.toString());
outputMessages.add("");
continue;
}
}
if (dn != null)
{
outputMessages.add("Attempting to bind as " + dn + " ....");
try
{
c.bind(dn, pw);
outputMessages.add("Authentication successful.");
outputMessages.add("");
}
catch (LDAPException le)
{
successful = false;
outputMessages.add("Authentication failed: " + le.toString());
outputMessages.add("");
continue;
}
}
successful &= testNonLDAPJobParameters(parameters, c, outputMessages);
}
catch (LDAPException le)
{
outputMessages.add("An unexpected failure occurred: " +
le.getExceptionMessage());
successful = false;
}
catch (Exception e)
{
outputMessages.add("An unexpected failure occurred: " +
stackTraceToString(e));
successful = false;
}
finally
{
c.close();
}
}
successful &= testNonLDAPJobParameters(parameters, outputMessages);
if (successful)
{
outputMessages.add("All tests completed successfully.");
}
return successful;
}
/**
* Performs any additional tests for job parameters that aren't related to the
* target directory server(s).
*
* @param parameters The parameters provided for the job.
* @param outputMessages A list into which output messages detailing the
* test results should be added.
*
* @return {@code true} if all tests were successful, or {@code false} if
* not.
*/
protected boolean testNonLDAPJobParameters(final ParameterList parameters,
final ArrayList<String> outputMessages)
{
// No implementation is required by default.
return true;
}
/**
* Performs any additional tests for job parameters that aren't related to the
* target directory server(s) using the provided connection.
*
* @param parameters The parameters provided for the job.
* @param connection The connection to use to perform the test.
* @param outputMessages A list into which output messages detailing the
* test results should be added.
*
* @return {@code true} if all tests were successful, or {@code false} if
* not.
*/
protected boolean testNonLDAPJobParameters(final ParameterList parameters,
final LDAPConnection connection,
final ArrayList<String> outputMessages)
{
// No implementation is required by default.
return true;
}
/**
* {@inheritDoc}
*/
@Override()
public final void initializeClient(final String clientID,
final ParameterList parameters)
throws UnableToRunException
{
ArrayList<String> addrList = new ArrayList<String>();
ArrayList<Integer> portList = new ArrayList<Integer>();
addressesParameter =
parameters.getMultiLineTextParameter(addressesParameter.getName());
// We want to alter the order of the address:port values between clients so
// that the load is better distributed across multiple clients.
ArrayList<String> addrValues = new ArrayList<String>(
Arrays.asList(addressesParameter.getNonBlankLines()));
int offset = (getClientNumber() % addrValues.size());
for (int i=0; i < offset; i++)
{
String addrValue = addrValues.remove(0);
addrValues.add(addrValue);
}
for (String a : addrValues)
{
int colonPos = a.indexOf(':');
if (colonPos > 0)
{
addrList.add(a.substring(0, colonPos));
portList.add(Integer.parseInt(a.substring(colonPos+1)));
}
else
{
addrList.add(a);
portList.add(389);
}
}
String[] hosts = new String[addrList.size()];
int[] ports = new int[hosts.length];
for (int i=0; i < hosts.length; i++)
{
hosts[i] = addrList.get(i);
ports[i] = portList.get(i);
}
SocketFactory socketFactory;
useStartTLS = false;
securityMethodParameter =
parameters.getMultiChoiceParameter(securityMethodParameter.getName());
if (securityMethodParameter.getValueString().equals(SECURITY_METHOD_SSL))
{
SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
try
{
socketFactory = sslUtil.createSSLSocketFactory();
}
catch (Exception e)
{
throw new UnableToRunException("Cannot create SSL socket factory: " +
String.valueOf(e), e);
}
}
else if (securityMethodParameter.getValueString().equals(
SECURITY_METHOD_STARTTLS))
{
useStartTLS = true;
SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
try
{
sslContext = sslUtil.createSSLContext();
}
catch (Exception e)
{
throw new UnableToRunException("Cannot create SSL context for " +
"StartTLS negotiation: " + String.valueOf(e), e);
}
socketFactory = SocketFactory.getDefault();
}
else
{
socketFactory = SocketFactory.getDefault();
}
LDAPConnectionOptions options = new LDAPConnectionOptions();
options.setAutoReconnect(true);
options.setUseSynchronousMode(useSynchronousMode());
serverSet = new RoundRobinServerSet(hosts, ports, socketFactory, options);
bindDN = null;
bindPW = null;
bindDNParameter = parameters.getStringParameter(bindDNParameter.getName());
bindPWParameter =
parameters.getPasswordParameter(bindPWParameter.getName());
if ((bindDNParameter != null) && bindDNParameter.hasValue())
{
bindDN = bindDNParameter.getStringValue();
bindPW = bindPWParameter.getStringValue();
}
initializeClientNonLDAP(clientID, parameters);
}
/**
* Performs any necessary initialization that should be performed on a
* per-client basis that doesn't involve parsing the LDAP-related parameters.
*
* @param clientID The client ID that has been assigned to the client on
* which this method has been invoked.
* @param parameters The set of parameters that have been scheduled for this
* job.
*
* @throws UnableToRunException If the client cannot run the job with the
* provided parameters.
*/
protected void initializeClientNonLDAP(final String clientID,
final ParameterList parameters)
throws UnableToRunException
{
// No action required by default.
}
/**
* Indicates whether the client should operate in synchronous mode. This
* should not be used if the client intends to perform asynchronous
* operations (either explicitly or by sharing a connection across multiple
* threads).
*
* @return {@code true} if the client should operate in synchronous mode, or
* {@code false} if not.
*/
protected boolean useSynchronousMode()
{
return true;
}
/**
* Retrieves the server set that can be used to create the connections.
*
* @return The server set that can be used to create the connections.
*/
protected static RoundRobinServerSet getServerSet()
{
return serverSet;
}
/**
* Retrieves a bind request that can be used to authenticate connections, if
* the connections should be authenticated.
*
* @return A bind request that can be used to authenticate connections, or
* {@code null} if no authentication should be performed.
*/
protected static BindRequest getBindRequest()
{
if ((bindDN == null) || (bindPW == null))
{
return null;
}
return new SimpleBindRequest(bindDN, bindPW);
}
/**
* Creates a new LDAP connection to one of the configured servers using the
* provided parameters.
*
* @return The created LDAP connection.
*
* @throws LDAPException If a problem occurs while trying to create the
* connection.
*/
protected static LDAPConnection createConnection()
throws LDAPException
{
LDAPConnection c = serverSet.getConnection();
prepareConnection(c);
return c;
}
/**
* Creates a new LDAP connection to the specified server using the provided
* parameters.
*
* @param address The address of the server to which the connection should
* be established.
* @param port The port number of the server to which the connection
* should be established.
*
* @return The created LDAP connection.
*
* @throws LDAPException If a problem occurs while trying to create the
* connection.
*/
protected static LDAPConnection createConnection(final String address,
final int port)
throws LDAPException
{
LDAPConnection c =
new LDAPConnection(serverSet.getSocketFactory(), address, port);
prepareConnection(c);
return c;
}
/**
* Creates a new LDAP connection pool based on the provided parameters.
*
* @param initialConnections The initial number of connections to include in
* the pool.
* @param maxConnections The maximum number of connections to include in
* the pool.
*
* Creates a new LDAP connection to one of the configured servers using the
* provided parameters.
*
* @return The created LDAP connection.
*
* @throws LDAPException If a problem occurs while trying to create the
* connection.
*/
protected static LDAPConnectionPool createConnectionPool(
final int initialConnections,
final int maxConnections)
throws LDAPException
{
SimpleBindRequest bindRequest = null;
if ((bindDN != null) && (bindPW != null))
{
bindRequest = new SimpleBindRequest(bindDN, bindPW);
}
StartTLSPostConnectProcessor postConnectProcessor = null;
if (useStartTLS)
{
postConnectProcessor = new StartTLSPostConnectProcessor(sslContext);
}
return new LDAPConnectionPool(serverSet, bindRequest, initialConnections,
maxConnections, postConnectProcessor);
}
/**
* Performs StartTLS negotiation and/or authentication on the provided
* connection, if appropriate.
*
* @param connection The connection to process.
*
* @throws LDAPException If a problem occurs during processing.
*/
private static void prepareConnection(final LDAPConnection connection)
throws LDAPException
{
if (useStartTLS)
{
try
{
ExtendedResult r = connection.processExtendedOperation(
new StartTLSExtendedRequest(sslContext));
if (! r.getResultCode().equals(ResultCode.SUCCESS))
{
throw new LDAPException(r);
}
}
catch (LDAPException le)
{
connection.close();
throw le;
}
}
if (bindDN != null)
{
try
{
connection.bind(bindDN, bindPW);
}
catch (LDAPException le)
{
connection.close();
throw le;
}
}
}
}