blob: 7ce23360306f977d0f5bdb33c458d105e9c44bb1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.jmeter.protocol.http.sampler;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import javax.security.auth.Subject;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpConnection;
import org.apache.http.HttpConnectionMetrics;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.NTCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpTrace;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.protocol.ResponseContentEncoding;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.DnsResolver;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.FormBodyPart;
import org.apache.http.entity.mime.FormBodyPartBuilder;
import org.apache.http.entity.mime.MIME;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultClientConnectionReuseStrategy;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.conn.SystemDefaultDnsResolver;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.message.BufferedHeader;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.DefaultedHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.SyncBasicHttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpCoreContext;
import org.apache.http.util.CharArrayBuffer;
import org.apache.jmeter.protocol.http.control.AuthManager;
import org.apache.jmeter.protocol.http.control.CacheManager;
import org.apache.jmeter.protocol.http.control.CookieManager;
import org.apache.jmeter.protocol.http.control.HeaderManager;
import org.apache.jmeter.protocol.http.util.EncoderCache;
import org.apache.jmeter.protocol.http.util.HTTPArgument;
import org.apache.jmeter.protocol.http.util.HTTPConstants;
import org.apache.jmeter.protocol.http.util.HTTPFileArg;
import org.apache.jmeter.protocol.http.util.SlowHC4SocketFactory;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.services.FileServer;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterVariables;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.util.JsseSSLManager;
import org.apache.jmeter.util.SSLManager;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;
/**
* HTTP Sampler using Apache HttpClient 4.x.
*
*/
public class HTTPHC4Impl extends HTTPHCAbstractImpl {
private static final Logger log = LoggingManager.getLoggerForClass();
/** retry count to be used (default 0); 0 = disable retries */
private static final int RETRY_COUNT = JMeterUtils.getPropDefault("httpclient4.retrycount", 0);
/** Idle timeout to be applied to connections if no Keep-Alive header is sent by the server (default 0 = disable) */
private static final int IDLE_TIMEOUT = JMeterUtils.getPropDefault("httpclient4.idletimeout", 0);
private static final int VALIDITY_AFTER_INACTIVITY_TIMEOUT = JMeterUtils.getPropDefault("httpclient4.validate_after_inactivity", 2000);
private static final int TIME_TO_LIVE = JMeterUtils.getPropDefault("httpclient4.time_to_live", 2000);
private static final String CONTEXT_METRICS = "jmeter_metrics"; // TODO hack for metrics related to HTTPCLIENT-1081, to be removed later
private static final Pattern PORT_PATTERN = Pattern.compile("\\d+"); // only used in .matches(), no need for anchors
private static final ConnectionKeepAliveStrategy IDLE_STRATEGY = new DefaultConnectionKeepAliveStrategy(){
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
long duration = super.getKeepAliveDuration(response, context);
if (duration <= 0 && IDLE_TIMEOUT > 0) {// none found by the superclass
if(log.isDebugEnabled()) {
log.debug("Setting keepalive to " + IDLE_TIMEOUT);
}
return IDLE_TIMEOUT;
}
return duration; // return the super-class value
}
};
/**
* Special interceptor made to keep metrics when connection is released for some method like HEAD
* Otherwise calling directly ((HttpConnection) localContext.getAttribute(HttpCoreContext.HTTP_CONNECTION)).getMetrics();
* would throw org.apache.http.impl.conn.ConnectionShutdownException
* See <a href="https://bz.apache.org/jira/browse/HTTPCLIENT-1081">HTTPCLIENT-1081</a>
*/
private static final HttpResponseInterceptor METRICS_SAVER = new HttpResponseInterceptor(){
@Override
public void process(HttpResponse response, HttpContext context)
throws HttpException, IOException {
HttpConnectionMetrics metrics = ((HttpConnection) context.getAttribute(HttpCoreContext.HTTP_CONNECTION)).getMetrics();
context.setAttribute(CONTEXT_METRICS, metrics);
}
};
private static final HttpRequestInterceptor METRICS_RESETTER = new HttpRequestInterceptor() {
@Override
public void process(HttpRequest request, HttpContext context)
throws HttpException, IOException {
HttpConnectionMetrics metrics = ((HttpConnection) context.getAttribute(HttpCoreContext.HTTP_CONNECTION)).getMetrics();
metrics.reset();
}
};
/**
* Headers to save
*/
private static final String[] HEADERS_TO_SAVE = new String[]{
"content-length",
"content-encoding",
"content-md5"
};
/**
* Custom implementation that backups headers related to Compressed responses
* that HC core {@link ResponseContentEncoding} removes after uncompressing
* See Bug 59401
*/
private static final HttpResponseInterceptor RESPONSE_CONTENT_ENCODING = new ResponseContentEncoding() {
@Override
public void process(HttpResponse response, HttpContext context)
throws HttpException, IOException {
ArrayList<Header[]> headersToSave = null;
final HttpEntity entity = response.getEntity();
final HttpClientContext clientContext = HttpClientContext.adapt(context);
final RequestConfig requestConfig = clientContext.getRequestConfig();
// store the headers if necessary
if (requestConfig.isContentCompressionEnabled() && entity != null && entity.getContentLength() != 0) {
final Header ceheader = entity.getContentEncoding();
if (ceheader != null) {
headersToSave = new ArrayList<>(3);
for(String name : HEADERS_TO_SAVE) {
Header[] hdr = response.getHeaders(name); // empty if none
headersToSave.add(hdr);
}
}
}
// Now invoke original parent code
super.process(response, clientContext);
// Should this be in a finally ?
if(headersToSave != null) {
for (Header[] headers : headersToSave) {
for (Header headerToRestore : headers) {
if (response.containsHeader(headerToRestore.getName())) {
break;
}
response.addHeader(headerToRestore);
}
}
}
}
};
/**
* 1 HttpClient instance per combination of (HttpClient,HttpClientKey)
*/
private static final ThreadLocal<Map<HttpClientKey, HttpClient>> HTTPCLIENTS_CACHE_PER_THREAD_AND_HTTPCLIENTKEY =
new InheritableThreadLocal<Map<HttpClientKey, HttpClient>>(){
@Override
protected Map<HttpClientKey, HttpClient> initialValue() {
return new HashMap<>();
}
};
// Scheme used for slow HTTP sockets. Cannot be set as a default, because must be set on an HttpClient instance.
private static final Scheme SLOW_HTTP;
/*
* Create a set of default parameters from the ones initially created.
* This allows the defaults to be overridden if necessary from the properties file.
*/
private static final HttpParams DEFAULT_HTTP_PARAMS;
private static final String USER_TOKEN = "__jmeter.USER_TOKEN__"; //$NON-NLS-1$
static final String SAMPLER_RESULT_TOKEN = "__jmeter.SAMPLER_RESULT__"; //$NON-NLS-1$
private static final String HTTPCLIENT_TOKEN = "__jmeter.HTTPCLIENT_TOKEN__";
static {
log.info("HTTP request retry count = "+RETRY_COUNT);
DEFAULT_HTTP_PARAMS = new SyncBasicHttpParams(); // Could we drop the Sync here?
DEFAULT_HTTP_PARAMS.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false);
DEFAULT_HTTP_PARAMS.setIntParameter(ClientPNames.MAX_REDIRECTS, HTTPSamplerBase.MAX_REDIRECTS);
DefaultHttpClient.setDefaultHttpParams(DEFAULT_HTTP_PARAMS);
// Process Apache HttpClient parameters file
String file=JMeterUtils.getProperty("hc.parameters.file"); // $NON-NLS-1$
if (file != null) {
HttpClientDefaultParameters.load(file, DEFAULT_HTTP_PARAMS);
}
// Set up HTTP scheme override if necessary
if (CPS_HTTP > 0) {
log.info("Setting up HTTP SlowProtocol, cps="+CPS_HTTP);
SLOW_HTTP = new Scheme(HTTPConstants.PROTOCOL_HTTP, HTTPConstants.DEFAULT_HTTP_PORT, new SlowHC4SocketFactory(CPS_HTTP));
} else {
SLOW_HTTP = null;
}
if (localAddress != null){
DEFAULT_HTTP_PARAMS.setParameter(ConnRoutePNames.LOCAL_ADDRESS, localAddress);
}
}
private volatile HttpUriRequest currentRequest; // Accessed from multiple threads
private volatile boolean resetSSLContext;
protected HTTPHC4Impl(HTTPSamplerBase testElement) {
super(testElement);
}
public static final class HttpDelete extends HttpEntityEnclosingRequestBase {
public HttpDelete(final URI uri) {
super();
setURI(uri);
}
@Override
public String getMethod() {
return HTTPConstants.DELETE;
}
}
@Override
protected HTTPSampleResult sample(URL url, String method,
boolean areFollowingRedirect, int frameDepth) {
if (log.isDebugEnabled()) {
log.debug("Start : sample " + url.toString());
log.debug("method " + method+ " followingRedirect " + areFollowingRedirect + " depth " + frameDepth);
}
HTTPSampleResult res = createSampleResult(url, method);
HttpClient httpClient = setupClient(url, res);
HttpRequestBase httpRequest = null;
try {
URI uri = url.toURI();
if (method.equals(HTTPConstants.POST)) {
httpRequest = new HttpPost(uri);
} else if (method.equals(HTTPConstants.GET)) {
httpRequest = new HttpGet(uri);
} else if (method.equals(HTTPConstants.PUT)) {
httpRequest = new HttpPut(uri);
} else if (method.equals(HTTPConstants.HEAD)) {
httpRequest = new HttpHead(uri);
} else if (method.equals(HTTPConstants.TRACE)) {
httpRequest = new HttpTrace(uri);
} else if (method.equals(HTTPConstants.OPTIONS)) {
httpRequest = new HttpOptions(uri);
} else if (method.equals(HTTPConstants.DELETE)) {
httpRequest = new HttpDelete(uri);
} else if (method.equals(HTTPConstants.PATCH)) {
httpRequest = new HttpPatch(uri);
} else if (HttpWebdav.isWebdavMethod(method)) {
httpRequest = new HttpWebdav(method, uri);
} else {
throw new IllegalArgumentException("Unexpected method: '"+method+"'");
}
setupRequest(url, httpRequest, res); // can throw IOException
} catch (Exception e) {
res.sampleStart();
res.sampleEnd();
errorResult(e, res);
return res;
}
HttpContext localContext = new BasicHttpContext();
setupClientContextBeforeSample(localContext);
res.sampleStart();
final CacheManager cacheManager = getCacheManager();
if (cacheManager != null && HTTPConstants.GET.equalsIgnoreCase(method)) {
if (cacheManager.inCache(url)) {
return updateSampleResultForResourceInCache(res);
}
}
try {
currentRequest = httpRequest;
handleMethod(method, res, httpRequest, localContext);
// store the SampleResult in LocalContext to compute connect time
localContext.setAttribute(SAMPLER_RESULT_TOKEN, res);
// perform the sample
HttpResponse httpResponse =
executeRequest(httpClient, httpRequest, localContext, url);
// Needs to be done after execute to pick up all the headers
final HttpRequest request = (HttpRequest) localContext.getAttribute(HttpCoreContext.HTTP_REQUEST);
extractClientContextAfterSample(localContext);
// We've finished with the request, so we can add the LocalAddress to it for display
final InetAddress localAddr = (InetAddress) httpRequest.getParams().getParameter(ConnRoutePNames.LOCAL_ADDRESS);
if (localAddr != null) {
request.addHeader(HEADER_LOCAL_ADDRESS, localAddr.toString());
}
res.setRequestHeaders(getConnectionHeaders(request));
Header contentType = httpResponse.getLastHeader(HTTPConstants.HEADER_CONTENT_TYPE);
if (contentType != null){
String ct = contentType.getValue();
res.setContentType(ct);
res.setEncodingAndType(ct);
}
HttpEntity entity = httpResponse.getEntity();
if (entity != null) {
res.setResponseData(readResponse(res, entity.getContent(), (int) entity.getContentLength()));
}
res.sampleEnd(); // Done with the sampling proper.
currentRequest = null;
// Now collect the results into the HTTPSampleResult:
StatusLine statusLine = httpResponse.getStatusLine();
int statusCode = statusLine.getStatusCode();
res.setResponseCode(Integer.toString(statusCode));
res.setResponseMessage(statusLine.getReasonPhrase());
res.setSuccessful(isSuccessCode(statusCode));
res.setResponseHeaders(getResponseHeaders(httpResponse, localContext));
if (res.isRedirect()) {
final Header headerLocation = httpResponse.getLastHeader(HTTPConstants.HEADER_LOCATION);
if (headerLocation == null) { // HTTP protocol violation, but avoids NPE
throw new IllegalArgumentException("Missing location header in redirect for " + httpRequest.getRequestLine());
}
String redirectLocation = headerLocation.getValue();
res.setRedirectLocation(redirectLocation);
}
// record some sizes to allow HTTPSampleResult.getBytes() with different options
HttpConnectionMetrics metrics = (HttpConnectionMetrics) localContext.getAttribute(CONTEXT_METRICS);
long headerBytes =
res.getResponseHeaders().length() // condensed length (without \r)
+ httpResponse.getAllHeaders().length // Add \r for each header
+ 1 // Add \r for initial header
+ 2; // final \r\n before data
long totalBytes = metrics.getReceivedBytesCount();
res.setHeadersSize((int) headerBytes);
res.setBodySize((int)(totalBytes - headerBytes));
if (log.isDebugEnabled()) {
log.debug("ResponseHeadersSize=" + res.getHeadersSize() + " Content-Length=" + res.getBodySize()
+ " Total=" + (res.getHeadersSize() + res.getBodySize()));
}
// If we redirected automatically, the URL may have changed
if (getAutoRedirects()){
HttpUriRequest req = (HttpUriRequest) localContext.getAttribute(HttpCoreContext.HTTP_REQUEST);
HttpHost target = (HttpHost) localContext.getAttribute(HttpCoreContext.HTTP_TARGET_HOST);
URI redirectURI = req.getURI();
if (redirectURI.isAbsolute()){
res.setURL(redirectURI.toURL());
} else {
res.setURL(new URL(new URL(target.toURI()),redirectURI.toString()));
}
}
// Store any cookies received in the cookie manager:
saveConnectionCookies(httpResponse, res.getURL(), getCookieManager());
// Save cache information
if (cacheManager != null){
cacheManager.saveDetails(httpResponse, res);
}
// Follow redirects and download page resources if appropriate:
res = resultProcessing(areFollowingRedirect, frameDepth, res);
} catch (IOException e) {
log.debug("IOException", e);
if (res.getEndTime() == 0) {
res.sampleEnd();
}
// pick up headers if failed to execute the request
if (res.getRequestHeaders() != null) {
log.debug("Overwriting request old headers: " + res.getRequestHeaders());
}
res.setRequestHeaders(getConnectionHeaders((HttpRequest) localContext.getAttribute(HttpCoreContext.HTTP_REQUEST)));
errorResult(e, res);
return res;
} catch (RuntimeException e) {
log.debug("RuntimeException", e);
if (res.getEndTime() == 0) {
res.sampleEnd();
}
errorResult(e, res);
return res;
} finally {
currentRequest = null;
JMeterContextService.getContext().getSamplerContext().remove(HTTPCLIENT_TOKEN);
}
return res;
}
/**
* Store in JMeter Variables the UserToken so that the SSL context is reused
* See <a href="https://bz.apache.org/bugzilla/show_bug.cgi?id=57804">Bug 57804</a>
* @param localContext {@link HttpContext}
*/
private void extractClientContextAfterSample(HttpContext localContext) {
Object userToken = localContext.getAttribute(HttpClientContext.USER_TOKEN);
if(userToken != null) {
if(log.isDebugEnabled()) {
log.debug("Extracted from HttpContext user token:"+userToken+", storing it as JMeter variable:"+USER_TOKEN);
}
// During recording JMeterContextService.getContext().getVariables() is null
JMeterVariables jMeterVariables = JMeterContextService.getContext().getVariables();
if (jMeterVariables != null) {
jMeterVariables.putObject(USER_TOKEN, userToken);
}
}
}
/**
* Configure the UserToken so that the SSL context is reused
* See <a href="https://bz.apache.org/bugzilla/show_bug.cgi?id=57804">Bug 57804</a>
* @param localContext {@link HttpContext}
*/
private void setupClientContextBeforeSample(HttpContext localContext) {
Object userToken = null;
// During recording JMeterContextService.getContext().getVariables() is null
JMeterVariables jMeterVariables = JMeterContextService.getContext().getVariables();
if(jMeterVariables != null) {
userToken = jMeterVariables.getObject(USER_TOKEN);
}
if(userToken != null) {
if(log.isDebugEnabled()) {
log.debug("Found user token:"+userToken+" as JMeter variable:"+USER_TOKEN+", storing it in HttpContext");
}
localContext.setAttribute(HttpClientContext.USER_TOKEN, userToken);
} else {
// It would be better to create a ClientSessionManager that would compute this value
// for now it can be Thread.currentThread().getName() but must be changed when we would change
// the Thread per User model
String userId = Thread.currentThread().getName();
if(log.isDebugEnabled()) {
log.debug("Storing in HttpContext the user token:"+userId);
}
localContext.setAttribute(HttpClientContext.USER_TOKEN, userId);
}
}
/**
* Calls {@link #sendPostData(HttpPost)} if method is <code>POST</code> and
* {@link #sendEntityData(HttpEntityEnclosingRequestBase)} if method is
* <code>PUT</code> or <code>PATCH</code>
* <p>
* Field HTTPSampleResult#queryString of result is modified in the 2 cases
*
* @param method
* String HTTP method
* @param result
* {@link HTTPSampleResult}
* @param httpRequest
* {@link HttpRequestBase}
* @param localContext
* {@link HttpContext}
* @throws IOException
* when posting data fails due to I/O
*/
protected void handleMethod(String method, HTTPSampleResult result,
HttpRequestBase httpRequest, HttpContext localContext) throws IOException {
// Handle the various methods
if (httpRequest instanceof HttpPost) {
String postBody = sendPostData((HttpPost)httpRequest);
result.setQueryString(postBody);
} else if (httpRequest instanceof HttpEntityEnclosingRequestBase) {
String entityBody = sendEntityData((HttpEntityEnclosingRequestBase) httpRequest);
result.setQueryString(entityBody);
}
}
/**
* Create HTTPSampleResult filling url, method and SampleLabel.
* Monitor field is computed calling isMonitor()
* @param url URL
* @param method HTTP Method
* @return {@link HTTPSampleResult}
*/
protected HTTPSampleResult createSampleResult(URL url, String method) {
HTTPSampleResult res = new HTTPSampleResult();
res.setMonitor(isMonitor());
res.setSampleLabel(url.toString()); // May be replaced later
res.setHTTPMethod(method);
res.setURL(url);
return res;
}
/**
* Execute request either as is or under PrivilegedAction
* if a Subject is available for url
* @param httpClient the {@link HttpClient} to be used to execute the httpRequest
* @param httpRequest the {@link HttpRequest} to be executed
* @param localContext th {@link HttpContext} to be used for execution
* @param url the target url (will be used to look up a possible subject for the execution)
* @return the result of the execution of the httpRequest
* @throws IOException
* @throws ClientProtocolException
*/
private HttpResponse executeRequest(final HttpClient httpClient,
final HttpRequestBase httpRequest, final HttpContext localContext, final URL url)
throws IOException, ClientProtocolException {
AuthManager authManager = getAuthManager();
if (authManager != null) {
Subject subject = authManager.getSubjectForUrl(url);
if(subject != null) {
try {
return Subject.doAs(subject,
new PrivilegedExceptionAction<HttpResponse>() {
@Override
public HttpResponse run() throws Exception {
return httpClient.execute(httpRequest,
localContext);
}
});
} catch (PrivilegedActionException e) {
log.error(
"Can't execute httpRequest with subject:"+subject,
e);
throw new RuntimeException("Can't execute httpRequest with subject:"+subject, e);
}
}
}
return httpClient.execute(httpRequest, localContext);
}
/**
* Holder class for all fields that define an HttpClient instance;
* used as the key to the ThreadLocal map of HttpClient instances.
*/
private static final class HttpClientKey {
private final String target; // protocol://[user:pass@]host:[port]
private final boolean hasProxy;
private final String proxyHost;
private final int proxyPort;
private final String proxyUser;
private final String proxyPass;
private final int hashCode; // Always create hash because we will always need it
/**
* @param url URL Only protocol and url authority are used (protocol://[user:pass@]host:[port])
* @param hasProxy has proxy
* @param proxyHost proxy host
* @param proxyPort proxy port
* @param proxyUser proxy user
* @param proxyPass proxy password
*/
public HttpClientKey(URL url, boolean hasProxy, String proxyHost,
int proxyPort, String proxyUser, String proxyPass) {
// N.B. need to separate protocol from authority otherwise http://server would match https://erver (<= sic, not typo error)
// could use separate fields, but simpler to combine them
this.target = url.getProtocol()+"://"+url.getAuthority();
this.hasProxy = hasProxy;
this.proxyHost = proxyHost;
this.proxyPort = proxyPort;
this.proxyUser = proxyUser;
this.proxyPass = proxyPass;
this.hashCode = getHash();
}
private int getHash() {
int hash = 17;
hash = hash*31 + (hasProxy ? 1 : 0);
if (hasProxy) {
hash = hash*31 + getHash(proxyHost);
hash = hash*31 + proxyPort;
hash = hash*31 + getHash(proxyUser);
hash = hash*31 + getHash(proxyPass);
}
hash = hash*31 + target.hashCode();
return hash;
}
// Allow for null strings
private int getHash(String s) {
return s == null ? 0 : s.hashCode();
}
@Override
public boolean equals (Object obj){
if (this == obj) {
return true;
}
if (!(obj instanceof HttpClientKey)) {
return false;
}
HttpClientKey other = (HttpClientKey) obj;
if (this.hasProxy) { // otherwise proxy String fields may be null
return
this.hasProxy == other.hasProxy &&
this.proxyPort == other.proxyPort &&
this.proxyHost.equals(other.proxyHost) &&
this.proxyUser.equals(other.proxyUser) &&
this.proxyPass.equals(other.proxyPass) &&
this.target.equals(other.target);
}
// No proxy, so don't check proxy fields
return
this.hasProxy == other.hasProxy &&
this.target.equals(other.target);
}
@Override
public int hashCode(){
return hashCode;
}
// For debugging
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(target);
if (hasProxy) {
sb.append(" via ");
sb.append(proxyUser);
sb.append("@");
sb.append(proxyHost);
sb.append(":");
sb.append(proxyPort);
}
return sb.toString();
}
}
private HttpClient setupClient(URL url, SampleResult res) {
Map<HttpClientKey, HttpClient> mapHttpClientPerHttpClientKey = HTTPCLIENTS_CACHE_PER_THREAD_AND_HTTPCLIENTKEY.get();
final String host = url.getHost();
String proxyHost = getProxyHost();
int proxyPort = getProxyPortInt();
String proxyPass = getProxyPass();
String proxyUser = getProxyUser();
// static proxy is the globally define proxy eg command line or properties
boolean useStaticProxy = isStaticProxy(host);
// dynamic proxy is the proxy defined for this sampler
boolean useDynamicProxy = isDynamicProxy(proxyHost, proxyPort);
boolean useProxy = useStaticProxy || useDynamicProxy;
// if both dynamic and static are used, the dynamic proxy has priority over static
if(!useDynamicProxy) {
proxyHost = PROXY_HOST;
proxyPort = PROXY_PORT;
proxyUser = PROXY_USER;
proxyPass = PROXY_PASS;
}
// Lookup key - must agree with all the values used to create the HttpClient.
HttpClientKey key = new HttpClientKey(url, useProxy, proxyHost, proxyPort, proxyUser, proxyPass);
HttpClient httpClient = null;
boolean concurrentDwn = this.testElement.isConcurrentDwn();
if(concurrentDwn) {
httpClient = (HttpClient) JMeterContextService.getContext().getSamplerContext().get(HTTPCLIENT_TOKEN);
}
if (httpClient == null) {
httpClient = mapHttpClientPerHttpClientKey.get(key);
}
if (httpClient != null && resetSSLContext && HTTPConstants.PROTOCOL_HTTPS.equalsIgnoreCase(url.getProtocol())) {
((AbstractHttpClient) httpClient).clearRequestInterceptors();
((AbstractHttpClient) httpClient).clearResponseInterceptors();
httpClient.getConnectionManager().closeIdleConnections(1L, TimeUnit.MICROSECONDS);
httpClient = null;
JsseSSLManager sslMgr = (JsseSSLManager) SSLManager.getInstance();
sslMgr.resetContext();
resetSSLContext = false;
}
if (httpClient == null) { // One-time init for this client
HttpParams clientParams = new DefaultedHttpParams(new BasicHttpParams(), DEFAULT_HTTP_PARAMS);
DnsResolver resolver = this.testElement.getDNSResolver();
if (resolver == null) {
resolver = SystemDefaultDnsResolver.INSTANCE;
}
MeasuringConnectionManager connManager = new MeasuringConnectionManager(
createSchemeRegistry(),
resolver,
TIME_TO_LIVE,
VALIDITY_AFTER_INACTIVITY_TIMEOUT);
// Modern browsers use more connections per host than the current httpclient default (2)
// when using parallel download the httpclient and connection manager are shared by the downloads threads
// to be realistic JMeter must set an higher value to DefaultMaxPerRoute
if(concurrentDwn) {
try {
int maxConcurrentDownloads = Integer.parseInt(this.testElement.getConcurrentPool());
connManager.setDefaultMaxPerRoute(Math.max(maxConcurrentDownloads, connManager.getDefaultMaxPerRoute()));
} catch (NumberFormatException nfe) {
// no need to log -> will be done by the sampler
}
}
httpClient = new DefaultHttpClient(connManager, clientParams) {
@Override
protected HttpRequestRetryHandler createHttpRequestRetryHandler() {
return new DefaultHttpRequestRetryHandler(RETRY_COUNT, false); // set retry count
}
};
if (IDLE_TIMEOUT > 0) {
((AbstractHttpClient) httpClient).setKeepAliveStrategy(IDLE_STRATEGY );
}
// see https://issues.apache.org/jira/browse/HTTPCORE-397
((AbstractHttpClient) httpClient).setReuseStrategy(DefaultClientConnectionReuseStrategy.INSTANCE);
((AbstractHttpClient) httpClient).addResponseInterceptor(RESPONSE_CONTENT_ENCODING);
((AbstractHttpClient) httpClient).addResponseInterceptor(METRICS_SAVER); // HACK
((AbstractHttpClient) httpClient).addRequestInterceptor(METRICS_RESETTER);
// Override the default schemes as necessary
SchemeRegistry schemeRegistry = httpClient.getConnectionManager().getSchemeRegistry();
if (SLOW_HTTP != null){
schemeRegistry.register(SLOW_HTTP);
}
// Set up proxy details
if(useProxy) {
HttpHost proxy = new HttpHost(proxyHost, proxyPort);
clientParams.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
if (proxyUser.length() > 0) {
((AbstractHttpClient) httpClient).getCredentialsProvider().setCredentials(
new AuthScope(proxyHost, proxyPort),
new NTCredentials(proxyUser, proxyPass, localHost, PROXY_DOMAIN));
}
}
// Bug 52126 - we do our own cookie handling
clientParams.setParameter(ClientPNames.COOKIE_POLICY, CookieSpecs.IGNORE_COOKIES);
if (log.isDebugEnabled()) {
log.debug("Created new HttpClient: @"+System.identityHashCode(httpClient) + " " + key.toString());
}
mapHttpClientPerHttpClientKey.put(key, httpClient); // save the agent for next time round
} else {
if (log.isDebugEnabled()) {
log.debug("Reusing the HttpClient: @"+System.identityHashCode(httpClient) + " " + key.toString());
}
}
if(concurrentDwn) {
JMeterContextService.getContext().getSamplerContext().put(HTTPCLIENT_TOKEN, httpClient);
}
// TODO - should this be done when the client is created?
// If so, then the details need to be added as part of HttpClientKey
setConnectionAuthorization(httpClient, url, getAuthManager(), key);
return httpClient;
}
/**
* Setup LazySchemeSocketFactory
* @see "https://bz.apache.org/bugzilla/show_bug.cgi?id=58099"
*/
private static SchemeRegistry createSchemeRegistry() {
final SchemeRegistry registry = new SchemeRegistry();
registry.register(
new Scheme("http", 80, PlainSocketFactory.getSocketFactory())); //$NON-NLS-1$
registry.register(
new Scheme("https", 443, new LazySchemeSocketFactory())); //$NON-NLS-1$
return registry;
}
/**
* Setup following elements on httpRequest:
* <ul>
* <li>ConnRoutePNames.LOCAL_ADDRESS enabling IP-SPOOFING</li>
* <li>Socket and connection timeout</li>
* <li>Redirect handling</li>
* <li>Keep Alive header or Connection Close</li>
* <li>Calls setConnectionHeaders to setup headers</li>
* <li>Calls setConnectionCookie to setup Cookie</li>
* </ul>
*
* @param url
* {@link URL} of the request
* @param httpRequest
* http request for the request
* @param res
* sample result to set cookies on
* @throws IOException
* if hostname/ip to use could not be figured out
*/
protected void setupRequest(URL url, HttpRequestBase httpRequest, HTTPSampleResult res)
throws IOException {
HttpParams requestParams = httpRequest.getParams();
// Set up the local address if one exists
final InetAddress inetAddr = getIpSourceAddress();
if (inetAddr != null) {// Use special field ip source address (for pseudo 'ip spoofing')
requestParams.setParameter(ConnRoutePNames.LOCAL_ADDRESS, inetAddr);
} else if (localAddress != null){
requestParams.setParameter(ConnRoutePNames.LOCAL_ADDRESS, localAddress);
} else { // reset in case was set previously
requestParams.removeParameter(ConnRoutePNames.LOCAL_ADDRESS);
}
int rto = getResponseTimeout();
if (rto > 0){
requestParams.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, rto);
}
int cto = getConnectTimeout();
if (cto > 0){
requestParams.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, cto);
}
requestParams.setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, getAutoRedirects());
// a well-behaved browser is supposed to send 'Connection: close'
// with the last request to an HTTP server. Instead, most browsers
// leave it to the server to close the connection after their
// timeout period. Leave it to the JMeter user to decide.
if (getUseKeepAlive()) {
httpRequest.setHeader(HTTPConstants.HEADER_CONNECTION, HTTPConstants.KEEP_ALIVE);
} else {
httpRequest.setHeader(HTTPConstants.HEADER_CONNECTION, HTTPConstants.CONNECTION_CLOSE);
}
setConnectionHeaders(httpRequest, url, getHeaderManager(), getCacheManager());
String cookies = setConnectionCookie(httpRequest, url, getCookieManager());
if (res != null) {
res.setCookies(cookies);
}
}
/**
* Set any default request headers to include
*
* @param request the HttpRequest to be used
*/
protected void setDefaultRequestHeaders(HttpRequest request) {
// Method left empty here, but allows subclasses to override
}
/**
* Gets the ResponseHeaders
*
* @param response
* containing the headers
* @param localContext {@link HttpContext}
* @return string containing the headers, one per line
*/
private String getResponseHeaders(HttpResponse response, HttpContext localContext) {
Header[] rh = response.getAllHeaders();
StringBuilder headerBuf = new StringBuilder(40 * (rh.length+1));
headerBuf.append(response.getStatusLine());// header[0] is not the status line...
headerBuf.append("\n"); // $NON-NLS-1$
for (Header responseHeader : rh) {
writeResponseHeader(headerBuf, responseHeader);
}
return headerBuf.toString();
}
/**
* Write responseHeader to headerBuffer in an optimized way
* @param headerBuffer {@link StringBuilder}
* @param responseHeader {@link Header}
*/
private void writeResponseHeader(StringBuilder headerBuffer, Header responseHeader) {
if(responseHeader instanceof BufferedHeader) {
CharArrayBuffer buffer = ((BufferedHeader)responseHeader).getBuffer();
headerBuffer.append(buffer.buffer(), 0, buffer.length()).append("\n"); // $NON-NLS-1$;
}
else {
headerBuffer.append(responseHeader.getName())
.append(": ") // $NON-NLS-1$
.append(responseHeader.getValue())
.append("\n"); // $NON-NLS-1$
}
}
/**
* Extracts all the required cookies for that particular URL request and
* sets them in the <code>HttpMethod</code> passed in.
*
* @param request <code>HttpRequest</code> for the request
* @param url <code>URL</code> of the request
* @param cookieManager the <code>CookieManager</code> containing all the cookies
* @return a String containing the cookie details (for the response)
* May be null
*/
protected String setConnectionCookie(HttpRequest request, URL url, CookieManager cookieManager) {
String cookieHeader = null;
if (cookieManager != null) {
cookieHeader = cookieManager.getCookieHeaderForURL(url);
if (cookieHeader != null) {
request.setHeader(HTTPConstants.HEADER_COOKIE, cookieHeader);
}
}
return cookieHeader;
}
/**
* Extracts all the required non-cookie headers for that particular URL request and
* sets them in the <code>HttpMethod</code> passed in
*
* @param request
* <code>HttpRequest</code> which represents the request
* @param url
* <code>URL</code> of the URL request
* @param headerManager
* the <code>HeaderManager</code> containing all the cookies
* for this <code>UrlConfig</code>
* @param cacheManager the CacheManager (may be null)
*/
protected void setConnectionHeaders(HttpRequestBase request, URL url, HeaderManager headerManager, CacheManager cacheManager) {
if (headerManager != null) {
CollectionProperty headers = headerManager.getHeaders();
if (headers != null) {
for (JMeterProperty jMeterProperty : headers) {
org.apache.jmeter.protocol.http.control.Header header
= (org.apache.jmeter.protocol.http.control.Header)
jMeterProperty.getObjectValue();
String n = header.getName();
// Don't allow override of Content-Length
// TODO - what other headers are not allowed?
if (! HTTPConstants.HEADER_CONTENT_LENGTH.equalsIgnoreCase(n)){
String v = header.getValue();
if (HTTPConstants.HEADER_HOST.equalsIgnoreCase(n)) {
int port = getPortFromHostHeader(v, url.getPort());
v = v.replaceFirst(":\\d+$",""); // remove any port specification // $NON-NLS-1$ $NON-NLS-2$
if (port != -1) {
if (port == url.getDefaultPort()) {
port = -1; // no need to specify the port if it is the default
}
}
request.getParams().setParameter(ClientPNames.VIRTUAL_HOST, new HttpHost(v, port));
} else {
request.addHeader(n, v);
}
}
}
}
}
if (cacheManager != null){
cacheManager.setHeaders(url, request);
}
}
/**
* Get port from the value of the Host header, or return the given
* defaultValue
*
* @param hostHeaderValue
* value of the http Host header
* @param defaultValue
* value to be used, when no port could be extracted from
* hostHeaderValue
* @return integer representing the port for the host header
*/
private int getPortFromHostHeader(String hostHeaderValue, int defaultValue) {
String[] hostParts = hostHeaderValue.split(":");
if (hostParts.length > 1) {
String portString = hostParts[hostParts.length - 1];
if (PORT_PATTERN.matcher(portString).matches()) {
return Integer.parseInt(portString);
}
}
return defaultValue;
}
/**
* Get all the request headers for the <code>HttpMethod</code>
*
* @param method
* <code>HttpMethod</code> which represents the request
* @return the headers as a string
*/
private String getConnectionHeaders(HttpRequest method) {
if(method != null) {
// Get all the request headers
StringBuilder hdrs = new StringBuilder(150);
Header[] requestHeaders = method.getAllHeaders();
for (Header requestHeader : requestHeaders) {
// Exclude the COOKIE header, since cookie is reported separately in the sample
if (!HTTPConstants.HEADER_COOKIE.equalsIgnoreCase(requestHeader.getName())) {
writeResponseHeader(hdrs, requestHeader);
}
}
return hdrs.toString();
}
return ""; ////$NON-NLS-1$
}
/**
* Setup credentials for url AuthScope but keeps Proxy AuthScope credentials
* @param client HttpClient
* @param url URL
* @param authManager {@link AuthManager}
* @param key key
*/
private void setConnectionAuthorization(HttpClient client, URL url, AuthManager authManager, HttpClientKey key) {
CredentialsProvider credentialsProvider =
((AbstractHttpClient) client).getCredentialsProvider();
if (authManager != null) {
if(authManager.hasAuthForURL(url)) {
authManager.setupCredentials(client, url, credentialsProvider, localHost);
} else {
credentialsProvider.clear();
}
} else {
Credentials credentials = null;
AuthScope authScope = null;
if(key.hasProxy && !StringUtils.isEmpty(key.proxyUser)) {
authScope = new AuthScope(key.proxyHost, key.proxyPort);
credentials = credentialsProvider.getCredentials(authScope);
}
credentialsProvider.clear();
if(credentials != null) {
credentialsProvider.setCredentials(authScope, credentials);
}
}
}
// Helper class so we can generate request data without dumping entire file contents
private static class ViewableFileBody extends FileBody {
private boolean hideFileData;
public ViewableFileBody(File file, String mimeType) {
super(file, mimeType);
hideFileData = false;
}
@Override
public void writeTo(final OutputStream out) throws IOException {
if (hideFileData) {
out.write("<actual file content, not shown here>".getBytes());// encoding does not really matter here
} else {
super.writeTo(out);
}
}
}
// TODO needs cleaning up
/**
*
* @param post {@link HttpPost}
* @return String posted body if computable
* @throws IOException if sending the data fails due to I/O
*/
protected String sendPostData(HttpPost post) throws IOException {
// Buffer to hold the post body, except file content
StringBuilder postedBody = new StringBuilder(1000);
HTTPFileArg[] files = getHTTPFiles();
final String contentEncoding = getContentEncodingOrNull();
final boolean haveContentEncoding = contentEncoding != null;
// Check if we should do a multipart/form-data or an
// application/x-www-form-urlencoded post request
if(getUseMultipartForPost()) {
// If a content encoding is specified, we use that as the
// encoding of any parameter values
Charset charset = null;
if(haveContentEncoding) {
charset = Charset.forName(contentEncoding);
} else {
charset = MIME.DEFAULT_CHARSET;
}
if(log.isDebugEnabled()) {
log.debug("Building multipart with:getDoBrowserCompatibleMultipart():"+
getDoBrowserCompatibleMultipart()+
", with charset:"+charset+
", haveContentEncoding:"+haveContentEncoding);
}
// Write the request to our own stream
MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create()
.setCharset(charset);
if(getDoBrowserCompatibleMultipart()) {
multipartEntityBuilder.setLaxMode();
} else {
multipartEntityBuilder.setStrictMode();
}
// Create the parts
// Add any parameters
for (JMeterProperty jMeterProperty : getArguments()) {
HTTPArgument arg = (HTTPArgument) jMeterProperty.getObjectValue();
String parameterName = arg.getName();
if (arg.isSkippable(parameterName)) {
continue;
}
StringBody stringBody = new StringBody(arg.getValue(), ContentType.create("text/plain", charset));
FormBodyPart formPart = FormBodyPartBuilder.create(
parameterName, stringBody).build();
multipartEntityBuilder.addPart(formPart);
}
// Add any files
// Cannot retrieve parts once added to the MultiPartEntity, so have to save them here.
ViewableFileBody[] fileBodies = new ViewableFileBody[files.length];
for (int i=0; i < files.length; i++) {
HTTPFileArg file = files[i];
File reservedFile = FileServer.getFileServer().getResolvedFile(file.getPath());
fileBodies[i] = new ViewableFileBody(reservedFile, file.getMimeType());
multipartEntityBuilder.addPart(file.getParamName(), fileBodies[i] );
}
HttpEntity entity = multipartEntityBuilder.build();
post.setEntity(entity);
if (entity.isRepeatable()){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
for(ViewableFileBody fileBody : fileBodies){
fileBody.hideFileData = true;
}
entity.writeTo(bos);
for(ViewableFileBody fileBody : fileBodies){
fileBody.hideFileData = false;
}
bos.flush();
// We get the posted bytes using the encoding used to create it
postedBody.append(bos.toString(
contentEncoding == null ? "US-ASCII" // $NON-NLS-1$ this is the default used by HttpClient
: contentEncoding));
bos.close();
} else {
postedBody.append("<Multipart was not repeatable, cannot view what was sent>"); // $NON-NLS-1$
}
// // Set the content type TODO - needed?
// String multiPartContentType = multiPart.getContentType().getValue();
// post.setHeader(HEADER_CONTENT_TYPE, multiPartContentType);
} else { // not multipart
// Check if the header manager had a content type header
// This allows the user to specify his own content-type for a POST request
Header contentTypeHeader = post.getFirstHeader(HTTPConstants.HEADER_CONTENT_TYPE);
boolean hasContentTypeHeader = contentTypeHeader != null && contentTypeHeader.getValue() != null && contentTypeHeader.getValue().length() > 0;
// If there are no arguments, we can send a file as the body of the request
// TODO: needs a multiple file upload scenerio
if(!hasArguments() && getSendFileAsPostBody()) {
// If getSendFileAsPostBody returned true, it's sure that file is not null
HTTPFileArg file = files[0];
if(!hasContentTypeHeader) {
// Allow the mimetype of the file to control the content type
if(file.getMimeType() != null && file.getMimeType().length() > 0) {
post.setHeader(HTTPConstants.HEADER_CONTENT_TYPE, file.getMimeType());
}
else {
post.setHeader(HTTPConstants.HEADER_CONTENT_TYPE, HTTPConstants.APPLICATION_X_WWW_FORM_URLENCODED);
}
}
FileEntity fileRequestEntity = new FileEntity(new File(file.getPath()),(ContentType) null);// TODO is null correct?
post.setEntity(fileRequestEntity);
// We just add placeholder text for file content
postedBody.append("<actual file content, not shown here>");
} else {
// In a post request which is not multipart, we only support
// parameters, no file upload is allowed
// If a content encoding is specified, we set it as http parameter, so that
// the post body will be encoded in the specified content encoding
if(haveContentEncoding) {
post.getParams().setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, contentEncoding);
}
// If none of the arguments have a name specified, we
// just send all the values as the post body
if(getSendParameterValuesAsPostBody()) {
// Allow the mimetype of the file to control the content type
// This is not obvious in GUI if you are not uploading any files,
// but just sending the content of nameless parameters
// TODO: needs a multiple file upload scenerio
if(!hasContentTypeHeader) {
HTTPFileArg file = files.length > 0? files[0] : null;
if(file != null && file.getMimeType() != null && file.getMimeType().length() > 0) {
post.setHeader(HTTPConstants.HEADER_CONTENT_TYPE, file.getMimeType());
}
else {
// TODO - is this the correct default?
post.setHeader(HTTPConstants.HEADER_CONTENT_TYPE, HTTPConstants.APPLICATION_X_WWW_FORM_URLENCODED);
}
}
// Just append all the parameter values, and use that as the post body
StringBuilder postBody = new StringBuilder();
for (JMeterProperty jMeterProperty : getArguments()) {
HTTPArgument arg = (HTTPArgument) jMeterProperty.getObjectValue();
// Note: if "Encoded?" is not selected, arg.getEncodedValue is equivalent to arg.getValue
if (haveContentEncoding) {
postBody.append(arg.getEncodedValue(contentEncoding));
} else {
postBody.append(arg.getEncodedValue());
}
}
// Let StringEntity perform the encoding
StringEntity requestEntity = new StringEntity(postBody.toString(), contentEncoding);
post.setEntity(requestEntity);
postedBody.append(postBody.toString());
} else {
// It is a normal post request, with parameter names and values
// Set the content type
if(!hasContentTypeHeader) {
post.setHeader(HTTPConstants.HEADER_CONTENT_TYPE, HTTPConstants.APPLICATION_X_WWW_FORM_URLENCODED);
}
// Add the parameters
PropertyIterator args = getArguments().iterator();
List <NameValuePair> nvps = new ArrayList<>();
String urlContentEncoding = contentEncoding;
if(urlContentEncoding == null || urlContentEncoding.length() == 0) {
// Use the default encoding for urls
urlContentEncoding = EncoderCache.URL_ARGUMENT_ENCODING;
}
while (args.hasNext()) {
HTTPArgument arg = (HTTPArgument) args.next().getObjectValue();
// The HTTPClient always urlencodes both name and value,
// so if the argument is already encoded, we have to decode
// it before adding it to the post request
String parameterName = arg.getName();
if (arg.isSkippable(parameterName)){
continue;
}
String parameterValue = arg.getValue();
if(!arg.isAlwaysEncoded()) {
// The value is already encoded by the user
// Must decode the value now, so that when the
// httpclient encodes it, we end up with the same value
// as the user had entered.
parameterName = URLDecoder.decode(parameterName, urlContentEncoding);
parameterValue = URLDecoder.decode(parameterValue, urlContentEncoding);
}
// Add the parameter, httpclient will urlencode it
nvps.add(new BasicNameValuePair(parameterName, parameterValue));
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(nvps, urlContentEncoding);
post.setEntity(entity);
if (entity.isRepeatable()){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
post.getEntity().writeTo(bos);
bos.flush();
// We get the posted bytes using the encoding used to create it
postedBody.append(bos.toString(contentEncoding != null?contentEncoding:SampleResult.DEFAULT_HTTP_ENCODING));
bos.close();
} else {
postedBody.append("<RequestEntity was not repeatable, cannot view what was sent>");
}
}
}
}
return postedBody.toString();
}
// TODO merge put and post methods as far as possible.
// e.g. post checks for multipart form/files, and if not, invokes sendData(HttpEntityEnclosingRequestBase)
/**
* Creates the entity data to be sent.
* <p>
* If there is a file entry with a non-empty MIME type we use that to
* set the request Content-Type header, otherwise we default to whatever
* header is present from a Header Manager.
* <p>
* If the content charset {@link #getContentEncoding()} is null or empty
* we use the HC4 default provided by {@link HTTP#DEF_CONTENT_CHARSET} which is
* ISO-8859-1.
*
* @param entity to be processed, e.g. PUT or PATCH
* @return the entity content, may be empty
* @throws UnsupportedEncodingException for invalid charset name
* @throws IOException cannot really occur for ByteArrayOutputStream methods
*/
protected String sendEntityData( HttpEntityEnclosingRequestBase entity) throws IOException {
// Buffer to hold the entity body
StringBuilder entityBody = new StringBuilder(1000);
boolean hasEntityBody = false;
final HTTPFileArg[] files = getHTTPFiles();
// Allow the mimetype of the file to control the content type
// This is not obvious in GUI if you are not uploading any files,
// but just sending the content of nameless parameters
final HTTPFileArg file = files.length > 0? files[0] : null;
String contentTypeValue = null;
if(file != null && file.getMimeType() != null && file.getMimeType().length() > 0) {
contentTypeValue = file.getMimeType();
entity.setHeader(HEADER_CONTENT_TYPE, contentTypeValue); // we provide the MIME type here
}
// Check for local contentEncoding (charset) override; fall back to default for content body
// we do this here rather so we can use the same charset to retrieve the data
final String charset = getContentEncoding(HTTP.DEF_CONTENT_CHARSET.name());
// Only create this if we are overriding whatever default there may be
// If there are no arguments, we can send a file as the body of the request
if(!hasArguments() && getSendFileAsPostBody()) {
hasEntityBody = true;
// If getSendFileAsPostBody returned true, it's sure that file is not null
File reservedFile = FileServer.getFileServer().getResolvedFile(files[0].getPath());
FileEntity fileRequestEntity = new FileEntity(reservedFile); // no need for content-type here
entity.setEntity(fileRequestEntity);
}
// If none of the arguments have a name specified, we
// just send all the values as the entity body
else if(getSendParameterValuesAsPostBody()) {
hasEntityBody = true;
// Just append all the parameter values, and use that as the entity body
StringBuilder entityBodyContent = new StringBuilder();
for (JMeterProperty jMeterProperty : getArguments()) {
HTTPArgument arg = (HTTPArgument) jMeterProperty.getObjectValue();
// Note: if "Encoded?" is not selected, arg.getEncodedValue is equivalent to arg.getValue
if (charset != null) {
entityBodyContent.append(arg.getEncodedValue(charset));
} else {
entityBodyContent.append(arg.getEncodedValue());
}
}
StringEntity requestEntity = new StringEntity(entityBodyContent.toString(), charset);
entity.setEntity(requestEntity);
}
// Check if we have any content to send for body
if(hasEntityBody) {
// If the request entity is repeatable, we can send it first to
// our own stream, so we can return it
final HttpEntity entityEntry = entity.getEntity();
if(entityEntry.isRepeatable()) {
entityBody.append("<actual file content, not shown here>");
}
else { // this probably cannot happen
entityBody.append("<RequestEntity was not repeatable, cannot view what was sent>");
}
}
return entityBody.toString(); // may be the empty string
}
/**
*
* @return the value of {@link #getContentEncoding()}; forced to null if empty
*/
private String getContentEncodingOrNull() {
return getContentEncoding(null);
}
/**
* @param dflt the default to be used
* @return the value of {@link #getContentEncoding()}; default if null or empty
*/
private String getContentEncoding(String dflt) {
String ce = getContentEncoding();
if (isNullOrEmptyTrimmed(ce)) {
return dflt;
} else {
return ce;
}
}
private void saveConnectionCookies(HttpResponse method, URL u, CookieManager cookieManager) {
if (cookieManager != null) {
Header[] hdrs = method.getHeaders(HTTPConstants.HEADER_SET_COOKIE);
for (Header hdr : hdrs) {
cookieManager.addCookieFromHeader(hdr.getValue(),u);
}
}
}
@Override
protected void notifyFirstSampleAfterLoopRestart() {
log.debug("notifyFirstSampleAfterLoopRestart");
resetSSLContext = !USE_CACHED_SSL_CONTEXT;
}
@Override
protected void threadFinished() {
log.debug("Thread Finished");
closeThreadLocalConnections();
}
/**
*
*/
private void closeThreadLocalConnections() {
// Does not need to be synchronised, as all access is from same thread
Map<HttpClientKey, HttpClient> mapHttpClientPerHttpClientKey = HTTPCLIENTS_CACHE_PER_THREAD_AND_HTTPCLIENTKEY.get();
if ( mapHttpClientPerHttpClientKey != null ) {
for ( HttpClient cl : mapHttpClientPerHttpClientKey.values() ) {
((AbstractHttpClient) cl).clearRequestInterceptors();
((AbstractHttpClient) cl).clearResponseInterceptors();
((AbstractHttpClient) cl).close();
cl.getConnectionManager().shutdown();
}
mapHttpClientPerHttpClientKey.clear();
}
}
@Override
public boolean interrupt() {
HttpUriRequest request = currentRequest;
if (request != null) {
currentRequest = null; // don't try twice
try {
request.abort();
} catch (UnsupportedOperationException e) {
log.warn("Could not abort pending request", e);
}
}
return request != null;
}
}