diff --git a/bin/jmeter.properties b/bin/jmeter.properties index e5bfbfbfff..b093963b4c 100644 --- a/bin/jmeter.properties +++ b/bin/jmeter.properties @@ -550,6 +550,9 @@ upgrade_properties=/bin/upgrade.properties #proxy.cert.alias= # 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 diff --git a/bin/proxyserver.jks b/bin/proxyserver.jks index a2f1dd9465..8d6fc05f53 100644 Binary files a/bin/proxyserver.jks and b/bin/proxyserver.jks differ diff --git a/src/core/org/apache/jmeter/resources/messages.properties b/src/core/org/apache/jmeter/resources/messages.properties index 8db69380df..4557c1a99f 100644 --- a/src/core/org/apache/jmeter/resources/messages.properties +++ b/src/core/org/apache/jmeter/resources/messages.properties @@ -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 diff --git a/src/core/org/apache/jmeter/resources/messages_fr.properties b/src/core/org/apache/jmeter/resources/messages_fr.properties index 9c23213a72..3d1b4151e6 100644 --- a/src/core/org/apache/jmeter/resources/messages_fr.properties +++ b/src/core/org/apache/jmeter/resources/messages_fr.properties @@ -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 ? diff --git a/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java b/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java index 91a19073d6..bed2e841e3 100644 --- a/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java +++ b/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java @@ -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 hashHost = new HashMap(); - // 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); - } - } diff --git a/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java b/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java index 64d67a18b0..226aa42feb 100644 --- a/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java +++ b/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java @@ -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()); @@ -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 defaultConfigurations = (Collection) findApplicableElements(myTarget, ConfigTestElement.class, false); @SuppressWarnings("unchecked") // OK, because find only returns correct element types Collection userDefinedVariables = (Collection) 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; + } + } diff --git a/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java b/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java index 07b1f96de4..bd86864c62 100644 --- a/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java +++ b/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java @@ -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; }