--- /bin/jmeter.properties (revision 800132) +++ /bin/jmeter.properties (working copy) @@ -419,6 +419,12 @@ # use command-line flags for user-name and password #http.proxyDomain=NTLM domain, if required by HTTPClient sampler +# SSL configuration +#proxy.cert.directory=./ +#proxy.cert.file=server.p12 +#proxy.cert.keystorepass=password +#proxy.cert.keypassword=password + #--------------------------------------------------------------------------- # HTTPSampleResponse Parser configuration #--------------------------------------------------------------------------- --- /build.xml (revision 800132) +++ /build.xml (working copy) @@ -995,6 +995,8 @@ + + --- /src/protocol/http/org/apache/jmeter/protocol/http/proxy/HttpRequestHdr.java (revision 800132) +++ /src/protocol/http/org/apache/jmeter/protocol/http/proxy/HttpRequestHdr.java (working copy) @@ -95,6 +95,8 @@ * Http Request method. Such as get or post. */ private String method = ""; // $NON-NLS-1$ + + private String paramHttps = ""; // $NON-NLS-1$ /** * The requested url. The universal resource locator that hopefully uniquely @@ -192,15 +194,9 @@ if (log.isDebugEnabled()) { log.debug("browser request: " + firstLine); } - if (!CharUtils.isAsciiAlphanumeric(firstLine.charAt(0))) { - throw new IllegalArgumentException("Unrecognised header line (probably used HTTPS)"); - } StringTokenizer tz = new StringTokenizer(firstLine); method = getToken(tz).toUpperCase(java.util.Locale.ENGLISH); url = getToken(tz); - if (url.toLowerCase(java.util.Locale.ENGLISH).startsWith(HTTPConstants.PROTOCOL_HTTPS)) { - throw new IllegalArgumentException("Cannot handle https URLS: " + url); - } version = getToken(tz); if (log.isDebugEnabled()) { log.debug("parser input: " + firstLine); @@ -208,9 +204,14 @@ log.debug("parsed url: " + url); log.debug("parsed version:" + version); } - if ("CONNECT".equalsIgnoreCase(method)){ - throw new IllegalArgumentException("Cannot handle CONNECT - probably used HTTPS"); + // SSL connection + if (getMethod().startsWith(HTTPConstants.CONNECT)) { + paramHttps = url; + } + if (url.startsWith("/")) { + url = HTTPS + "://" + paramHttps + url; // $NON-NLS-1$ } + log.debug("First Line: " + url); } /* @@ -414,7 +415,7 @@ if (log.isDebugEnabled()) { log.debug("Proxy: setting path: " + sampler.getPath()); } - if (numberRequests) { + if (!HTTPConstants.CONNECT.equals(getMethod()) && numberRequests) { requestNumber++; sampler.setName(requestNumber + " " + sampler.getPath()); } else { @@ -429,7 +430,7 @@ // If it was a HTTP GET request, then all parameters in the URL // has been handled by the sampler.setPath above, so we just need // to do parse the rest of the request if it is not a GET request - if(!HTTPConstants.GET.equals(method)) { + if((!HTTPConstants.CONNECT.equals(getMethod())) && (!HTTPConstants.GET.equals(method))) { // Check if it was a multipart http post request final String contentType = getContentType(); MultipartUrlConfig urlConfig = getMultipartConfig(contentType); @@ -567,6 +568,15 @@ public String getUrl(){ return url; } + + /** + * Returns the method string extracted from the first line of the client request. + * + * @return the url + */ + public String getMethod(){ + return method.toUpperCase(); + } /** * Returns the next token in a string. --- /src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java (revision 800132) +++ /src/protocol/http/org/apache/jmeter/protocol/http/proxy/Proxy.java (working copy) @@ -21,13 +21,24 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; 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.net.Socket; +import java.net.URL; import java.net.UnknownHostException; -import java.net.URL; +import java.security.KeyStore; +import java.util.HashMap; import java.util.Map; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + import org.apache.jmeter.protocol.http.control.HeaderManager; import org.apache.jmeter.protocol.http.parser.HTMLParseException; import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase; @@ -64,7 +75,29 @@ private static final String PROXY_HEADERS_REMOVE_DEFAULT = "If-Modified-Since,If-None-Match,Host"; // $NON-NLS-1$ private static final String PROXY_HEADERS_REMOVE_SEPARATOR = ","; // $NON-NLS-1$ + + // for ssl connection + private static final String KEYSTORE_INSTANCE = "PKCS12"; // $NON-NLS-1$ + + private static final String KEYMANAGERFACTORY_INSTANCE = "SunX509"; // $NON-NLS-1$ + + private static final String SSLCONTEXT_INSTANCE = "SSLv3"; // $NON-NLS-1$ + + // Haspmap to save ssl connection between Jmeter proxy and browser + private static HashMap hashHost = new HashMap(); + + // Proxy configuration SSL + private static String CERT_DIRECTORY = JMeterUtils.getPropDefault("proxy.cert.directory", "./"); // $NON-NLS-1$ $NON-NLS-2$ + + private static String CERT_FILE = JMeterUtils.getPropDefault("proxy.cert.file", "server.p12"); // $NON-NLS-1$ $NON-NLS-2$ + + private static char[] KEYSTORE_PASSWORD = JMeterUtils.getPropDefault("proxy.cert.keystorepass", "password").toCharArray(); // $NON-NLS-1$ $NON-NLS-2$ + + private static char[] KEY_PASSWORD = JMeterUtils.getPropDefault("proxy.cert.keypassword","password").toCharArray(); // $NON-NLS-1$ $NON-NLS-2$ + // Use with SSL connection + private OutputStream outStreamClient = null; + static { String removeList = JMeterUtils.getPropDefault(PROXY_HEADERS_REMOVE,PROXY_HEADERS_REMOVE_DEFAULT); headersToRemove = JOrphanUtils.split(removeList,PROXY_HEADERS_REMOVE_SEPARATOR); @@ -161,9 +194,28 @@ SampleResult result = null; HeaderManager headers = null; - try { + try { + // Now, parse only first line request.parse(new BufferedInputStream(clientSocket.getInputStream())); - + outStreamClient = clientSocket.getOutputStream(); + + if ((request.getMethod().startsWith(HTTPConstants.CONNECT)) && (outStreamClient != null)) { + log.debug("Method CONNECT => SSL"); + // write a OK reponse to browser, to engage SSL exchange + outStreamClient.write(("HTTP/1.0 200 OK\r\n\r\n").getBytes()); // $NON-NLS-1$ + outStreamClient.flush(); + // With ssl request, url is host:port (without https:// or path) + String[] param = request.getUrl().split(":"); // $NON-NLS-1$ + if (param.length == 2) { + log.debug("Start to negociate SSL connection, host: " + param[0]); + clientSocket = startSSL(clientSocket, param[0]); + } else { + log.warn("In SSL request, unable to find host and port in CONNECT request"); + } + // Re-parse (now it's the http request over SSL) + request.parse(new BufferedInputStream(clientSocket.getInputStream())); + } + // Populate the sampler. It is the same sampler as we sent into // the constructor of the HttpRequestHdr instance above request.getSampler(pageEncodings, formEncodings); @@ -225,6 +277,9 @@ "To record https requests, see " + "HTTP Proxy Server documentation")); result = generateErrorResult(result, e); // Generate result (if nec.) and populate it + } catch (IOException ioe) { + log.warn("IOE, certainly during response OK to CONNECT: anti-phishing method browser (request canceled by browser)." + + " Please accept fake JMeter SSL cert", ioe); } catch (Exception e) { log.error("Exception when processing sample", e); writeErrorToClient(HttpReplyHdr.formTimeout()); @@ -253,7 +308,92 @@ sampler.threadFinished(); // Needed for HTTPSampler2 } } + + /** + * SSL connection hashmap + * @param host + * @return a ssl socket factory + */ + private SSLSocketFactory getSSLSocketFactory(String host) { + synchronized (hashHost) { + if (hashHost.containsKey(host)) { + log.debug("Good, already in map, host=" + host); + return (SSLSocketFactory) hashHost.get(host); + } + InputStream in = getCertificat(); + if (in != null) { + KeyStore ks = null; + KeyManagerFactory kmf = null; + SSLContext sslcontext = null; + try { + ks = KeyStore.getInstance(KEYSTORE_INSTANCE); + ks.load(in, KEYSTORE_PASSWORD); + kmf = KeyManagerFactory + .getInstance(KEYMANAGERFACTORY_INSTANCE); + kmf.init(ks, KEY_PASSWORD); + sslcontext = SSLContext.getInstance(SSLCONTEXT_INSTANCE); + sslcontext.init(kmf.getKeyManagers(), null, null); + SSLSocketFactory sslFactory = sslcontext.getSocketFactory(); + hashHost.put(host, sslFactory); + log.info("KeyStore for SSL load OK and put host in map ("+host+")"); + return sslFactory; + } catch (Exception e) { + log.error("Exception with keystore: " + e); + } + } else { + throw new NullPointerException("Unable to read keystore"); + } + return null; + } + } + /** + * Negociate a SSL connection + * @param sock socket in + * @param host + * @return a new client socket over ssl + * @throws Exception if negociation failed + */ + private Socket startSSL(Socket sock, String host) throws Exception { + SSLSocketFactory sslFactory = getSSLSocketFactory(host); + SSLSocket secureSocket; + if (sslFactory != null) { + try { + secureSocket = (SSLSocket) sslFactory.createSocket(sock, sock + .getInetAddress().getHostName(), sock.getPort(), true); + secureSocket.setUseClientMode(false); + log.debug("SSL transaction ok with cipher: " + secureSocket.getSession().getCipherSuite()); + return secureSocket; + } catch (Exception e) { + log.error("Error in SSL socket negociation: ", e); + throw e; + } + } else { + log.warn("Unable to negociate SSL transaction, no keystore"); + throw new Exception("Unable to negociate SSL transaction, no keystore"); + } + } + + /** + * Load (fake) cert file + * @return stream to key cert + */ + private InputStream getCertificat() { + File certFile = new File(CERT_DIRECTORY + CERT_FILE); + InputStream in = null; + if (certFile.exists() && certFile.canRead()) { + try { + log.info("Keystore file: "+CERT_DIRECTORY+CERT_FILE); + in = new FileInputStream(certFile); + } catch (FileNotFoundException e) { + log.error("No server cert file found", e); + } + } else { + throw new NullPointerException("No keystore found"); + } + return in; + } + private SampleResult generateErrorResult(SampleResult result, Exception e) { if (result == null) { result = new SampleResult(); --- /src/protocol/http/org/apache/jmeter/protocol/http/util/HTTPConstantsInterface.java (revision 800132) +++ /src/protocol/http/org/apache/jmeter/protocol/http/util/HTTPConstantsInterface.java (working copy) @@ -38,6 +38,7 @@ public static final String OPTIONS = "OPTIONS"; // $NON-NLS-1$ public static final String TRACE = "TRACE"; // $NON-NLS-1$ public static final String DELETE = "DELETE"; // $NON-NLS-1$ + public static final String CONNECT = "CONNECT"; // $NON-NLS-1$ public static final String HEADER_AUTHORIZATION = "Authorization"; // $NON-NLS-1$ public static final String HEADER_COOKIE = "Cookie"; // $NON-NLS-1$ public static final String HEADER_CONNECTION = "Connection"; // $NON-NLS-1$