You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jmeter.apache.org by se...@apache.org on 2013/09/10 02:02:40 UTC

svn commit: r1521320 - in /jmeter/trunk: bin/ src/core/org/apache/jmeter/resources/ src/protocol/http/org/apache/jmeter/protocol/http/proxy/ src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/

Author: sebb
Date: Tue Sep 10 00:02:39 2013
New Revision: 1521320

URL: http://svn.apache.org/r1521320
Log:
Proxy SSL recording does not handle external embedded resources well
Reworked; setup is now done by ProxyControl on pressing Start
TODO: better notification of progress of keystore init
Bugzilla Id: 55507

Modified:
    jmeter/trunk/bin/jmeter.properties
    jmeter/trunk/bin/proxyserver.jks
    jmeter/trunk/src/core/org/apache/jmeter/resources/messages.properties
    jmeter/trunk/src/core/org/apache/jmeter/resources/messages_fr.properties
    jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java
    jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java
    jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java

Modified: jmeter/trunk/bin/jmeter.properties
URL: http://svn.apache.org/viewvc/jmeter/trunk/bin/jmeter.properties?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
--- jmeter/trunk/bin/jmeter.properties (original)
+++ jmeter/trunk/bin/jmeter.properties Tue Sep 10 00:02:39 2013
@@ -550,6 +550,9 @@ upgrade_properties=/bin/upgrade.properti
 #proxy.cert.alias=<none>
 # The default validity for certificates created by JMeter
 #proxy.cert.validity=7
+# Use dynamic key generation (if supported by JMeter/JVM)
+# If false, will revert to using a single key with no certificate
+#proxy.cert.dynamic_keys=true
 
 # SSL configuration
 #proxy.ssl.protocol=SSLv3

Modified: jmeter/trunk/bin/proxyserver.jks
URL: http://svn.apache.org/viewvc/jmeter/trunk/bin/proxyserver.jks?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
Binary files - no diff available.

