You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jmeter.apache.org by se...@apache.org on 2013/09/10 02:02:40 UTC
svn commit: r1521320 - in /jmeter/trunk: bin/
src/core/org/apache/jmeter/resources/
src/protocol/http/org/apache/jmeter/protocol/http/proxy/
src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/
Author: sebb
Date: Tue Sep 10 00:02:39 2013
New Revision: 1521320
URL: http://svn.apache.org/r1521320
Log:
Proxy SSL recording does not handle external embedded resources well
Reworked; setup is now done by ProxyControl on pressing Start
TODO: better notification of progress of keystore init
Bugzilla Id: 55507
Modified:
jmeter/trunk/bin/jmeter.properties
jmeter/trunk/bin/proxyserver.jks
jmeter/trunk/src/core/org/apache/jmeter/resources/messages.properties
jmeter/trunk/src/core/org/apache/jmeter/resources/messages_fr.properties
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java
Modified: jmeter/trunk/bin/jmeter.properties
URL: http://svn.apache.org/viewvc/jmeter/trunk/bin/jmeter.properties?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
--- jmeter/trunk/bin/jmeter.properties (original)
+++ jmeter/trunk/bin/jmeter.properties Tue Sep 10 00:02:39 2013
@@ -550,6 +550,9 @@ upgrade_properties=/bin/upgrade.properti
#proxy.cert.alias=<none>
# The default validity for certificates created by JMeter
#proxy.cert.validity=7
+# Use dynamic key generation (if supported by JMeter/JVM)
+# If false, will revert to using a single key with no certificate
+#proxy.cert.dynamic_keys=true
# SSL configuration
#proxy.ssl.protocol=SSLv3
Modified: jmeter/trunk/bin/proxyserver.jks
URL: http://svn.apache.org/viewvc/jmeter/trunk/bin/proxyserver.jks?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
Binary files - no diff available.
Modified: jmeter/trunk/src/core/org/apache/jmeter/resources/messages.properties
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/resources/messages.properties?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/resources/messages.properties (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/resources/messages.properties Tue Sep 10 00:02:39 2013
@@ -723,6 +723,7 @@ proxy_content_type_filter=Content-type f
proxy_content_type_include=Include\:
proxy_daemon_bind_error=Could not create proxy - port in use. Choose another port.
proxy_daemon_error=Could not create proxy - see log for details
+proxy_domains=HTTPS Domains \:
proxy_general_settings=Global Settings
proxy_headers=Capture HTTP Headers
proxy_regex=Regex matching
Modified: jmeter/trunk/src/core/org/apache/jmeter/resources/messages_fr.properties
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/resources/messages_fr.properties?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/resources/messages_fr.properties (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/resources/messages_fr.properties Tue Sep 10 00:02:39 2013
@@ -716,6 +716,7 @@ proxy_content_type_filter=Filtre de type
proxy_content_type_include=Inclure \:
proxy_daemon_bind_error=Impossible de lancer le serveur proxy, le port est d\u00E9j\u00E0 utilis\u00E9. Choisissez un autre port.
proxy_daemon_error=Impossible de lancer le serveur proxy, voir le journal pour plus de d\u00E9tails
+proxy_domains=Domaines HTTPS \:
proxy_general_settings=Param\u00E8tres g\u00E9n\u00E9raux
proxy_headers=Capturer les ent\u00EAtes HTTP
proxy_regex=Correspondance des variables par regex ?
Modified: jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
--- jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java (original)
+++ jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java Tue Sep 10 00:02:39 2013
@@ -22,11 +22,7 @@ import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
import java.io.IOException;
-import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
@@ -34,9 +30,9 @@ import java.net.URL;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
+import java.security.KeyStoreException;
import java.util.HashMap;
import java.util.Map;
-import java.util.prefs.Preferences;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
@@ -44,8 +40,6 @@ import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.RandomStringUtils;
import org.apache.jmeter.protocol.http.control.HeaderManager;
import org.apache.jmeter.protocol.http.parser.HTMLParseException;
import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
@@ -54,7 +48,6 @@ import org.apache.jmeter.protocol.http.u
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.util.JMeterUtils;
-import org.apache.jorphan.exec.KeyToolUtils;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.jorphan.util.JMeterException;
import org.apache.jorphan.util.JOrphanUtils;
@@ -84,10 +77,6 @@ public class Proxy extends Thread {
private static final String PROXY_HEADERS_REMOVE_SEPARATOR = ","; // $NON-NLS-1$
- // for ssl connection
- private static final String KEYSTORE_TYPE =
- JMeterUtils.getPropDefault("proxy.cert.type", "JKS"); // $NON-NLS-1$ $NON-NLS-2$
-
private static final String KEYMANAGERFACTORY =
JMeterUtils.getPropDefault("proxy.cert.factory", "SunX509"); // $NON-NLS-1$ $NON-NLS-2$
@@ -97,30 +86,8 @@ public class Proxy extends Thread {
// HashMap to save ssl connection between Jmeter proxy and browser
private static final HashMap<String, SSLSocketFactory> hashHost = new HashMap<String, SSLSocketFactory>();
- // Proxy configuration SSL
- private static final String CERT_DIRECTORY =
- JMeterUtils.getPropDefault("proxy.cert.directory", JMeterUtils.getJMeterBinDir()); // $NON-NLS-1$
-
- private static final String CERT_FILE_DEFAULT = "proxyserver.jks";// $NON-NLS-1$
-
- private static final String CERT_FILE =
- JMeterUtils.getPropDefault("proxy.cert.file", CERT_FILE_DEFAULT); // $NON-NLS-1$
-
- // The alias to be used if dynamic host names are not possible
- private static final String JMETER_SERVER_ALIAS = ":jmeter:"; // $NON-NLS-1$
-
- private static final int CERT_VALIDITY = JMeterUtils.getPropDefault("proxy.cert.validity", 7); // $NON-NLS-1$
-
- private static final String DEFAULT_PASSWORD = "password"; // $NON-NLS-1$
-
private static final SamplerCreatorFactory factory = new SamplerCreatorFactory();
- // Keys for user preferences
- private static final String USER_PASSWORD_KEY = "proxy_cert_PASSWORD";
-
- private static final Preferences prefs = Preferences.userNodeForPackage(Proxy.class);
- // Note: Windows user preferences are stored relative to: HKEY_CURRENT_USER\Software\JavaSoft\Prefs
-
// Use with SSL connection
private OutputStream outStreamClient = null;
@@ -146,6 +113,10 @@ public class Proxy extends Thread {
private String port; // For identifying log messages
+ private KeyStore keyStore; // keystore for SSL keys; fixed at config except for dynamic host key generation
+
+ private String keyPassword;
+
/**
* Default constructor - used by newInstance call in Daemon
*/
@@ -175,6 +146,8 @@ public class Proxy extends Thread {
this.pageEncodings = _pageEncodings;
this.formEncodings = _formEncodings;
this.port = "["+ clientSocket.getPort() + "] ";
+ this.keyStore = _target.getKeyStore();
+ this.keyPassword = _target.getKeyPassword();
}
/**
@@ -315,17 +288,57 @@ public class Proxy extends Thread {
* @throws IOException
*/
private SSLSocketFactory getSSLSocketFactory(String host) {
+ if (keyStore == null) {
+ log.error(port + "No keystore available, cannot record SSL");
+ return null;
+ }
+ final String hashAlias;
+ final String keyAlias;
+ switch(ProxyControl.KEYSTORE_MODE) {
+ case DYNAMIC_KEYSTORE:
+ try {
+ keyStore = target.getKeyStore(); // pick up any recent changes from other threads
+ String alias = getDomainMatch(keyStore, host);
+ if (alias == null) {
+ hashAlias = host;
+ keyAlias = host;
+ keyStore = target.updateKeyStore(port, keyAlias);
+ } else if (alias.equals(host)) { // the host has a key already
+ hashAlias = host;
+ keyAlias = host;
+ } else { // the host matches a domain; use its key
+ hashAlias = alias;
+ keyAlias = alias;
+ }
+ } catch (IOException e) {
+ log.error(port + "Problem with keystore", e);
+ return null;
+ } catch (GeneralSecurityException e) {
+ log.error(port + "Problem with keystore", e);
+ return null;
+ }
+ break;
+ case JMETER_KEYSTORE:
+ hashAlias = keyAlias = ProxyControl.JMETER_SERVER_ALIAS;
+ break;
+ case USER_KEYSTORE:
+ hashAlias = keyAlias = ProxyControl.CERT_ALIAS;
+ break;
+ default:
+ throw new IllegalStateException("Impossible case: " + ProxyControl.KEYSTORE_MODE);
+ }
synchronized (hashHost) {
- if (hashHost.containsKey(host)) {
- log.debug(port + "Good, already in map, host=" + host);
- return hashHost.get(host);
+ final SSLSocketFactory sslSocketFactory = hashHost.get(hashAlias);
+ if (sslSocketFactory != null) {
+ log.debug(port + "Good, already in map, host=" + host + " using alias " + hashAlias);
+ return sslSocketFactory;
}
try {
SSLContext sslcontext = SSLContext.getInstance(SSLCONTEXT_PROTOCOL);
- sslcontext.init(getKeyManagers(host), null, null);
+ sslcontext.init(getWrappedKeyManagers(keyAlias), null, null);
SSLSocketFactory sslFactory = sslcontext.getSocketFactory();
- hashHost.put(host, sslFactory);
- log.info(port + "KeyStore for SSL loaded OK and put host in map ("+host+")");
+ hashHost.put(hashAlias, sslFactory);
+ log.info(port + "KeyStore for SSL loaded OK and put host '" + host + "' in map with key ("+hashAlias+")");
return sslFactory;
} catch (GeneralSecurityException e) {
log.error(port + "Problem with SSL certificate", e);
@@ -337,115 +350,57 @@ public class Proxy extends Thread {
}
/**
- * Return the key managers, wrapped if necessary to return a specific alias
- *
- * @param serverAlias the alias to return, or null to use whatever is present
- * @param host the target host
- * @return the key managers
- * @throws GeneralSecurityException
- * @throws IOException if the store cannot be opened or read or the alias is missing
- */
- private KeyManager[] getKeyManagers(String host) throws GeneralSecurityException, IOException {
- final KeyStore ks;
- final String serverAlias;
- String keyPass;
- switch(ProxyControl.keystoreType) {
- case JMETER_KEYSTORE:
- ks = getJMeterKeyStore(getPassword(), (String) null);
- keyPass = getPassword(); // above call may have updated the stored password
- serverAlias = JMETER_SERVER_ALIAS;
- break;
- case DYNAMIC_KEYSTORE:
- ks = getJMeterKeyStore(getPassword(), host);
- keyPass = getPassword(); // above call may have updated the stored password
- serverAlias = host;
- break;
- case USER_KEYSTORE:
- default: // Not really needed, but avoids complaints about non-init password strings
- String keyStorePass = JMeterUtils.getPropDefault("proxy.cert.keystorepass", DEFAULT_PASSWORD); // $NON-NLS-1$
- ks = getKeyStore(keyStorePass.toCharArray());
- keyPass = JMeterUtils.getPropDefault("proxy.cert.keypassword", DEFAULT_PASSWORD); // $NON-NLS-1$
- serverAlias = ProxyControl.CERT_ALIAS;
- break;
+ * Get matching alias for a host from keyStore that may contain domain aliases.
+ * Assumes domains must have at least 2 parts (apache.org);
+ * does not check if TLD requires more (google.co.uk).
+ * ProxyControl checks for valid domains before adding them, and any subsequent
+ * additions by the Proxy class will be hosts, not domains.
+ * @param keyStore the KeyStore to search
+ * @param host the hostname to match
+ * @return
+ * @throws KeyStoreException
+ */
+ private String getDomainMatch(KeyStore keyStore, String host) throws KeyStoreException {
+ if (keyStore.containsAlias(host)) {
+ return host;
+ }
+ String parts[] = host.split("\\."); // get the component parts
+ // Assume domains must have at least 2 parts, e.g. apache.org
+ // Don't try matching against *.org; however we don't check *.co.uk here
+ for(int i = 1; i <= parts.length - 2; i++) {
+ StringBuilder sb = new StringBuilder("*");
+ for(int j = i; j < parts.length ; j++) { // add the remaining parts
+ sb.append('.');
+ sb.append(parts[j]);
+ }
+ String alias = sb.toString();
+ if (keyStore.containsAlias(alias)) {
+ return alias;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the key managers, wrapped to return a specific alias
+ */
+ private KeyManager[] getWrappedKeyManagers(final String keyAlias)
+ throws GeneralSecurityException, IOException {
+ if (!keyStore.containsAlias(keyAlias)) {
+ throw new IOException("Keystore does not contain alias " + keyAlias);
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KEYMANAGERFACTORY);
- kmf.init(ks, keyPass.toCharArray());
+ kmf.init(keyStore, keyPassword.toCharArray());
final KeyManager[] keyManagers = kmf.getKeyManagers();
- if (serverAlias == null) {
- return keyManagers;
- } else {
- // Check if alias is suitable here, rather than waiting for connection to fail
- if (!ks.containsAlias(serverAlias)) {
- throw new IOException("Keystore does not contain alias " + serverAlias);
- }
- final int keyManagerCount = keyManagers.length;
- final KeyManager[] wrappedKeyManagers = new KeyManager[keyManagerCount];
- for (int i =0; i < keyManagerCount; i++) {
- wrappedKeyManagers[i] = new ServerAliasKeyManager(keyManagers[i], serverAlias);
- }
- return wrappedKeyManagers;
- }
- }
-
- private KeyStore getKeyStore(char[] password) throws GeneralSecurityException, IOException {
- File certFile = new File(CERT_DIRECTORY, CERT_FILE);
- InputStream in = null;
- final String certPath = certFile.getAbsolutePath();
- try {
- in = new BufferedInputStream(new FileInputStream(certFile));
- log.info(port + "Opened Keystore file: " + certPath);
- KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE);
- ks.load(in, password);
- return ks;
- } catch (FileNotFoundException e) {
- log.error(port + "Could not open Keystore file: " + certPath, e);
- throw e;
- } finally {
- IOUtils.closeQuietly(in);
+ // Check if alias is suitable here, rather than waiting for connection to fail
+ final int keyManagerCount = keyManagers.length;
+ final KeyManager[] wrappedKeyManagers = new KeyManager[keyManagerCount];
+ for (int i =0; i < keyManagerCount; i++) {
+ wrappedKeyManagers[i] = new ServerAliasKeyManager(keyManagers[i], keyAlias);
}
+ return wrappedKeyManagers;
}
- // If host == null, we are not using dynamic keys
- private KeyStore getJMeterKeyStore(String keyStorePass, String host) throws GeneralSecurityException, IOException {
- final File certFile = new File(CERT_DIRECTORY, CERT_FILE);
- final String subject = host == null ? JMETER_SERVER_ALIAS : host;
- KeyStore keyStore = null;
- final String canonicalPath = certFile.getCanonicalPath();
- if (keyStorePass != null) { // Assume we have already created the store
- try {
- keyStore = getKeyStore(keyStorePass.toCharArray());
- } catch (Exception e) { // store is faulty, we need to recreate it
- log.warn(port + "Could not open expected file " + canonicalPath + " " + e.getMessage());
- }
- }
- if (keyStore == null) { // no existing file or not valid
- keyStorePass = RandomStringUtils.randomAscii(20);
- setPassword(keyStorePass);
- if (host != null) { // i.e. Java 7
- log.info(port + "Creating Proxy CA in " + canonicalPath);
- KeyToolUtils.generateProxyCA(certFile, keyStorePass, CERT_VALIDITY);
- log.info(port + "Creating entry " + subject + " in " + canonicalPath);
- KeyToolUtils.generateHostCert(certFile, keyStorePass, subject, CERT_VALIDITY);
- log.info(port + "Created keystore in " + canonicalPath);
- } else {
- log.info(port + "Generating standard keypair in " + canonicalPath);
- // Must not exist
- if(certFile.exists() && !certFile.delete()) {
- throw new IOException("Could not delete file:"+certFile.getAbsolutePath()+", this is needed for certificate generation");
- }
- KeyToolUtils.genkeypair(certFile, JMETER_SERVER_ALIAS, keyStorePass, CERT_VALIDITY, null, null);
- }
- keyStore = getKeyStore(keyStorePass.toCharArray()); // This should now work
- }
- // keyStorePass should not be null here; checking it avoids a possible NPE warning below
- if (keyStorePass != null && host != null && !keyStore.containsAlias(host)) {
- log.info(port + "Creating entry '" + host + "' in " + canonicalPath);
- // Requires Java 7
- KeyToolUtils.generateHostCert(certFile, keyStorePass, host, CERT_VALIDITY);
- keyStore = getKeyStore(keyStorePass.toCharArray()); // reload
- }
- return keyStore;
- }
/**
* Negotiate a SSL connection.
*
@@ -639,13 +594,4 @@ public class Proxy extends Thread {
}
return urlWithoutQuery;
}
-
- private String getPassword() {
- return prefs.get(USER_PASSWORD_KEY, null);
- }
-
- private void setPassword(String password) {
- prefs.put(USER_PASSWORD_KEY, password);
- }
-
}
Modified: jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
--- jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java (original)
+++ jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java Tue Sep 10 00:02:39 2013
@@ -18,9 +18,18 @@
package org.apache.jmeter.protocol.http.proxy;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
+import java.security.cert.X509Certificate;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.UnrecoverableKeyException;
import java.util.Collection;
+import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
@@ -29,7 +38,12 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.prefs.Preferences;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.time.DateUtils;
+import org.apache.http.conn.ssl.AbstractVerifier;
import org.apache.jmeter.assertions.ResponseAssertion;
import org.apache.jmeter.assertions.gui.AssertionGui;
import org.apache.jmeter.config.Arguments;
@@ -104,6 +118,8 @@ public class ProxyControl extends Generi
//+ JMX file attributes
private static final String PORT = "ProxyControlGui.port"; // $NON-NLS-1$
+ private static final String DOMAINS = "ProxyControlGui.domains"; // $NON-NLS-1$
+
private static final String EXCLUDE_LIST = "ProxyControlGui.exclude_list"; // $NON-NLS-1$
private static final String INCLUDE_LIST = "ProxyControlGui.include_list"; // $NON-NLS-1$
@@ -150,31 +166,66 @@ public class ProxyControl extends Generi
JMeterUtils.getPropDefault("proxy.pause", 1000); // $NON-NLS-1$
// Detect if user has pressed a new link
- public static final String CERT_ALIAS = JMeterUtils.getProperty("proxy.cert.alias"); // $NON-NLS-1$
+ // for ssl connection
+ private static final String KEYSTORE_TYPE =
+ JMeterUtils.getPropDefault("proxy.cert.type", "JKS"); // $NON-NLS-1$ $NON-NLS-2$
+
+ // Proxy configuration SSL
+ private static final String CERT_DIRECTORY =
+ JMeterUtils.getPropDefault("proxy.cert.directory", JMeterUtils.getJMeterBinDir()); // $NON-NLS-1$
+
+ private static final String CERT_FILE_DEFAULT = "proxyserver.jks";// $NON-NLS-1$
+
+ private static final String CERT_FILE =
+ JMeterUtils.getPropDefault("proxy.cert.file", CERT_FILE_DEFAULT); // $NON-NLS-1$
+
+ private static final File CERT_PATH = new File(CERT_DIRECTORY, CERT_FILE);
+
+ private static final String CERT_PATH_ABS = CERT_PATH.getAbsolutePath();
+
+ private static final String DEFAULT_PASSWORD = "password"; // $NON-NLS-1$
+
+ // Keys for user preferences
+ private static final String USER_PASSWORD_KEY = "proxy_cert_password";
+
+ private static final Preferences prefs = Preferences.userNodeForPackage(ProxyControl.class);
+ // Note: Windows user preferences are stored relative to: HKEY_CURRENT_USER\Software\JavaSoft\Prefs
+
+ // Whether to use dymanic key generation (if supported)
+ private static final boolean USE_DYNAMIC_KEYS = JMeterUtils.getPropDefault("proxy.cert.dynamic_keys", true); // $NON-NLS-1$;
+
+ // The alias to be used if dynamic host names are not possible
+ static final String JMETER_SERVER_ALIAS = ":jmeter:"; // $NON-NLS-1$
- public static enum KEYSTORE_IMPL {
+ static final int CERT_VALIDITY = JMeterUtils.getPropDefault("proxy.cert.validity", 7); // $NON-NLS-1$
+
+ static final String CERT_ALIAS = JMeterUtils.getProperty("proxy.cert.alias"); // $NON-NLS-1$
+
+ public static enum KeystoreMode {
USER_KEYSTORE, // user-provided keystore
JMETER_KEYSTORE, // keystore generated by JMeter; single entry
DYNAMIC_KEYSTORE
}
- public static final KEYSTORE_IMPL keystoreType;
+ static final KeystoreMode KEYSTORE_MODE;
static {
if (CERT_ALIAS != null) {
- log.info("Proxy Server will use the specified SSL keystore with the alias: '" + CERT_ALIAS + "'");
- keystoreType = KEYSTORE_IMPL.USER_KEYSTORE;
+ KEYSTORE_MODE = KeystoreMode.USER_KEYSTORE;
+ log.info("Proxy Server will use the keystore '"+ CERT_PATH_ABS + "' with the alias: '" + CERT_ALIAS + "'");
} else {
- if (KeyToolUtils.SUPPORTS_HOST_CERT) {
- keystoreType = KEYSTORE_IMPL.DYNAMIC_KEYSTORE;
- log.info("Java 7 detected: Proxy Server SSL Proxy will use keys that support embedded 3rd party resources");
+ if (KeyToolUtils.SUPPORTS_HOST_CERT && USE_DYNAMIC_KEYS) {
+ KEYSTORE_MODE = KeystoreMode.DYNAMIC_KEYSTORE;
+ log.info("Proxy Server SSL Proxy will use keys that support embedded 3rd party resources in file " + CERT_PATH_ABS);
} else {
- keystoreType = KEYSTORE_IMPL.JMETER_KEYSTORE;
- log.warn("Java 7 not detected: Proxy Server SSL Proxy will use keys that may not work for embedded resources");
+ KEYSTORE_MODE = KeystoreMode.JMETER_KEYSTORE;
+ log.warn("Proxy Server SSL Proxy will use keys that may not work for embedded resources in file " + CERT_PATH_ABS);
}
- }
+ }
}
+ private transient KeyStore keyStore;
+
private AtomicBoolean addAssertions = new AtomicBoolean(false);
private AtomicInteger groupingMode = new AtomicInteger(0);
@@ -196,6 +247,10 @@ public class ProxyControl extends Generi
*/
private JMeterTreeNode target;
+ private String storePassword;
+
+ private String keyPassword;
+
public ProxyControl() {
setPort(DEFAULT_PORT);
setExcludeList(new HashSet<String>());
@@ -211,6 +266,14 @@ public class ProxyControl extends Generi
setProperty(PORT, port);
}
+ public void setSslDomains(String domains) {
+ setProperty(DOMAINS, domains, "");
+ }
+
+ public String getSslDomains() {
+ return getPropertyAsString(DOMAINS,"");
+ }
+
public void setCaptureHttpHeaders(boolean capture) {
setProperty(new BooleanProperty(CAPTURE_HTTP_HEADERS, capture));
}
@@ -310,9 +373,9 @@ public class ProxyControl extends Generi
if (SAMPLER_TYPE_HTTP_SAMPLER_JAVA.equals(type)){
type = HTTPSamplerFactory.IMPL_JAVA;
} else if (SAMPLER_TYPE_HTTP_SAMPLER_HC3_1.equals(type)){
- type = HTTPSamplerFactory.IMPL_HTTP_CLIENT3_1;
+ type = HTTPSamplerFactory.IMPL_HTTP_CLIENT3_1;
} else if (SAMPLER_TYPE_HTTP_SAMPLER_HC4.equals(type)){
- type = HTTPSamplerFactory.IMPL_HTTP_CLIENT4;
+ type = HTTPSamplerFactory.IMPL_HTTP_CLIENT4;
}
return type;
}
@@ -350,6 +413,15 @@ public class ProxyControl extends Generi
}
public void startProxy() throws IOException {
+ try {
+ initKeyStore(); // TODO display warning dialog as this can take some time
+ } catch (GeneralSecurityException e) {
+ log.error("Could not initialise key store", e);
+ throw new IOException("Could not create keystore", e);
+ } catch (IOException e) { // make sure we log the error
+ log.error("Could not initialise key store", e);
+ throw e;
+ }
notifyTestListenersOfStart();
try {
server = new Daemon(getPort(), this);
@@ -419,14 +491,14 @@ public class ProxyControl extends Generi
Collection<ConfigTestElement> defaultConfigurations = (Collection<ConfigTestElement>) findApplicableElements(myTarget, ConfigTestElement.class, false);
@SuppressWarnings("unchecked") // OK, because find only returns correct element types
Collection<Arguments> userDefinedVariables = (Collection<Arguments>) findApplicableElements(myTarget, Arguments.class, true);
-
+
removeValuesFromSampler(sampler, defaultConfigurations);
replaceValues(sampler, subConfigs, userDefinedVariables);
sampler.setAutoRedirects(samplerRedirectAutomatically.get());
sampler.setFollowRedirects(samplerFollowRedirects.get());
sampler.setUseKeepAlive(useKeepAlive.get());
sampler.setImageParser(samplerDownloadImages.get());
-
+
placeSampler(sampler, subConfigs, myTarget);
} else {
if(log.isDebugEnabled()) {
@@ -509,13 +581,13 @@ public class ProxyControl extends Generi
if(log.isDebugEnabled()) {
log.debug("Content-type to filter : " + sampleContentType);
}
-
+
// Check if the include pattern is matched
boolean matched = testPattern(includeExp, sampleContentType, true);
if(!matched) {
return false;
}
-
+
// Check if the exclude pattern is matched
matched = testPattern(excludeExp, sampleContentType, false);
if(!matched) {
@@ -529,7 +601,7 @@ public class ProxyControl extends Generi
* Returns true if matching pattern was different from expectedToMatch
* @param expression Expression to match
* @param sampleContentType
- * @return boolean true if Matching expression
+ * @return boolean true if Matching expression
*/
private final boolean testPattern(String expression, String sampleContentType, boolean expectedToMatch) {
if(expression != null && expression.length() > 0) {
@@ -591,8 +663,8 @@ public class ProxyControl extends Generi
* Node in the tree where we will add the Controller
* @param name
* A name for the Controller
- * @throws InvocationTargetException
- * @throws InterruptedException
+ * @throws InvocationTargetException
+ * @throws InterruptedException
*/
private void addSimpleController(final JMeterTreeModel model, final JMeterTreeNode node, String name)
throws InterruptedException, InvocationTargetException {
@@ -621,8 +693,8 @@ public class ProxyControl extends Generi
* Node in the tree where we will add the Controller
* @param name
* A name for the Controller
- * @throws InvocationTargetException
- * @throws InterruptedException
+ * @throws InvocationTargetException
+ * @throws InterruptedException
*/
private void addTransactionController(final JMeterTreeModel model, final JMeterTreeNode node, String name)
throws InterruptedException, InvocationTargetException {
@@ -815,7 +887,7 @@ public class ProxyControl extends Generi
return elements;
}
- private void placeSampler(final HTTPSamplerBase sampler, final TestElement[] subConfigs,
+ private void placeSampler(final HTTPSamplerBase sampler, final TestElement[] subConfigs,
JMeterTreeNode myTarget) {
try {
final JMeterTreeModel treeModel = GuiPackage.getInstance().getTreeModel();
@@ -893,7 +965,7 @@ public class ProxyControl extends Generi
});
} catch (Exception e) {
JMeterUtils.reportErrorToUser(e.getMessage());
- }
+ }
}
/**
@@ -1070,4 +1142,178 @@ public class ProxyControl extends Generi
return null == server;
}
+ private void initKeyStore() throws IOException, GeneralSecurityException {
+ switch(KEYSTORE_MODE) {
+ case DYNAMIC_KEYSTORE:
+ storePassword = getPassword();
+ keyPassword = getPassword();
+ initDynamicKeyStore();
+ break;
+ case JMETER_KEYSTORE:
+ storePassword = getPassword();
+ keyPassword = getPassword();
+ initJMeterKeyStore();
+ break;
+ case USER_KEYSTORE:
+ storePassword = JMeterUtils.getPropDefault("proxy.cert.keystorepass", DEFAULT_PASSWORD); // $NON-NLS-1$;
+ keyPassword = JMeterUtils.getPropDefault("proxy.cert.keypassword", DEFAULT_PASSWORD); // $NON-NLS-1$;
+ log.info("Proxy Server will use the keystore '"+ CERT_PATH_ABS + "' with the alias: '" + CERT_ALIAS + "'");
+ initUserKeyStore();
+ break;
+ default:
+ throw new IllegalStateException("Impossible case: " + KEYSTORE_MODE);
+ }
+ }
+
+ /**
+ * Initialise the user-provided keystore
+ */
+ private void initUserKeyStore() {
+ try {
+ keyStore = getKeyStore(storePassword.toCharArray());
+ X509Certificate caCert = (X509Certificate) keyStore.getCertificate(CERT_ALIAS);
+ if (caCert == null) {
+ log.error("Could not find key with alias " + CERT_ALIAS);
+ keyStore = null;
+ } else {
+ caCert.checkValidity(new Date(System.currentTimeMillis()+DateUtils.MILLIS_PER_DAY));
+ }
+ } catch (Exception e) {
+ keyStore = null;
+ log.error("Could not open keystore or certificate is not valid " + CERT_PATH_ABS + " " + e.getMessage());
+ }
+ }
+
+ /**
+ * Initialise the dynamic domain keystore
+ */
+ private void initDynamicKeyStore() throws IOException, GeneralSecurityException {
+ if (storePassword != null) { // Assume we have already created the store
+ try {
+ keyStore = getKeyStore(storePassword.toCharArray());
+ X509Certificate caCert = (X509Certificate) keyStore.getCertificate(KeyToolUtils.CA_ALIAS);
+ if (caCert == null) {
+ keyStore = null; // no CA key - probably the wrong store type.
+ } else {
+ caCert.checkValidity(new Date(System.currentTimeMillis()+DateUtils.MILLIS_PER_DAY));
+ }
+ } catch (IOException e) { // store is faulty, we need to recreate it
+ keyStore = null; // if cert is not valid, flag up to recreate it
+ if (e.getCause() instanceof UnrecoverableKeyException) {
+ log.warn("Could not read key store " + e.getMessage() + " cause " + e.getCause().getMessage());
+ } else {
+ log.warn("Could not open/read key store " + e.getMessage()); // message includes the file name
+ }
+ } catch (GeneralSecurityException e) {
+ log.warn("Problem reading key store" + e.getMessage());
+ }
+ }
+ if (keyStore == null) { // no existing file or not valid
+ storePassword = RandomStringUtils.randomAlphanumeric(20); // Alphanum to avoid issues with command-line quoting
+ keyPassword = storePassword; // we use same password for both
+ setPassword(storePassword);
+ log.info("Creating Proxy CA in " + CERT_PATH_ABS);
+ KeyToolUtils.generateProxyCA(CERT_PATH, storePassword, CERT_VALIDITY);
+ log.info("Created keystore in " + CERT_PATH_ABS);
+ keyStore = getKeyStore(storePassword.toCharArray()); // This should now work
+ }
+ final String sslDomains = getSslDomains().trim();
+ if (sslDomains.length() > 0) {
+ final String[] domains = sslDomains.split(",");
+ // The subject may be either a host or a domain
+ for(String subject : domains) {
+ if (isValid(subject)) {
+ if (!keyStore.containsAlias(subject)) {
+ log.info("Creating entry " + subject + " in " + CERT_PATH_ABS);
+ KeyToolUtils.generateHostCert(CERT_PATH, storePassword, subject, CERT_VALIDITY);
+ keyStore = getKeyStore(storePassword.toCharArray()); // reload to pick up new aliases
+ // reloading is very quick compared with creating an entry currently
+ }
+ } else {
+ log.warn("Attempt to create an invalid domain certificate: " + subject);
+ }
+ }
+ }
+ }
+
+ private boolean isValid(String subject) {
+ String parts[] = subject.split("\\.");
+ if (!parts[0].endsWith("*")) { // not a wildcard
+ return true;
+ }
+ return parts.length >= 3 && AbstractVerifier.acceptableCountryWildcard(subject);
+ }
+
+ // This should only be called for a specific host
+ KeyStore updateKeyStore(String port, String host) throws IOException, GeneralSecurityException {
+ synchronized(CERT_PATH) { // ensure Proxy threads cannot interfere with each other
+ if (!keyStore.containsAlias(host)) {
+ log.info(port + "Creating entry " + host + " in " + CERT_PATH_ABS);
+ KeyToolUtils.generateHostCert(CERT_PATH, storePassword, host, CERT_VALIDITY);
+ }
+ keyStore = getKeyStore(storePassword.toCharArray()); // reload after adding alias
+ }
+ return keyStore;
+ }
+
+ /**
+ * Initialise the single key JMeter keystore (original behaviour)
+ */
+ private void initJMeterKeyStore() throws IOException, GeneralSecurityException {
+ if (storePassword != null) { // Assume we have already created the store
+ try {
+ keyStore = getKeyStore(storePassword.toCharArray());
+ X509Certificate caCert = (X509Certificate) keyStore.getCertificate(JMETER_SERVER_ALIAS);
+ caCert.checkValidity(new Date(System.currentTimeMillis()+DateUtils.MILLIS_PER_DAY));
+ } catch (Exception e) { // store is faulty, we need to recreate it
+ keyStore = null; // if cert is not valid, flag up to recreate it
+ log.warn("Could not open expected file or certificate is not valid " + CERT_PATH_ABS + " " + e.getMessage());
+ }
+ }
+ if (keyStore == null) { // no existing file or not valid
+ storePassword = RandomStringUtils.randomAlphanumeric(20); // Alphanum to avoid issues with command-line quoting
+ keyPassword = storePassword; // we use same password for both
+ setPassword(storePassword);
+ log.info("Generating standard keypair in " + CERT_PATH_ABS);
+ CERT_PATH.delete(); // safer to start afresh
+ KeyToolUtils.genkeypair(CERT_PATH, JMETER_SERVER_ALIAS, storePassword, CERT_VALIDITY, null, null);
+ keyStore = getKeyStore(storePassword.toCharArray()); // This should now work
+ }
+ }
+
+ private KeyStore getKeyStore(char[] password) throws GeneralSecurityException, IOException {
+ InputStream in = null;
+ try {
+ in = new BufferedInputStream(new FileInputStream(CERT_PATH));
+ log.debug("Opened Keystore file: " + CERT_PATH_ABS);
+ KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE);
+ ks.load(in, password);
+ log.debug("Loaded Keystore file: " + CERT_PATH_ABS);
+ return ks;
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ }
+
+ private String getPassword() {
+ return prefs.get(USER_PASSWORD_KEY, null);
+ }
+
+ private void setPassword(String password) {
+ prefs.put(USER_PASSWORD_KEY, password);
+ }
+
+ // the keystore for use by the Proxy
+ KeyStore getKeyStore() {
+ return keyStore;
+ }
+
+ String getKeyPassword() {
+ return keyPassword;
+ }
+
+ public static boolean isDynamicMode() {
+ return KEYSTORE_MODE == KeystoreMode.DYNAMIC_KEYSTORE;
+ }
+
}
Modified: jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
--- jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java (original)
+++ jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java Tue Sep 10 00:02:39 2013
@@ -19,6 +19,7 @@
package org.apache.jmeter.protocol.http.proxy.gui;
import java.awt.BorderLayout;
+import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
@@ -70,6 +71,7 @@ import org.apache.jmeter.testelement.Wor
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.gui.GuiUtils;
+import org.apache.jorphan.gui.JLabeledTextField;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;
@@ -89,6 +91,8 @@ public class ProxyControlGui extends Log
private JTextField portField;
+ private JLabeledTextField sslDomains;
+
/**
* Used to indicate that HTTP request headers should be captured. The
* default is to capture the HTTP request headers, which are specific to
@@ -230,6 +234,7 @@ public class ProxyControlGui extends Log
if (el instanceof ProxyControl) {
model = (ProxyControl) el;
model.setPort(portField.getText());
+ model.setSslDomains(sslDomains.getText());
setIncludeListInProxyControl(model);
setExcludeListInProxyControl(model);
model.setCaptureHttpHeaders(httpHeaders.isSelected());
@@ -294,6 +299,7 @@ public class ProxyControlGui extends Log
super.configure(element);
model = (ProxyControl) element;
portField.setText(model.getPortString());
+ sslDomains.setText(model.getSslDomains());
httpHeaders.setSelected(model.getCaptureHttpHeaders());
groupingMode.setSelectedIndex(model.getGroupingMode());
addAssertions.setSelected(model.getAssertions());
@@ -453,6 +459,10 @@ public class ProxyControlGui extends Log
private void startProxy() {
ValueReplacer replacer = GuiPackage.getInstance().getReplacer();
modifyTestElement(model);
+ // Proxy can take some while to start up; show a wating cursor
+ Cursor cursor = getCursor();
+ setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+ // TODO somehow show progress
try {
replacer.replaceValues(model);
model.startProxy();
@@ -474,6 +484,8 @@ public class ProxyControlGui extends Log
JMeterUtils.getResString("proxy_daemon_error"), // $NON-NLS-1$
"Error",
JOptionPane.ERROR_MESSAGE);
+ } finally {
+ setCursor(cursor);
}
}
@@ -584,9 +596,13 @@ public class ProxyControlGui extends Log
HorizontalPanel panel = new HorizontalPanel();
panel.add(label);
panel.add(portField);
+ panel.add(Box.createHorizontalStrut(10));
gPane.add(panel, BorderLayout.WEST);
- gPane.add(Box.createHorizontalStrut(10));
+
+ sslDomains = new JLabeledTextField(JMeterUtils.getResString("proxy_domains")); // $NON-NLS-1$
+ sslDomains.setEnabled(ProxyControl.isDynamicMode());
+ gPane.add(sslDomains, BorderLayout.CENTER);
return gPane;
}