Bug 42506 - JMeter threads all use the same SSL session

git-svn-id: https://svn.apache.org/repos/asf/jakarta/jmeter/branches/rel-2-2@543739 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Sebastian Bazley 2007-06-02 13:48:56 +00:00
parent 81ec3b51a6
commit c76f9ad943
5 changed files with 220 additions and 119 deletions

View File

@ -16,4 +16,8 @@
## limitations under the License.
#
#search_paths=../addons/addons.jar
#log_level.jorphan.reflect=DEBUG
#log_level.jorphan.reflect=DEBUG
# Warning: enabling the next debug line causes javax.net.ssl.SSLException: Received fatal alert: unexpected_message
# for certain sites when used with the default HTTP Sampler
#log_level.jmeter.util.HttpSSLProtocolSocketFactory=DEBUG
#log_level.jmeter.util.JsseSSLManager=DEBUG

View File

@ -23,8 +23,10 @@ import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
@ -46,37 +48,53 @@ public class HttpSSLProtocolSocketFactory
private static final Logger log = LoggingManager.getLoggerForClass();
private SSLSocketFactory sslfac;
private JsseSSLManager sslManager;
private HttpSSLProtocolSocketFactory(){
}
public HttpSSLProtocolSocketFactory(SSLContext context) {
public HttpSSLProtocolSocketFactory(JsseSSLManager sslManager) {
super();
sslfac=context.getSocketFactory();
this.sslManager = sslManager;
}
private static final String protocolList = JMeterUtils.getPropDefault("https.socket.protocols", "");
private static final String protocolList =
JMeterUtils.getPropDefault("https.socket.protocols", ""); // $NON-NLS-1$ $NON-NLS-2$
static {
if (protocolList.length()>0){
log.info("Using protocol list: "+protocolList);
}
}
private static final String[] protocols = protocolList.split(" ");
private void setSocket(Socket sock){
if (protocolList.length() <= 0) return;
if (sock instanceof SSLSocket){
try {
((SSLSocket) sock).setEnabledProtocols(protocols);
} catch (IllegalArgumentException e) {
log.warn("Could not set protocol list: "+protocolList+".");
log.warn("Valid protocols are: "+join(((SSLSocket) sock).getSupportedProtocols()));
}
} else {
throw new IllegalArgumentException("Expecting only SSL socket; found "+sock.getClass().getName());
private static final String[] protocols = protocolList.split(" "); // $NON-NLS-1$
private void setSocket(Socket socket){
if (!(socket instanceof SSLSocket)) {
throw new IllegalArgumentException("Expected SSLSocket");
}
SSLSocket sock = (SSLSocket) socket;
if (log.isDebugEnabled()) {
SSLSession sslSession = sock.getSession();
byte[] bytes = sslSession.getId();
StringBuffer buffer = new StringBuffer("SSL session id: ");
for (int i = 0; i < bytes.length; i++) {
int b = bytes[i] & 0xff;
buffer.append(Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)));
buffer.append(Character.toUpperCase(Character.forDigit(b & 0xF, 16)));
}
buffer.append(" for ").append(Thread.currentThread().getName());
log.debug(buffer.toString());
}
if (protocolList.length() > 0) {
try {
sock.setEnabledProtocols(protocols);
} catch (IllegalArgumentException e) {
log.warn("Could not set protocol list: " + protocolList + ".");
log.warn("Valid protocols are: " + join(sock.getSupportedProtocols()));
}
}
}
private String join(String[] strings) {
@ -87,6 +105,15 @@ public class HttpSSLProtocolSocketFactory
}
return sb.toString();
}
private SSLSocketFactory getSSLSocketFactory() throws IOException {
try {
SSLContext sslContext = this.sslManager.getContext();
return sslContext.getSocketFactory();
} catch (GeneralSecurityException ex) {
throw new IOException(ex.getMessage());
}
}
/**
* Attempts to get a new socket connection to the given host within the given time limit.
@ -114,6 +141,8 @@ public class HttpSSLProtocolSocketFactory
throw new IllegalArgumentException("Parameters may not be null");
}
int timeout = params.getConnectionTimeout();
SSLSocketFactory sslfac = getSSLSocketFactory();
Socket socket;
if (timeout == 0) {
socket = sslfac.createSocket(host, port, localAddress, localPort);
@ -133,6 +162,7 @@ public class HttpSSLProtocolSocketFactory
*/
public Socket createSocket(String host, int port)
throws IOException, UnknownHostException {
SSLSocketFactory sslfac = getSSLSocketFactory();
Socket sock = sslfac.createSocket(
host,
port
@ -150,6 +180,7 @@ public class HttpSSLProtocolSocketFactory
int port,
boolean autoClose)
throws IOException, UnknownHostException {
SSLSocketFactory sslfac = getSSLSocketFactory();
Socket sock = sslfac.createSocket(
socket,
host,
@ -170,6 +201,7 @@ public class HttpSSLProtocolSocketFactory
int clientPort)
throws IOException, UnknownHostException {
SSLSocketFactory sslfac = getSSLSocketFactory();
Socket sock = sslfac.createSocket(
host,
port,
@ -181,22 +213,34 @@ public class HttpSSLProtocolSocketFactory
}
public Socket createSocket(InetAddress host, int port) throws IOException {
SSLSocketFactory sslfac = getSSLSocketFactory();
Socket sock=sslfac.createSocket(host,port);
setSocket(sock);
return sock;
}
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
SSLSocketFactory sslfac = getSSLSocketFactory();
Socket sock=sslfac.createSocket(address, port, localAddress, localPort);
setSocket(sock);
return sock;
}
public String[] getDefaultCipherSuites() {
return sslfac.getDefaultCipherSuites();
try {
SSLSocketFactory sslfac = getSSLSocketFactory();
return sslfac.getDefaultCipherSuites();
} catch (IOException ex) {
return new String[] {};
}
}
public String[] getSupportedCipherSuites() {
return sslfac.getSupportedCipherSuites();
try {
SSLSocketFactory sslfac = getSSLSocketFactory();
return sslfac.getSupportedCipherSuites();
} catch (IOException ex) {
return new String[] {};
}
}
}

View File

@ -20,18 +20,13 @@ package org.apache.jmeter.util;
import java.net.HttpURLConnection;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
import org.apache.jmeter.util.keystore.JmeterKeyStore;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
@ -43,6 +38,12 @@ import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
import org.apache.jmeter.util.keystore.JmeterKeyStore;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;
/**
* The SSLManager handles the KeyStore information for JMeter. Basically, it
* handles all the logic for loading and initializing all the JSSE parameters
@ -53,8 +54,6 @@ import javax.net.ssl.X509TrustManager;
*
* TODO: does not actually prompt
*
* @author <a href="bloritsch@apache.org">Berin Loritsch</a> Created March 21,
* 2002
*/
public class JsseSSLManager extends SSLManager {
private static final Logger log = LoggingManager.getLoggerForClass();
@ -65,8 +64,13 @@ public class JsseSSLManager extends SSLManager {
private static final String DEFAULT_SSL_PROTOCOL =
JMeterUtils.getPropDefault("https.default.protocol","TLS"); // $NON-NLS-1$ // $NON-NLS-2$
// Allow reversion to original shared session context
private static final boolean SHARED_SESSION_CONTEXT =
JMeterUtils.getPropDefault("https.sessioncontext.shared",false); // $NON-NLS-1$
static {
log.info("Using default SSL protocol: "+DEFAULT_SSL_PROTOCOL);
log.info("SSL session context: "+(SHARED_SESSION_CONTEXT ? "shared" : "per-thread"));
}
/**
@ -74,13 +78,11 @@ public class JsseSSLManager extends SSLManager {
*/
private SecureRandom rand;
/**
* Cache the Context so we can retrieve it from other places
*/
private SSLContext context = null;
private Provider pro = null;
private SSLContext defaultContext; // If we are using a single session
private ThreadLocal threadlocal; // Otherwise
/**
* Create the SSLContext, and wrap all the X509KeyManagers with
* our X509KeyManager so that we can choose our alias.
@ -91,11 +93,37 @@ public class JsseSSLManager extends SSLManager {
public JsseSSLManager(Provider provider) {
log.debug("ssl Provider = " + provider);
setProvider(provider);
if (null == this.rand) {
if (null == this.rand) { // Surely this is always null in the constructor?
this.rand = new SecureRandom();
}
this.getContext();
try {
if (SHARED_SESSION_CONTEXT) {
log.debug("Creating shared context");
this.defaultContext = createContext();
} else {
this.threadlocal = new ThreadLocal();
}
HttpSSLProtocolSocketFactory sockFactory = new HttpSSLProtocolSocketFactory(this);
HttpsURLConnection.setDefaultSSLSocketFactory(sockFactory);
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
/*
* Also set up HttpClient defaults
*/
Protocol protocol = new Protocol(
JsseSSLManager.HTTPS,
(ProtocolSocketFactory) sockFactory,
443);
Protocol.registerProtocol(JsseSSLManager.HTTPS, protocol);
log.debug("SSL stuff all set");
} catch (GeneralSecurityException ex) {
log.error("Could not set up SSLContext", ex);
}
log.debug("JsseSSLManager installed");
}
@ -133,94 +161,101 @@ public class JsseSSLManager extends SSLManager {
}
/**
* Returns the SSLContext we are using. It is useful for obtaining the
* SSLSocketFactory so that your created sockets are authenticated.
* Returns the SSLContext we are using.
* This is either a context per thread,
* or, for backwards compatibility, a single shared context.
*
* @return The Context value
*/
private SSLContext getContext() {
if (null == this.context) {
try {
if (pro != null) {
this.context = SSLContext.getInstance(DEFAULT_SSL_PROTOCOL, pro); // $NON-NLS-1$
} else {
this.context = SSLContext.getInstance(DEFAULT_SSL_PROTOCOL); // $NON-NLS-1$
}
log.debug("SSL context = " + context);
} catch (Exception ee) {
log.error("Could not create SSLContext", ee);
}
try {
KeyManagerFactory managerFactory =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
JmeterKeyStore keys = this.getKeyStore();
managerFactory.init(null, this.defaultpw.toCharArray());
KeyManager[] managers = managerFactory.getKeyManagers();
log.debug(keys.getClass().toString());
// Now wrap the default managers with our key manager
for (int i = 0; i < managers.length; i++) {
if (managers[i] instanceof X509KeyManager) {
X509KeyManager manager = (X509KeyManager) managers[i];
managers[i] = new WrappedX509KeyManager(manager, keys);
}
}
// Get the default trust managers
TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmfactory.init(this.getTrustStore());
// Wrap the defaults in our custom trust manager
TrustManager[] trustmanagers = tmfactory.getTrustManagers();
for (int i = 0; i < trustmanagers.length; i++) {
if (trustmanagers[i] instanceof X509TrustManager) {
trustmanagers[i] = new CustomX509TrustManager(
(X509TrustManager)trustmanagers[i]);
}
}
context.init(managers, trustmanagers, this.rand);
/*
* The following will need to be removed if the SSL properties are to be
* applied on a per-connection basis
*/
HttpsURLConnection.setDefaultSSLSocketFactory(new HttpSSLProtocolSocketFactory(context));
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
/*
* Also set up HttpClient defaults
*/
Protocol protocol = new Protocol(
JsseSSLManager.HTTPS,
(ProtocolSocketFactory) new HttpSSLProtocolSocketFactory(context),
443
);
Protocol.registerProtocol(JsseSSLManager.HTTPS, protocol);
log.debug("SSL stuff all set");
} catch (Exception e) {
log.error("Could not set up SSLContext", e);
}
if (log.isDebugEnabled()){
String[] dCiphers = this.context.getSocketFactory().getDefaultCipherSuites();
String[] sCiphers = this.context.getSocketFactory().getSupportedCipherSuites();
int len = (dCiphers.length > sCiphers.length) ? dCiphers.length : sCiphers.length;
for (int i = 0; i < len; i++) {
if (i < dCiphers.length) {
log.debug("Default Cipher: " + dCiphers[i]);
}
if (i < sCiphers.length) {
log.debug("Supported Cipher: " + sCiphers[i]);
}
}
public SSLContext getContext() throws GeneralSecurityException {
if (SHARED_SESSION_CONTEXT) {
if (log.isDebugEnabled()){
log.debug("Using shared SSL context for: "+Thread.currentThread().getName());
}
return this.defaultContext;
}
SSLContext sslContext = (SSLContext) this.threadlocal.get();
if (sslContext == null) {
if (log.isDebugEnabled()){
log.debug("Creating threadLocal SSL context for: "+Thread.currentThread().getName());
}
sslContext = createContext();
this.threadlocal.set(sslContext);
}
if (log.isDebugEnabled()){
log.debug("Using threadLocal SSL context for: "+Thread.currentThread().getName());
}
return sslContext;
}
/**
* Resets the SSLContext if using per-thread contexts.
*
*/
public void resetContext() {
if (!SHARED_SESSION_CONTEXT) {
log.debug("Clearing session context for current thread");
this.threadlocal.set(null);
}
}
/*
*
* Creates new SSL context
* @return SSL context
* @throws GeneralSecurityException
*/
private SSLContext createContext() throws GeneralSecurityException {
SSLContext context;
if (pro != null) {
context = SSLContext.getInstance(DEFAULT_SSL_PROTOCOL, pro); // $NON-NLS-1$
} else {
context = SSLContext.getInstance(DEFAULT_SSL_PROTOCOL); // $NON-NLS-1$
}
KeyManagerFactory managerFactory =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
JmeterKeyStore keys = this.getKeyStore();
managerFactory.init(null, this.defaultpw.toCharArray());
KeyManager[] managers = managerFactory.getKeyManagers();
log.debug(keys.getClass().toString());
// Now wrap the default managers with our key manager
for (int i = 0; i < managers.length; i++) {
if (managers[i] instanceof X509KeyManager) {
X509KeyManager manager = (X509KeyManager) managers[i];
managers[i] = new WrappedX509KeyManager(manager, keys);
}
}
return this.context;
}
}
// Get the default trust managers
TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmfactory.init(this.getTrustStore());
// Wrap the defaults in our custom trust manager
TrustManager[] trustmanagers = tmfactory.getTrustManagers();
for (int i = 0; i < trustmanagers.length; i++) {
if (trustmanagers[i] instanceof X509TrustManager) {
trustmanagers[i] = new CustomX509TrustManager(
(X509TrustManager)trustmanagers[i]);
}
}
context.init(managers, trustmanagers, this.rand);
if (log.isDebugEnabled()){
String[] dCiphers = context.getSocketFactory().getDefaultCipherSuites();
String[] sCiphers = context.getSocketFactory().getSupportedCipherSuites();
int len = (dCiphers.length > sCiphers.length) ? dCiphers.length : sCiphers.length;
for (int i = 0; i < len; i++) {
if (i < dCiphers.length) {
log.debug("Default Cipher: " + dCiphers[i]);
}
if (i < sCiphers.length) {
log.debug("Supported Cipher: " + sCiphers[i]);
}
}
}
return context;
}
/**
* This is the X509KeyManager we have defined for the sole purpose of

View File

@ -43,6 +43,7 @@ Some of the main enhancements are:
<li>LDAP Ext sampler optionally parses result sets and supports secure mode</li>
<li>FTP Sampler supports Ascii/Binary mode and upload</li>
<li>Transaction Controller now generates Sample with subresults</li>
<li>HTTPS session contexts are now per-thread, rather than shared. This gives better emulation of multiple users</li>
</ul>
<p>
The main bug fixes are:
@ -93,6 +94,13 @@ has been removed.
<p>
Control-Z no longer used for Remote Start All; replaced by Control+Shift+R
</p>
<p>
By default, SSL session contexts are now created per-thread, rather than being shared.
The original behaviour can be enabled by setting the JMeter property:
<pre>
https.sessioncontext.shared=true
</pre>
</p>
<h4>Incompatible changes (development):</h4>
<p>
Calulator and SamplingStatCalculator classes no longer provide any formatting of their data.
@ -149,6 +157,7 @@ Removed deprecated method JMeterUtils.split() - use JOrphanUtils version instead
<li>Use ISO date-time format for Tree View Listener (previously the year was not shown)</li>
<li>Improve loading of CSV files: if possible, use header to determine format; guess timestamp format if not milliseconds</li>
<li>Bug 41913 - TransactionController now creates samples as sub-samples of the transaction</li>
<li>Bug 42506 - JMeter threads all use the same SSL session</li>
</ul>
<h4>Non-functional improvements:</h4>

View File

@ -122,6 +122,15 @@ JMeter assumes the FTP server is listening on the default port.</property>
and the appropriate parameters from the form definition.
If the page uses HTTP, you can use the JMeter Proxy to capture the login sequence.
</p>
<p>
In versions of JMeter up to 2.2, only a single SSL context was used for all threads and samplers.
This did not generate the proper load for multiple users.
A separate SSL context is now used for each thread.
To revert to the original behaviour, set the JMeter property:
<pre>
https.sessioncontext.shared=true
</pre>
</p>
<p>If the request uses cookies, then you will also need an
<complink name="HTTP Cookie Manager"/>. You can
add either of these elements to the Thread Group or the HTTP Request. If you have