Author: sebb Date: Sat Jun 2 06:48:56 2007 New Revision: 543739 URL: http://svn.apache.org/viewvc?view=rev&rev=543739 Log: Bug 42506 - JMeter threads all use the same SSL session
Modified: jakarta/jmeter/branches/rel-2-2/bin/user.properties jakarta/jmeter/branches/rel-2-2/src/core/org/apache/jmeter/util/HttpSSLProtocolSocketFactory.java jakarta/jmeter/branches/rel-2-2/src/core/org/apache/jmeter/util/JsseSSLManager.java jakarta/jmeter/branches/rel-2-2/xdocs/changes.xml jakarta/jmeter/branches/rel-2-2/xdocs/usermanual/component_reference.xml Modified: jakarta/jmeter/branches/rel-2-2/bin/user.properties URL: http://svn.apache.org/viewvc/jakarta/jmeter/branches/rel-2-2/bin/user.properties?view=diff&rev=543739&r1=543738&r2=543739 ============================================================================== --- jakarta/jmeter/branches/rel-2-2/bin/user.properties (original) +++ jakarta/jmeter/branches/rel-2-2/bin/user.properties Sat Jun 2 06:48:56 2007 @@ -16,4 +16,8 @@ ## limitations under the License. # #search_paths=../addons/addons.jar -#log_level.jorphan.reflect=DEBUG \ No newline at end of file +#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 \ No newline at end of file Modified: jakarta/jmeter/branches/rel-2-2/src/core/org/apache/jmeter/util/HttpSSLProtocolSocketFactory.java URL: http://svn.apache.org/viewvc/jakarta/jmeter/branches/rel-2-2/src/core/org/apache/jmeter/util/HttpSSLProtocolSocketFactory.java?view=diff&rev=543739&r1=543738&r2=543739 ============================================================================== --- jakarta/jmeter/branches/rel-2-2/src/core/org/apache/jmeter/util/HttpSSLProtocolSocketFactory.java (original) +++ jakarta/jmeter/branches/rel-2-2/src/core/org/apache/jmeter/util/HttpSSLProtocolSocketFactory.java Sat Jun 2 06:48:56 2007 @@ -23,8 +23,10 @@ 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 @@ 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 @@ } 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 @@ 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 Socket createSocket(String host, int port) throws IOException, UnknownHostException { + SSLSocketFactory sslfac = getSSLSocketFactory(); Socket sock = sslfac.createSocket( host, port @@ -150,6 +180,7 @@ int port, boolean autoClose) throws IOException, UnknownHostException { + SSLSocketFactory sslfac = getSSLSocketFactory(); Socket sock = sslfac.createSocket( socket, host, @@ -170,6 +201,7 @@ int clientPort) throws IOException, UnknownHostException { + SSLSocketFactory sslfac = getSSLSocketFactory(); Socket sock = sslfac.createSocket( host, port, @@ -181,22 +213,34 @@ } 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[] {}; + } } } Modified: jakarta/jmeter/branches/rel-2-2/src/core/org/apache/jmeter/util/JsseSSLManager.java URL: http://svn.apache.org/viewvc/jakarta/jmeter/branches/rel-2-2/src/core/org/apache/jmeter/util/JsseSSLManager.java?view=diff&rev=543739&r1=543738&r2=543739 ============================================================================== --- jakarta/jmeter/branches/rel-2-2/src/core/org/apache/jmeter/util/JsseSSLManager.java (original) +++ jakarta/jmeter/branches/rel-2-2/src/core/org/apache/jmeter/util/JsseSSLManager.java Sat Jun 2 06:48:56 2007 @@ -20,18 +20,13 @@ 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.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 @@ * * TODO: does not actually prompt * - * @author <a href="[EMAIL PROTECTED]">Berin Loritsch</a> Created March 21, - * 2002 */ public class JsseSSLManager extends SSLManager { private static final Logger log = LoggingManager.getLoggerForClass(); @@ -65,8 +64,13 @@ 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 @@ */ 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 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 @@ } /** - * 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 Modified: jakarta/jmeter/branches/rel-2-2/xdocs/changes.xml URL: http://svn.apache.org/viewvc/jakarta/jmeter/branches/rel-2-2/xdocs/changes.xml?view=diff&rev=543739&r1=543738&r2=543739 ============================================================================== --- jakarta/jmeter/branches/rel-2-2/xdocs/changes.xml (original) +++ jakarta/jmeter/branches/rel-2-2/xdocs/changes.xml Sat Jun 2 06:48:56 2007 @@ -43,6 +43,7 @@ <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 @@ <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 @@ <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> Modified: jakarta/jmeter/branches/rel-2-2/xdocs/usermanual/component_reference.xml URL: http://svn.apache.org/viewvc/jakarta/jmeter/branches/rel-2-2/xdocs/usermanual/component_reference.xml?view=diff&rev=543739&r1=543738&r2=543739 ============================================================================== --- jakarta/jmeter/branches/rel-2-2/xdocs/usermanual/component_reference.xml (original) +++ jakarta/jmeter/branches/rel-2-2/xdocs/usermanual/component_reference.xml Sat Jun 2 06:48:56 2007 @@ -122,6 +122,15 @@ 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 --------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]