mirror of https://github.com/apache/jmeter.git
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:
parent
0202d3d09d
commit
f440fc8fc3
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ?
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue