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


git-svn-id: https://svn.apache.org/repos/asf/jmeter/trunk@1521320 13f79535-47bb-0310-9956-ffa450edef68

Former-commit-id: 80e2196c14
This commit is contained in:
Sebastian Bazley 2013-09-10 00:02:39 +00:00
parent 0202d3d09d
commit f440fc8fc3
7 changed files with 389 additions and 176 deletions

View File

@ -550,6 +550,9 @@ upgrade_properties=/bin/upgrade.properties
#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

Binary file not shown.

View File

@ -723,6 +723,7 @@ proxy_content_type_filter=Content-type filter
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

View File

@ -716,6 +716,7 @@ proxy_content_type_filter=Filtre de type de contenu
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 ?

View File

@ -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.util.HTTPConstants;
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
* 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 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;
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;
// 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;
}
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);
}
}
// 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);
}
}

View File

@ -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 GenericController {
//+ 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 GenericController {
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$
public static enum KEYSTORE_IMPL {
// 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$
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 GenericController {
*/
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 GenericController {
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 GenericController {
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 GenericController {
}
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 GenericController {
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 GenericController {
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 GenericController {
* 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 GenericController {
* 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 GenericController {
* 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 GenericController {
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 GenericController {
});
} catch (Exception e) {
JMeterUtils.reportErrorToUser(e.getMessage());
}
}
}
/**
@ -1070,4 +1142,178 @@ public class ProxyControl extends GenericController {
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;
}
}

View File

@ -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.WorkBench;
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 LogicControllerGui implements JMeterGUIComp
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 LogicControllerGui implements JMeterGUIComp
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 LogicControllerGui implements JMeterGUIComp
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 LogicControllerGui implements JMeterGUIComp
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 LogicControllerGui implements JMeterGUIComp
JMeterUtils.getResString("proxy_daemon_error"), // $NON-NLS-1$
"Error",
JOptionPane.ERROR_MESSAGE);
} finally {
setCursor(cursor);
}
}
@ -584,9 +596,13 @@ public class ProxyControlGui extends LogicControllerGui implements JMeterGUIComp
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;
}