Modified: jmeter/trunk/src/core/org/apache/jmeter/resources/messages.properties
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/resources/messages.properties?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/resources/messages.properties (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/resources/messages.properties Tue Sep 10 00:02:39 2013
@@ -723,6 +723,7 @@ proxy_content_type_filter=Content-type f
 proxy_content_type_include=Include\:
 proxy_daemon_bind_error=Could not create proxy - port in use. Choose another port.
 proxy_daemon_error=Could not create proxy - see log for details
+proxy_domains=HTTPS Domains \:
 proxy_general_settings=Global Settings
 proxy_headers=Capture HTTP Headers
 proxy_regex=Regex matching

Modified: jmeter/trunk/src/core/org/apache/jmeter/resources/messages_fr.properties
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/resources/messages_fr.properties?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/resources/messages_fr.properties (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/resources/messages_fr.properties Tue Sep 10 00:02:39 2013
@@ -716,6 +716,7 @@ proxy_content_type_filter=Filtre de type
 proxy_content_type_include=Inclure \:
 proxy_daemon_bind_error=Impossible de lancer le serveur proxy, le port est d\u00E9j\u00E0 utilis\u00E9. Choisissez un autre port.
 proxy_daemon_error=Impossible de lancer le serveur proxy, voir le journal pour plus de d\u00E9tails
+proxy_domains=Domaines HTTPS \:
 proxy_general_settings=Param\u00E8tres g\u00E9n\u00E9raux
 proxy_headers=Capturer les ent\u00EAtes HTTP
 proxy_regex=Correspondance des variables par regex ?

Modified: jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
--- jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java (original)
+++ jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java Tue Sep 10 00:02:39 2013
@@ -22,11 +22,7 @@ import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.DataOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.PrintStream;
 import java.net.Socket;
@@ -34,9 +30,9 @@ import java.net.URL;
 import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
 import java.security.KeyStore;
+import java.security.KeyStoreException;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.prefs.Preferences;
 
 import javax.net.ssl.KeyManager;
 import javax.net.ssl.KeyManagerFactory;
@@ -44,8 +40,6 @@ import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
 
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.jmeter.protocol.http.control.HeaderManager;
 import org.apache.jmeter.protocol.http.parser.HTMLParseException;
 import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
@@ -54,7 +48,6 @@ import org.apache.jmeter.protocol.http.u
 import org.apache.jmeter.samplers.SampleResult;
 import org.apache.jmeter.testelement.TestElement;
 import org.apache.jmeter.util.JMeterUtils;
-import org.apache.jorphan.exec.KeyToolUtils;
 import org.apache.jorphan.logging.LoggingManager;
 import org.apache.jorphan.util.JMeterException;
 import org.apache.jorphan.util.JOrphanUtils;
@@ -84,10 +77,6 @@ public class Proxy extends Thread {
 
     private static final String PROXY_HEADERS_REMOVE_SEPARATOR = ","; // $NON-NLS-1$
 
-    // for ssl connection
-    private static final String KEYSTORE_TYPE =
-        JMeterUtils.getPropDefault("proxy.cert.type", "JKS"); // $NON-NLS-1$ $NON-NLS-2$
-
     private static final String KEYMANAGERFACTORY =
         JMeterUtils.getPropDefault("proxy.cert.factory", "SunX509"); // $NON-NLS-1$ $NON-NLS-2$
 
@@ -97,30 +86,8 @@ public class Proxy extends Thread {
     // HashMap to save ssl connection between Jmeter proxy and browser
     private static final HashMap<String, SSLSocketFactory> hashHost = new HashMap<String, SSLSocketFactory>();
 
-    // Proxy configuration SSL
-    private static final String CERT_DIRECTORY =
-        JMeterUtils.getPropDefault("proxy.cert.directory", JMeterUtils.getJMeterBinDir()); // $NON-NLS-1$
-
-    private static final String CERT_FILE_DEFAULT = "proxyserver.jks";// $NON-NLS-1$
-
-    private static final String CERT_FILE =
-        JMeterUtils.getPropDefault("proxy.cert.file", CERT_FILE_DEFAULT); // $NON-NLS-1$
-
-    // The alias to be used if dynamic host names are not possible
-    private static final String JMETER_SERVER_ALIAS = ":jmeter:"; // $NON-NLS-1$
-
-    private static final int CERT_VALIDITY = JMeterUtils.getPropDefault("proxy.cert.validity", 7); // $NON-NLS-1$
-
-    private static final String DEFAULT_PASSWORD = "password"; // $NON-NLS-1$
-
     private static final SamplerCreatorFactory factory = new SamplerCreatorFactory();
 
-    // Keys for user preferences
-    private static final String USER_PASSWORD_KEY = "proxy_cert_PASSWORD";
-
-    private static final Preferences prefs = Preferences.userNodeForPackage(Proxy.class);
-    // Note: Windows user preferences are stored relative to: HKEY_CURRENT_USER\Software\JavaSoft\Prefs
-
     // Use with SSL connection
     private OutputStream outStreamClient = null;
 
@@ -146,6 +113,10 @@ public class Proxy extends Thread {
 
     private String port; // For identifying log messages
 
+    private KeyStore keyStore; // keystore for SSL keys; fixed at config except for dynamic host key generation
+
+    private String keyPassword;
+
     /**
      * Default constructor - used by newInstance call in Daemon
      */
@@ -175,6 +146,8 @@ public class Proxy extends Thread {
         this.pageEncodings = _pageEncodings;
         this.formEncodings = _formEncodings;
         this.port = "["+ clientSocket.getPort() + "] ";
+        this.keyStore = _target.getKeyStore();
+        this.keyPassword = _target.getKeyPassword();
     }
 
     /**
@@ -315,17 +288,57 @@ public class Proxy extends Thread {
      * @throws IOException
      */
     private SSLSocketFactory getSSLSocketFactory(String host) {
+        if (keyStore == null) {
+            log.error(port + "No keystore available, cannot record SSL");
+            return null;
+        }
+        final String hashAlias;
+        final String keyAlias;
+        switch(ProxyControl.KEYSTORE_MODE) {
+        case DYNAMIC_KEYSTORE:
+            try {
+                keyStore = target.getKeyStore(); // pick up any recent changes from other threads
+                String alias = getDomainMatch(keyStore, host);
+                if (alias == null) {
+                    hashAlias = host;
+                    keyAlias = host;
+                    keyStore = target.updateKeyStore(port, keyAlias);
+                } else if (alias.equals(host)) { // the host has a key already
+                    hashAlias = host;
+                    keyAlias = host;
+                } else { // the host matches a domain; use its key
+                    hashAlias = alias;
+                    keyAlias = alias;
+                }
+            } catch (IOException e) {
+                log.error(port + "Problem with keystore", e);
+                return null;
+            } catch (GeneralSecurityException e) {
+                log.error(port + "Problem with keystore", e);
+                return null;
+            }
+            break;
+        case JMETER_KEYSTORE:
+            hashAlias = keyAlias = ProxyControl.JMETER_SERVER_ALIAS;
+            break;
+        case USER_KEYSTORE:
+            hashAlias = keyAlias = ProxyControl.CERT_ALIAS;
+            break;
+        default:
+            throw new IllegalStateException("Impossible case: " + ProxyControl.KEYSTORE_MODE);
+        }
         synchronized (hashHost) {
-            if (hashHost.containsKey(host)) {
-                log.debug(port + "Good, already in map, host=" + host);
-                return hashHost.get(host);
+            final SSLSocketFactory sslSocketFactory = hashHost.get(hashAlias);
+            if (sslSocketFactory != null) {
+                log.debug(port + "Good, already in map, host=" + host + " using alias " + hashAlias);
+                return sslSocketFactory;
             }
             try {
                 SSLContext sslcontext = SSLContext.getInstance(SSLCONTEXT_PROTOCOL);
-                sslcontext.init(getKeyManagers(host), null, null);
+                sslcontext.init(getWrappedKeyManagers(keyAlias), null, null);
                 SSLSocketFactory sslFactory = sslcontext.getSocketFactory();
-                hashHost.put(host, sslFactory);
-                log.info(port + "KeyStore for SSL loaded OK and put host in map ("+host+")");
+                hashHost.put(hashAlias, sslFactory);
+                log.info(port + "KeyStore for SSL loaded OK and put host '" + host + "' in map with key ("+hashAlias+")");
                 return sslFactory;
             } catch (GeneralSecurityException e) {
                 log.error(port + "Problem with SSL certificate", e);
@@ -337,115 +350,57 @@ public class Proxy extends Thread {
     }
 
     /**
-     * Return the key managers, wrapped if necessary to return a specific alias
-     * 
-     * @param serverAlias the alias to return, or null to use whatever is present
-     * @param host the target host
-     * @return the key managers
-     * @throws GeneralSecurityException
-     * @throws IOException if the store cannot be opened or read or the alias is missing
-     */
-    private KeyManager[] getKeyManagers(String host) throws GeneralSecurityException, IOException {
-        final KeyStore ks;
-        final String serverAlias;
-        String keyPass;
-        switch(ProxyControl.keystoreType) {
-        case JMETER_KEYSTORE:
-            ks = getJMeterKeyStore(getPassword(), (String) null);
-            keyPass = getPassword(); // above call may have updated the stored password
-            serverAlias = JMETER_SERVER_ALIAS;
-            break;
-        case DYNAMIC_KEYSTORE:
-            ks = getJMeterKeyStore(getPassword(), host);
-            keyPass = getPassword(); // above call may have updated the stored password
-            serverAlias = host;
-            break;
-        case USER_KEYSTORE:
-        default: // Not really needed, but avoids complaints about non-init password strings
-            String keyStorePass = JMeterUtils.getPropDefault("proxy.cert.keystorepass", DEFAULT_PASSWORD); // $NON-NLS-1$
-            ks = getKeyStore(keyStorePass.toCharArray());
-            keyPass = JMeterUtils.getPropDefault("proxy.cert.keypassword", DEFAULT_PASSWORD); // $NON-NLS-1$
-            serverAlias = ProxyControl.CERT_ALIAS;
-            break;
+     * Get matching alias for a host from keyStore that may contain domain aliases.
+     * Assumes domains must have at least 2 parts (apache.org);
+     * does not check if TLD requires more (google.co.uk).
+     * ProxyControl checks for valid domains before adding them, and any subsequent
+     * additions by the Proxy class will be hosts, not domains.
+     * @param keyStore the KeyStore to search
+     * @param host the hostname to match
+     * @return
+     * @throws KeyStoreException 
+     */
+    private String getDomainMatch(KeyStore keyStore, String host) throws KeyStoreException {
+        if (keyStore.containsAlias(host)) {
+            return host;
+        }
+        String parts[] = host.split("\\."); // get the component parts
+        // Assume domains must have at least 2 parts, e.g. apache.org
+        // Don't try matching against *.org; however we don't check *.co.uk here
+        for(int i = 1; i <= parts.length - 2; i++) {
+            StringBuilder sb = new StringBuilder("*");
+            for(int j = i; j < parts.length ; j++) { // add the remaining parts
+                sb.append('.');
+                sb.append(parts[j]);
+            }
+            String alias = sb.toString();
+            if (keyStore.containsAlias(alias)) {
+                return alias;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the key managers, wrapped to return a specific alias
+     */
+    private KeyManager[] getWrappedKeyManagers(final String keyAlias)
+            throws GeneralSecurityException, IOException {
+        if (!keyStore.containsAlias(keyAlias)) {
+            throw new IOException("Keystore does not contain alias " + keyAlias);
         }
         KeyManagerFactory kmf = KeyManagerFactory.getInstance(KEYMANAGERFACTORY);
-        kmf.init(ks, keyPass.toCharArray());
+        kmf.init(keyStore, keyPassword.toCharArray());
         final KeyManager[] keyManagers = kmf.getKeyManagers();
-        if (serverAlias == null) {
-            return keyManagers;
-        } else {
-            // Check if alias is suitable here, rather than waiting for connection to fail
-            if (!ks.containsAlias(serverAlias)) {
-                throw new IOException("Keystore does not contain alias " + serverAlias);
-            }
-            final int keyManagerCount = keyManagers.length;
-            final KeyManager[] wrappedKeyManagers = new KeyManager[keyManagerCount];
-            for (int i =0; i < keyManagerCount; i++) {
-                wrappedKeyManagers[i] = new ServerAliasKeyManager(keyManagers[i], serverAlias);
-            }
-            return wrappedKeyManagers;
-        }
-    }
-
-    private KeyStore getKeyStore(char[] password) throws GeneralSecurityException, IOException {
-        File certFile = new File(CERT_DIRECTORY, CERT_FILE);
-        InputStream in = null;
-        final String certPath = certFile.getAbsolutePath();
-        try {
-            in = new BufferedInputStream(new FileInputStream(certFile));
-            log.info(port + "Opened Keystore file: " + certPath);
-            KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE);
-            ks.load(in, password);
-            return ks;
-        } catch (FileNotFoundException e) {
-            log.error(port + "Could not open Keystore file: " + certPath, e);
-            throw e;
-        } finally {
-            IOUtils.closeQuietly(in);
+        // Check if alias is suitable here, rather than waiting for connection to fail
+        final int keyManagerCount = keyManagers.length;
+        final KeyManager[] wrappedKeyManagers = new KeyManager[keyManagerCount];
+        for (int i =0; i < keyManagerCount; i++) {
+            wrappedKeyManagers[i] = new ServerAliasKeyManager(keyManagers[i], keyAlias);
         }
+        return wrappedKeyManagers;
     }
 
-    // If host == null, we are not using dynamic keys
-    private KeyStore getJMeterKeyStore(String keyStorePass, String host) throws GeneralSecurityException, IOException {
-        final File certFile = new File(CERT_DIRECTORY, CERT_FILE);
-        final String subject = host == null ? JMETER_SERVER_ALIAS : host;
-        KeyStore keyStore = null;
-        final String canonicalPath = certFile.getCanonicalPath();
-        if (keyStorePass != null) { // Assume we have already created the store
-            try {
-                keyStore = getKeyStore(keyStorePass.toCharArray());
-            } catch (Exception e) { // store is faulty, we need to recreate it
-                log.warn(port + "Could not open expected file " + canonicalPath + " " + e.getMessage());            
-            }
-        }
-        if (keyStore == null) { // no existing file or not valid
-            keyStorePass = RandomStringUtils.randomAscii(20);
-            setPassword(keyStorePass);
-            if (host != null) { // i.e. Java 7
-                log.info(port + "Creating Proxy CA in " + canonicalPath);
-                KeyToolUtils.generateProxyCA(certFile, keyStorePass, CERT_VALIDITY);
-                log.info(port + "Creating entry " + subject + " in " + canonicalPath);
-                KeyToolUtils.generateHostCert(certFile, keyStorePass, subject, CERT_VALIDITY);
-                log.info(port + "Created keystore in " + canonicalPath);
-            } else {
-                log.info(port + "Generating standard keypair in " + canonicalPath);
-                // Must not exist
-                if(certFile.exists() && !certFile.delete()) {
-                    throw new IOException("Could not delete file:"+certFile.getAbsolutePath()+", this is needed for certificate generation");
-                }
-                KeyToolUtils.genkeypair(certFile, JMETER_SERVER_ALIAS, keyStorePass, CERT_VALIDITY, null, null);                    
-            }
-            keyStore = getKeyStore(keyStorePass.toCharArray()); // This should now work
-        }
-        // keyStorePass should not be null here; checking it avoids a possible NPE warning below
-        if (keyStorePass != null && host != null && !keyStore.containsAlias(host)) {
-            log.info(port + "Creating entry '" + host + "' in " + canonicalPath);
-        // Requires Java 7
-            KeyToolUtils.generateHostCert(certFile, keyStorePass, host, CERT_VALIDITY);
-            keyStore = getKeyStore(keyStorePass.toCharArray()); // reload
-        }
-        return keyStore;
-    }
     /**
      * Negotiate a SSL connection.
      *
@@ -639,13 +594,4 @@ public class Proxy extends Thread {
         }
         return urlWithoutQuery;
     }
-
-    private String getPassword() {
-        return prefs.get(USER_PASSWORD_KEY, null);
-    }
-
-    private void setPassword(String password) {
-        prefs.put(USER_PASSWORD_KEY, password);        
-    }
-
 }

Modified: jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
--- jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java (original)
+++ jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/ProxyControl.java Tue Sep 10 00:02:39 2013
@@ -18,9 +18,18 @@
 
 package org.apache.jmeter.protocol.http.proxy;
 
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.lang.reflect.InvocationTargetException;
+import java.security.cert.X509Certificate;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.UnrecoverableKeyException;
 import java.util.Collection;
+import java.util.Date;
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -29,7 +38,12 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.prefs.Preferences;
 
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.time.DateUtils;
+import org.apache.http.conn.ssl.AbstractVerifier;
 import org.apache.jmeter.assertions.ResponseAssertion;
 import org.apache.jmeter.assertions.gui.AssertionGui;
 import org.apache.jmeter.config.Arguments;
@@ -104,6 +118,8 @@ public class ProxyControl extends Generi
     //+ JMX file attributes
     private static final String PORT = "ProxyControlGui.port"; // $NON-NLS-1$
 
+    private static final String DOMAINS = "ProxyControlGui.domains"; // $NON-NLS-1$
+
     private static final String EXCLUDE_LIST = "ProxyControlGui.exclude_list"; // $NON-NLS-1$
 
     private static final String INCLUDE_LIST = "ProxyControlGui.include_list"; // $NON-NLS-1$
@@ -150,31 +166,66 @@ public class ProxyControl extends Generi
         JMeterUtils.getPropDefault("proxy.pause", 1000); // $NON-NLS-1$
     // Detect if user has pressed a new link
 
-    public static final String CERT_ALIAS = JMeterUtils.getProperty("proxy.cert.alias"); // $NON-NLS-1$
+    // for ssl connection
+    private static final String KEYSTORE_TYPE =
+        JMeterUtils.getPropDefault("proxy.cert.type", "JKS"); // $NON-NLS-1$ $NON-NLS-2$
+
+    // Proxy configuration SSL
+    private static final String CERT_DIRECTORY =
+        JMeterUtils.getPropDefault("proxy.cert.directory", JMeterUtils.getJMeterBinDir()); // $NON-NLS-1$
+
+    private static final String CERT_FILE_DEFAULT = "proxyserver.jks";// $NON-NLS-1$
+
+    private static final String CERT_FILE =
+        JMeterUtils.getPropDefault("proxy.cert.file", CERT_FILE_DEFAULT); // $NON-NLS-1$
+
+    private static final File CERT_PATH = new File(CERT_DIRECTORY, CERT_FILE);
+
+    private static final String CERT_PATH_ABS = CERT_PATH.getAbsolutePath();
+
+    private static final String DEFAULT_PASSWORD = "password"; // $NON-NLS-1$
+
+    // Keys for user preferences
+    private static final String USER_PASSWORD_KEY = "proxy_cert_password";
+
+    private static final Preferences prefs = Preferences.userNodeForPackage(ProxyControl.class);
+    // Note: Windows user preferences are stored relative to: HKEY_CURRENT_USER\Software\JavaSoft\Prefs
+
+    // Whether to use dymanic key generation (if supported)
+    private static final boolean USE_DYNAMIC_KEYS = JMeterUtils.getPropDefault("proxy.cert.dynamic_keys", true); // $NON-NLS-1$;
+
+    // The alias to be used if dynamic host names are not possible
+    static final String JMETER_SERVER_ALIAS = ":jmeter:"; // $NON-NLS-1$
 
-    public static enum KEYSTORE_IMPL {
+    static final int CERT_VALIDITY = JMeterUtils.getPropDefault("proxy.cert.validity", 7); // $NON-NLS-1$
+
+    static final String CERT_ALIAS = JMeterUtils.getProperty("proxy.cert.alias"); // $NON-NLS-1$
+
+    public static enum KeystoreMode {
         USER_KEYSTORE,   // user-provided keystore
         JMETER_KEYSTORE, // keystore generated by JMeter; single entry
         DYNAMIC_KEYSTORE
     }
 
-    public static final KEYSTORE_IMPL keystoreType;
+    static final KeystoreMode KEYSTORE_MODE;
     
     static {
         if (CERT_ALIAS != null) {
-            log.info("Proxy Server will use the specified SSL keystore with the alias: '" + CERT_ALIAS + "'");
-            keystoreType = KEYSTORE_IMPL.USER_KEYSTORE;
+            KEYSTORE_MODE = KeystoreMode.USER_KEYSTORE;
+            log.info("Proxy Server will use the keystore '"+ CERT_PATH_ABS + "' with the alias: '" + CERT_ALIAS + "'");
         } else {
-            if (KeyToolUtils.SUPPORTS_HOST_CERT) {
-                keystoreType = KEYSTORE_IMPL.DYNAMIC_KEYSTORE;
-                log.info("Java 7 detected: Proxy Server SSL Proxy will use keys that support embedded 3rd party resources");                
+            if (KeyToolUtils.SUPPORTS_HOST_CERT && USE_DYNAMIC_KEYS) {
+                KEYSTORE_MODE = KeystoreMode.DYNAMIC_KEYSTORE;
+                log.info("Proxy Server SSL Proxy will use keys that support embedded 3rd party resources in file " + CERT_PATH_ABS);
             } else {
-                keystoreType = KEYSTORE_IMPL.JMETER_KEYSTORE;
-               log.warn("Java 7 not detected: Proxy Server SSL Proxy will use keys that may not work for embedded resources");
+                KEYSTORE_MODE = KeystoreMode.JMETER_KEYSTORE;
+                log.warn("Proxy Server SSL Proxy will use keys that may not work for embedded resources in file " + CERT_PATH_ABS);
             }
-        }        
+        }
     }
 
+    private transient KeyStore keyStore;
+
     private AtomicBoolean addAssertions = new AtomicBoolean(false);
 
     private AtomicInteger groupingMode = new AtomicInteger(0);
@@ -196,6 +247,10 @@ public class ProxyControl extends Generi
      */
     private JMeterTreeNode target;
 
+    private String storePassword;
+
+    private String keyPassword;
+
     public ProxyControl() {
         setPort(DEFAULT_PORT);
         setExcludeList(new HashSet<String>());
@@ -211,6 +266,14 @@ public class ProxyControl extends Generi
         setProperty(PORT, port);
     }
 
+    public void setSslDomains(String domains) {
+        setProperty(DOMAINS, domains, "");
+    }
+
+    public String getSslDomains() {
+        return getPropertyAsString(DOMAINS,"");
+    }
+
     public void setCaptureHttpHeaders(boolean capture) {
         setProperty(new BooleanProperty(CAPTURE_HTTP_HEADERS, capture));
     }
@@ -310,9 +373,9 @@ public class ProxyControl extends Generi
         if (SAMPLER_TYPE_HTTP_SAMPLER_JAVA.equals(type)){
             type = HTTPSamplerFactory.IMPL_JAVA;
         } else if (SAMPLER_TYPE_HTTP_SAMPLER_HC3_1.equals(type)){
-            type = HTTPSamplerFactory.IMPL_HTTP_CLIENT3_1;            
+            type = HTTPSamplerFactory.IMPL_HTTP_CLIENT3_1;
         } else if (SAMPLER_TYPE_HTTP_SAMPLER_HC4.equals(type)){
-            type = HTTPSamplerFactory.IMPL_HTTP_CLIENT4;       
+            type = HTTPSamplerFactory.IMPL_HTTP_CLIENT4;
         }
         return type;
     }
@@ -350,6 +413,15 @@ public class ProxyControl extends Generi
     }
 
     public void startProxy() throws IOException {
+        try {
+            initKeyStore(); // TODO display warning dialog as this can take some time
+        } catch (GeneralSecurityException e) {
+            log.error("Could not initialise key store", e);
+            throw new IOException("Could not create keystore", e);
+        } catch (IOException e) { // make sure we log the error
+            log.error("Could not initialise key store", e);
+            throw e;
+        }
         notifyTestListenersOfStart();
         try {
             server = new Daemon(getPort(), this);
@@ -419,14 +491,14 @@ public class ProxyControl extends Generi
                 Collection<ConfigTestElement> defaultConfigurations = (Collection<ConfigTestElement>) findApplicableElements(myTarget, ConfigTestElement.class, false);
                 @SuppressWarnings("unchecked") // OK, because find only returns correct element types
                 Collection<Arguments> userDefinedVariables = (Collection<Arguments>) findApplicableElements(myTarget, Arguments.class, true);
-    
+
                 removeValuesFromSampler(sampler, defaultConfigurations);
                 replaceValues(sampler, subConfigs, userDefinedVariables);
                 sampler.setAutoRedirects(samplerRedirectAutomatically.get());
                 sampler.setFollowRedirects(samplerFollowRedirects.get());
                 sampler.setUseKeepAlive(useKeepAlive.get());
                 sampler.setImageParser(samplerDownloadImages.get());
-    
+
                 placeSampler(sampler, subConfigs, myTarget);
             } else {
                 if(log.isDebugEnabled()) {
@@ -509,13 +581,13 @@ public class ProxyControl extends Generi
         if(log.isDebugEnabled()) {
             log.debug("Content-type to filter : " + sampleContentType);
         }
-        
+
         // Check if the include pattern is matched
         boolean matched = testPattern(includeExp, sampleContentType, true);
         if(!matched) {
             return false;
         }
-        
+
         // Check if the exclude pattern is matched
         matched = testPattern(excludeExp, sampleContentType, false);
         if(!matched) {
@@ -529,7 +601,7 @@ public class ProxyControl extends Generi
      * Returns true if matching pattern was different from expectedToMatch
      * @param expression Expression to match
      * @param sampleContentType
-     * @return boolean true if Matching expression 
+     * @return boolean true if Matching expression
      */
     private final boolean testPattern(String expression, String sampleContentType, boolean expectedToMatch) {
         if(expression != null && expression.length() > 0) {
@@ -591,8 +663,8 @@ public class ProxyControl extends Generi
      *            Node in the tree where we will add the Controller
      * @param name
      *            A name for the Controller
-     * @throws InvocationTargetException 
-     * @throws InterruptedException 
+     * @throws InvocationTargetException
+     * @throws InterruptedException
      */
     private void addSimpleController(final JMeterTreeModel model, final JMeterTreeNode node, String name)
             throws InterruptedException, InvocationTargetException {
@@ -621,8 +693,8 @@ public class ProxyControl extends Generi
      *            Node in the tree where we will add the Controller
      * @param name
      *            A name for the Controller
-     * @throws InvocationTargetException 
-     * @throws InterruptedException 
+     * @throws InvocationTargetException
+     * @throws InterruptedException
      */
     private void addTransactionController(final JMeterTreeModel model, final JMeterTreeNode node, String name)
             throws InterruptedException, InvocationTargetException {
@@ -815,7 +887,7 @@ public class ProxyControl extends Generi
         return elements;
     }
 
-    private void placeSampler(final HTTPSamplerBase sampler, final TestElement[] subConfigs, 
+    private void placeSampler(final HTTPSamplerBase sampler, final TestElement[] subConfigs,
             JMeterTreeNode myTarget) {
         try {
             final JMeterTreeModel treeModel = GuiPackage.getInstance().getTreeModel();
@@ -893,7 +965,7 @@ public class ProxyControl extends Generi
             });
         } catch (Exception e) {
             JMeterUtils.reportErrorToUser(e.getMessage());
-        } 
+        }
     }
 
     /**
@@ -1070,4 +1142,178 @@ public class ProxyControl extends Generi
         return null == server;
     }
 
+    private void initKeyStore() throws IOException, GeneralSecurityException {
+        switch(KEYSTORE_MODE) {
+        case DYNAMIC_KEYSTORE:
+            storePassword = getPassword();
+            keyPassword = getPassword();
+            initDynamicKeyStore();
+            break;
+        case JMETER_KEYSTORE:
+            storePassword = getPassword();
+            keyPassword = getPassword();
+            initJMeterKeyStore();
+            break;
+        case USER_KEYSTORE:
+            storePassword = JMeterUtils.getPropDefault("proxy.cert.keystorepass", DEFAULT_PASSWORD); // $NON-NLS-1$;
+            keyPassword = JMeterUtils.getPropDefault("proxy.cert.keypassword", DEFAULT_PASSWORD); // $NON-NLS-1$;
+            log.info("Proxy Server will use the keystore '"+ CERT_PATH_ABS + "' with the alias: '" + CERT_ALIAS + "'");
+            initUserKeyStore();
+            break;
+        default:
+            throw new IllegalStateException("Impossible case: " + KEYSTORE_MODE);        
+        }
+    }
+
+    /**
+     * Initialise the user-provided keystore
+     */
+    private void initUserKeyStore() {
+        try {
+            keyStore = getKeyStore(storePassword.toCharArray());
+            X509Certificate  caCert = (X509Certificate) keyStore.getCertificate(CERT_ALIAS);
+            if (caCert == null) {
+                log.error("Could not find key with alias " + CERT_ALIAS);
+                keyStore = null;
+            } else {
+                caCert.checkValidity(new Date(System.currentTimeMillis()+DateUtils.MILLIS_PER_DAY));
+            }
+        } catch (Exception e) {
+            keyStore = null;
+            log.error("Could not open keystore or certificate is not valid " + CERT_PATH_ABS + " " + e.getMessage());
+        }
+    }
+
+    /**
+     * Initialise the dynamic domain keystore
+     */
+    private void initDynamicKeyStore() throws IOException, GeneralSecurityException {
+        if (storePassword  != null) { // Assume we have already created the store
+            try {
+                keyStore = getKeyStore(storePassword.toCharArray());
+                X509Certificate  caCert = (X509Certificate) keyStore.getCertificate(KeyToolUtils.CA_ALIAS);
+                if (caCert == null) {
+                    keyStore = null; // no CA key - probably the wrong store type.
+                } else {
+                    caCert.checkValidity(new Date(System.currentTimeMillis()+DateUtils.MILLIS_PER_DAY));
+                }
+            } catch (IOException e) { // store is faulty, we need to recreate it
+                keyStore = null; // if cert is not valid, flag up to recreate it
+                if (e.getCause() instanceof UnrecoverableKeyException) {
+                    log.warn("Could not read key store " + e.getMessage() + " cause " + e.getCause().getMessage());
+                } else {
+                    log.warn("Could not open/read key store " + e.getMessage()); // message includes the file name
+                }
+            } catch (GeneralSecurityException e) {
+                log.warn("Problem reading key store" + e.getMessage());
+            }
+        }
+        if (keyStore == null) { // no existing file or not valid
+            storePassword = RandomStringUtils.randomAlphanumeric(20); // Alphanum to avoid issues with command-line quoting
+            keyPassword = storePassword; // we use same password for both
+            setPassword(storePassword);
+            log.info("Creating Proxy CA in " + CERT_PATH_ABS);
+            KeyToolUtils.generateProxyCA(CERT_PATH, storePassword, CERT_VALIDITY);
+            log.info("Created keystore in " + CERT_PATH_ABS);
+            keyStore = getKeyStore(storePassword.toCharArray()); // This should now work
+        }
+        final String sslDomains = getSslDomains().trim();
+        if (sslDomains.length() > 0) {
+            final String[] domains = sslDomains.split(",");
+            // The subject may be either a host or a domain
+            for(String subject : domains) {
+                if (isValid(subject)) {
+                    if (!keyStore.containsAlias(subject)) {
+                        log.info("Creating entry " + subject + " in " + CERT_PATH_ABS);
+                        KeyToolUtils.generateHostCert(CERT_PATH, storePassword, subject, CERT_VALIDITY);
+                        keyStore = getKeyStore(storePassword.toCharArray()); // reload to pick up new aliases
+                        // reloading is very quick compared with creating an entry currently
+                    }
+                } else {
+                    log.warn("Attempt to create an invalid domain certificate: " + subject);
+                }
+            }
+        }
+    }
+
+    private boolean isValid(String subject) {
+        String parts[] = subject.split("\\.");
+        if (!parts[0].endsWith("*")) { // not a wildcard
+            return true;
+        }
+        return parts.length >= 3 && AbstractVerifier.acceptableCountryWildcard(subject);
+    }
+
+    // This should only be called for a specific host
+    KeyStore updateKeyStore(String port, String host) throws IOException, GeneralSecurityException {
+        synchronized(CERT_PATH) { // ensure Proxy threads cannot interfere with each other
+            if (!keyStore.containsAlias(host)) {
+                log.info(port + "Creating entry " + host + " in " + CERT_PATH_ABS);
+                KeyToolUtils.generateHostCert(CERT_PATH, storePassword, host, CERT_VALIDITY);
+            }
+            keyStore = getKeyStore(storePassword.toCharArray()); // reload after adding alias
+        }
+        return keyStore;
+    }
+
+    /**
+     * Initialise the single key JMeter keystore (original behaviour)
+     */
+    private void initJMeterKeyStore() throws IOException, GeneralSecurityException {
+        if (storePassword  != null) { // Assume we have already created the store
+            try {
+                keyStore = getKeyStore(storePassword.toCharArray());
+                X509Certificate  caCert = (X509Certificate) keyStore.getCertificate(JMETER_SERVER_ALIAS);
+                caCert.checkValidity(new Date(System.currentTimeMillis()+DateUtils.MILLIS_PER_DAY));
+            } catch (Exception e) { // store is faulty, we need to recreate it
+                keyStore = null; // if cert is not valid, flag up to recreate it
+                log.warn("Could not open expected file or certificate is not valid " + CERT_PATH_ABS  + " " + e.getMessage());
+            }
+        }
+        if (keyStore == null) { // no existing file or not valid
+            storePassword = RandomStringUtils.randomAlphanumeric(20); // Alphanum to avoid issues with command-line quoting
+            keyPassword = storePassword; // we use same password for both
+            setPassword(storePassword);
+            log.info("Generating standard keypair in " + CERT_PATH_ABS);
+            CERT_PATH.delete(); // safer to start afresh
+            KeyToolUtils.genkeypair(CERT_PATH, JMETER_SERVER_ALIAS, storePassword, CERT_VALIDITY, null, null);
+            keyStore = getKeyStore(storePassword.toCharArray()); // This should now work
+        }
+    }
+
+    private KeyStore getKeyStore(char[] password) throws GeneralSecurityException, IOException {
+        InputStream in = null;
+        try {
+            in = new BufferedInputStream(new FileInputStream(CERT_PATH));
+            log.debug("Opened Keystore file: " + CERT_PATH_ABS);
+            KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE);
+            ks.load(in, password);
+            log.debug("Loaded Keystore file: " + CERT_PATH_ABS);
+            return ks;
+        } finally {
+            IOUtils.closeQuietly(in);
+        }
+    }
+
+    private String getPassword() {
+        return prefs.get(USER_PASSWORD_KEY, null);
+    }
+
+    private void setPassword(String password) {
+        prefs.put(USER_PASSWORD_KEY, password);
+    }
+
+    // the keystore for use by the Proxy
+    KeyStore getKeyStore() {
+        return keyStore;
+    }
+
+    String getKeyPassword() {
+        return keyPassword;
+    }
+
+    public static boolean isDynamicMode() {
+        return KEYSTORE_MODE == KeystoreMode.DYNAMIC_KEYSTORE;
+    }
+
 }

Modified: jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java?rev=1521320&r1=1521319&r2=1521320&view=diff
==============================================================================
--- jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java (original)
+++ jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java Tue Sep 10 00:02:39 2013
@@ -19,6 +19,7 @@
 package org.apache.jmeter.protocol.http.proxy.gui;
 
 import java.awt.BorderLayout;
+import java.awt.Cursor;
 import java.awt.Dimension;
 import java.awt.datatransfer.DataFlavor;
 import java.awt.datatransfer.UnsupportedFlavorException;
@@ -70,6 +71,7 @@ import org.apache.jmeter.testelement.Wor
 import org.apache.jmeter.testelement.property.PropertyIterator;
 import org.apache.jmeter.util.JMeterUtils;
 import org.apache.jorphan.gui.GuiUtils;
+import org.apache.jorphan.gui.JLabeledTextField;
 import org.apache.jorphan.logging.LoggingManager;
 import org.apache.log.Logger;
 
@@ -89,6 +91,8 @@ public class ProxyControlGui extends Log
     
     private JTextField portField;
 
+    private JLabeledTextField sslDomains;
+
     /**
      * Used to indicate that HTTP request headers should be captured. The
      * default is to capture the HTTP request headers, which are specific to
@@ -230,6 +234,7 @@ public class ProxyControlGui extends Log
         if (el instanceof ProxyControl) {
             model = (ProxyControl) el;
             model.setPort(portField.getText());
+            model.setSslDomains(sslDomains.getText());
             setIncludeListInProxyControl(model);
             setExcludeListInProxyControl(model);
             model.setCaptureHttpHeaders(httpHeaders.isSelected());
@@ -294,6 +299,7 @@ public class ProxyControlGui extends Log
         super.configure(element);
         model = (ProxyControl) element;
         portField.setText(model.getPortString());
+        sslDomains.setText(model.getSslDomains());
         httpHeaders.setSelected(model.getCaptureHttpHeaders());
         groupingMode.setSelectedIndex(model.getGroupingMode());
         addAssertions.setSelected(model.getAssertions());
@@ -453,6 +459,10 @@ public class ProxyControlGui extends Log
     private void startProxy() {
         ValueReplacer replacer = GuiPackage.getInstance().getReplacer();
         modifyTestElement(model);
+        // Proxy can take some while to start up; show a wating cursor
+        Cursor cursor = getCursor();
+        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+        // TODO somehow show progress
         try {
             replacer.replaceValues(model);
             model.startProxy();
@@ -474,6 +484,8 @@ public class ProxyControlGui extends Log
                     JMeterUtils.getResString("proxy_daemon_error"), // $NON-NLS-1$
                     "Error",
                     JOptionPane.ERROR_MESSAGE);
+        } finally {
+            setCursor(cursor);
         }
     }
 
@@ -584,9 +596,13 @@ public class ProxyControlGui extends Log
         HorizontalPanel panel = new HorizontalPanel();
         panel.add(label);
         panel.add(portField);
+        panel.add(Box.createHorizontalStrut(10));
 
         gPane.add(panel, BorderLayout.WEST);
-        gPane.add(Box.createHorizontalStrut(10));
+
+        sslDomains = new JLabeledTextField(JMeterUtils.getResString("proxy_domains")); // $NON-NLS-1$
+        sslDomains.setEnabled(ProxyControl.isDynamicMode());
+        gPane.add(sslDomains, BorderLayout.CENTER);
         return gPane;
     }