/*
* 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.tcp.sampler;
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.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.config.ConfigTestElement;
import org.apache.jmeter.samplers.AbstractSampler;
import org.apache.jmeter.samplers.Entry;
import org.apache.jmeter.samplers.Interruptible;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.ThreadListener;
import org.apache.jmeter.util.JMeterUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A sampler which understands Tcp requests.
*
*/
public class TCPSampler extends AbstractSampler implements ThreadListener, Interruptible {
private static final long serialVersionUID = 280L;
private static final Logger log = LoggerFactory.getLogger(TCPSampler.class);
private static final Set<String> APPLIABLE_CONFIG_CLASSES = new HashSet<>(
Arrays.asList(
"org.apache.jmeter.config.gui.LoginConfigGui",
"org.apache.jmeter.protocol.tcp.config.gui.TCPConfigGui",
"org.apache.jmeter.config.gui.SimpleConfigGui"
));
public static final String SERVER = "TCPSampler.server"; //$NON-NLS-1$
public static final String PORT = "TCPSampler.port"; //$NON-NLS-1$
public static final String FILENAME = "TCPSampler.filename"; //$NON-NLS-1$
public static final String CLASSNAME = "TCPSampler.classname";//$NON-NLS-1$
public static final String NODELAY = "TCPSampler.nodelay"; //$NON-NLS-1$
public static final String TIMEOUT = "TCPSampler.timeout"; //$NON-NLS-1$
public static final String TIMEOUT_CONNECT = "TCPSampler.ctimeout"; //$NON-NLS-1$
public static final String REQUEST = "TCPSampler.request"; //$NON-NLS-1$
public static final String RE_USE_CONNECTION = "TCPSampler.reUseConnection"; //$NON-NLS-1$
public static final boolean RE_USE_CONNECTION_DEFAULT = true;
public static final String CLOSE_CONNECTION = "TCPSampler.closeConnection"; //$NON-NLS-1$
public static final boolean CLOSE_CONNECTION_DEFAULT = false;
public static final String SO_LINGER = "TCPSampler.soLinger"; //$NON-NLS-1$
public static final String EOL_BYTE = "TCPSampler.EolByte"; //$NON-NLS-1$
private static final String TCPKEY = "TCP"; //$NON-NLS-1$ key for HashMap
private static final String ERRKEY = "ERR"; //$NON-NLS-1$ key for HashMap
// the response is scanned for these strings
private static final String STATUS_PREFIX = JMeterUtils.getPropDefault("tcp.status.prefix", ""); //$NON-NLS-1$
private static final String STATUS_SUFFIX = JMeterUtils.getPropDefault("tcp.status.suffix", ""); //$NON-NLS-1$
private static final String STATUS_PROPERTIES = JMeterUtils.getPropDefault("tcp.status.properties", ""); //$NON-NLS-1$
private static final Properties STATUS_PROPS = new Properties();
private static final String PROTO_PREFIX = "org.apache.jmeter.protocol.tcp.sampler."; //$NON-NLS-1$
private static final boolean HAVE_STATUS_PROPS;
static {
boolean hsp = false;
log.debug("Status prefix=" + STATUS_PREFIX); //$NON-NLS-1$
log.debug("Status suffix=" + STATUS_SUFFIX); //$NON-NLS-1$
log.debug("Status properties=" + STATUS_PROPERTIES); //$NON-NLS-1$
if (STATUS_PROPERTIES.length() > 0) {
File f = new File(STATUS_PROPERTIES);
try (FileInputStream fis = new FileInputStream(f)){
STATUS_PROPS.load(fis);
log.debug("Successfully loaded properties"); //$NON-NLS-1$
hsp = true;
} catch (FileNotFoundException e) {
log.debug("Property file not found"); //$NON-NLS-1$
} catch (IOException e) {
log.debug("Property file error " + e.toString()); //$NON-NLS-1$
}
}
HAVE_STATUS_PROPS = hsp;
}
/** the cache of TCP Connections */
// KEY = TCPKEY or ERRKEY, Entry= Socket or String
private static final ThreadLocal<Map<String, Object>> tp =
ThreadLocal.withInitial(HashMap::new);
private transient TCPClient protocolHandler;
private transient boolean firstSample; // Are we processing the first sample?
private transient volatile Socket currentSocket; // used for handling interrupt
public TCPSampler() {
log.debug("Created " + this); //$NON-NLS-1$
}
private String getError() {
Map<String, Object> cp = tp.get();
return (String) cp.get(ERRKEY);
}
private Socket getSocket(String socketKey) {
Map<String, Object> cp = tp.get();
Socket con = null;
if (isReUseConnection()) {
con = (Socket) cp.get(socketKey);
if (con != null) {
log.debug(this + " Reusing connection " + con); //$NON-NLS-1$
}
}
if (con == null) {
// Not in cache, so create new one and cache it
try {
closeSocket(socketKey); // Bug 44910 - close previous socket (if any)
SocketAddress sockaddr = new InetSocketAddress(getServer(), getPort());
con = new Socket(); // NOSONAR socket is either cache in ThreadLocal for reuse and closed at end of thread or closed here
if (getPropertyAsString(SO_LINGER,"").length() > 0){
con.setSoLinger(true, getSoLinger());
}
con.connect(sockaddr, getConnectTimeout());
if(log.isDebugEnabled()) {
log.debug("Created new connection " + con); //$NON-NLS-1$
}
cp.put(socketKey, con);
} catch (UnknownHostException e) {
log.warn("Unknown host for " + getLabel(), e);//$NON-NLS-1$
cp.put(ERRKEY, e.toString());
return null;
} catch (IOException e) {
log.warn("Could not create socket for " + getLabel(), e); //$NON-NLS-1$
cp.put(ERRKEY, e.toString());
return null;
}
}
// (re-)Define connection params - Bug 50977
try {
con.setSoTimeout(getTimeout());
con.setTcpNoDelay(getNoDelay());
if(log.isDebugEnabled()) {
log.debug(this + " Timeout " + getTimeout() + " NoDelay " + getNoDelay()); //$NON-NLS-1$
}
} catch (SocketException se) {
log.warn("Could not set timeout or nodelay for " + getLabel(), se); //$NON-NLS-1$
cp.put(ERRKEY, se.toString());
}
return con;
}
/**
* @return String socket key in cache Map
*/
private String getSocketKey() {
return TCPKEY+"#"+getServer()+"#"+getPort()+"#"+getUsername()+"#"+getPassword();
}
public String getUsername() {
return getPropertyAsString(ConfigTestElement.USERNAME);
}
public String getPassword() {
return getPropertyAsString(ConfigTestElement.PASSWORD);
}
public void setServer(String newServer) {
this.setProperty(SERVER, newServer);
}
public String getServer() {
return getPropertyAsString(SERVER);
}
public boolean isReUseConnection() {
return getPropertyAsBoolean(RE_USE_CONNECTION, RE_USE_CONNECTION_DEFAULT);
}
public void setCloseConnection(String close) {
this.setProperty(CLOSE_CONNECTION, close, "");
}
public boolean isCloseConnection() {
return getPropertyAsBoolean(CLOSE_CONNECTION, CLOSE_CONNECTION_DEFAULT);
}
public void setSoLinger(String soLinger) {
this.setProperty(SO_LINGER, soLinger, "");
}
public int getSoLinger() {
return getPropertyAsInt(SO_LINGER);
}
public void setEolByte(String eol) {
this.setProperty(EOL_BYTE, eol, "");
}
public int getEolByte() {
return getPropertyAsInt(EOL_BYTE);
}
public void setPort(String newFilename) {
this.setProperty(PORT, newFilename);
}
public int getPort() {
return getPropertyAsInt(PORT);
}
public void setFilename(String newFilename) {
this.setProperty(FILENAME, newFilename);
}
public String getFilename() {
return getPropertyAsString(FILENAME);
}
public void setRequestData(String newRequestData) {
this.setProperty(REQUEST, newRequestData);
}
public String getRequestData() {
return getPropertyAsString(REQUEST);
}
public void setTimeout(String newTimeout) {
this.setProperty(TIMEOUT, newTimeout);
}
public int getTimeout() {
return getPropertyAsInt(TIMEOUT);
}
public void setConnectTimeout(String newTimeout) {
this.setProperty(TIMEOUT_CONNECT, newTimeout, "");
}
public int getConnectTimeout() {
return getPropertyAsInt(TIMEOUT_CONNECT, 0);
}
public boolean getNoDelay() {
return getPropertyAsBoolean(NODELAY);
}
public void setClassname(String classname) {
this.setProperty(CLASSNAME, classname, ""); //$NON-NLS-1$
}
public String getClassname() {
String clazz = getPropertyAsString(CLASSNAME,"");
if (clazz==null || clazz.length()==0){
clazz = JMeterUtils.getPropDefault("tcp.handler", "TCPClientImpl"); //$NON-NLS-1$ $NON-NLS-2$
}
return clazz;
}
/**
* Returns a formatted string label describing this sampler Example output:
* Tcp://Tcp.nowhere.com/pub/README.txt
*
* @return a formatted string label describing this sampler
*/
public String getLabel() {
return "tcp://" + this.getServer() + ":" + this.getPort();//$NON-NLS-1$ $NON-NLS-2$
}
private Class<?> getClass(String className) {
Class<?> c = null;
try {
c = Class.forName(className, false, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
try {
c = Class.forName(PROTO_PREFIX + className, false, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e1) {
log.error("Could not find protocol class '" + className+"'"); //$NON-NLS-1$
}
}
return c;
}
private TCPClient getProtocol() {
TCPClient TCPClient = null;
Class<?> javaClass = getClass(getClassname());
if (javaClass == null){
return null;
}
try {
TCPClient = (TCPClient) javaClass.newInstance();
if (getPropertyAsString(EOL_BYTE, "").length()>0){
TCPClient.setEolByte(getEolByte());
log.info("Using eolByte=" + getEolByte());
}
if (log.isDebugEnabled()) {
log.debug(this + "Created: " + getClassname() + "@" + Integer.toHexString(TCPClient.hashCode())); //$NON-NLS-1$
}
} catch (Exception e) {
log.error(this + " Exception creating: " + getClassname(), e); //$NON-NLS-1$
}
return TCPClient;
}
@Override
public SampleResult sample(Entry e)// Entry tends to be ignored ...
{
if (firstSample) { // Do stuff we cannot do as part of threadStarted()
initSampling();
firstSample=false;
}
final boolean reUseConnection = isReUseConnection();
final boolean closeConnection = isCloseConnection();
String socketKey = getSocketKey();
if (log.isDebugEnabled()){
log.debug(getLabel() + " " + getFilename() + " " + getUsername() + " " + getPassword());
}
SampleResult res = new SampleResult();
boolean isSuccessful = false;
res.setSampleLabel(getName());// Use the test element name for the label
StringBuilder sb = new StringBuilder();
sb.append("Host: ").append(getServer()); // $NON-NLS-1$
sb.append(" Port: ").append(getPort()); // $NON-NLS-1$
sb.append("\n"); // $NON-NLS-1$
sb.append("Reuse: ").append(reUseConnection); // $NON-NLS-1$
sb.append(" Close: ").append(closeConnection); // $NON-NLS-1$
sb.append("\n["); // $NON-NLS-1$
sb.append("SOLINGER: ").append(getSoLinger()); // $NON-NLS-1$
sb.append(" EOL: ").append(getEolByte()); // $NON-NLS-1$
sb.append(" noDelay: ").append(getNoDelay()); // $NON-NLS-1$
sb.append("]"); // $NON-NLS-1$
res.setSamplerData(sb.toString());
res.sampleStart();
try {
Socket sock;
try {
sock = getSocket(socketKey);
} finally {
res.connectEnd();
}
if (sock == null) {
res.setResponseCode("500"); //$NON-NLS-1$
res.setResponseMessage(getError());
} else if (protocolHandler == null){
res.setResponseCode("500"); //$NON-NLS-1$
res.setResponseMessage("Protocol handler not found");
} else {
currentSocket = sock;
InputStream is = sock.getInputStream();
OutputStream os = sock.getOutputStream();
String req = getRequestData();
// TODO handle filenames
res.setSamplerData(req);
protocolHandler.write(os, req);
String in = protocolHandler.read(is);
isSuccessful = setupSampleResult(res, in, null, protocolHandler);
}
} catch (ReadException ex) {
log.error("", ex);
isSuccessful=setupSampleResult(res, ex.getPartialResponse(), ex,protocolHandler);
closeSocket(socketKey);
} catch (Exception ex) {
log.error("", ex);
isSuccessful=setupSampleResult(res, "", ex, protocolHandler);
closeSocket(socketKey);
} finally {
currentSocket = null;
// Calculate response time
res.sampleEnd();
// Set if we were successful or not
res.setSuccessful(isSuccessful);
if (!reUseConnection || closeConnection) {
closeSocket(socketKey);
}
}
return res;
}
/**
* Fills SampleResult object
* @param sampleResult {@link SampleResult}
* @param readResponse Response read until error occurred
* @param exception Source exception
* @param protocolHandler {@link TCPClient}
* @return boolean if sample is considered as successful
*/
private boolean setupSampleResult(SampleResult sampleResult,
String readResponse,
Exception exception,
TCPClient protocolHandler) {
sampleResult.setResponseData(readResponse,
protocolHandler != null ? protocolHandler.getCharset() : null);
sampleResult.setDataType(SampleResult.TEXT);
if(exception==null) {
sampleResult.setResponseCodeOK();
sampleResult.setResponseMessage("OK"); //$NON-NLS-1$
} else {
sampleResult.setResponseCode("500"); //$NON-NLS-1$
sampleResult.setResponseMessage(exception.toString()); //$NON-NLS-1$
}
boolean isSuccessful = exception == null;
// Reset the status code if the message contains one
if (!StringUtils.isEmpty(readResponse) && STATUS_PREFIX.length() > 0) {
int i = readResponse.indexOf(STATUS_PREFIX);
int j = readResponse.indexOf(STATUS_SUFFIX, i + STATUS_PREFIX.length());
if (i != -1 && j > i) {
String rc = readResponse.substring(i + STATUS_PREFIX.length(), j);
sampleResult.setResponseCode(rc);
isSuccessful = isSuccessful && checkResponseCode(rc);
if (HAVE_STATUS_PROPS) {
sampleResult.setResponseMessage(STATUS_PROPS.getProperty(rc, "Status code not found in properties")); //$NON-NLS-1$
} else {
sampleResult.setResponseMessage("No status property file");
}
} else {
sampleResult.setResponseCode("999"); //$NON-NLS-1$
sampleResult.setResponseMessage("Status value not found");
isSuccessful = false;
}
}
return isSuccessful;
}
/**
* @param rc response code
* @return whether this represents success or not
*/
private boolean checkResponseCode(String rc) {
if (rc.compareTo("400") >= 0 && rc.compareTo("499") <= 0) { //$NON-NLS-1$ $NON-NLS-2$
return false;
}
if (rc.compareTo("500") >= 0 && rc.compareTo("599") <= 0) { //$NON-NLS-1$ $NON-NLS-2$
return false;
}
return true;
}
@Override
public void threadStarted() {
log.debug("Thread Started"); //$NON-NLS-1$
firstSample = true;
}
// Cannot do this as part of threadStarted() because the Config elements have not been processed.
private void initSampling(){
protocolHandler = getProtocol();
log.debug("Using Protocol Handler: " + //$NON-NLS-1$
(protocolHandler == null ? "NONE" : protocolHandler.getClass().getName())); //$NON-NLS-1$
if (protocolHandler != null){
protocolHandler.setupTest();
}
}
/**
* Close socket of current sampler
*/
private void closeSocket(String socketKey) {
Map<String, Object> cp = tp.get();
Socket con = (Socket) cp.remove(socketKey);
if (con != null) {
log.debug(this + " Closing connection " + con); //$NON-NLS-1$
try {
con.close();
} catch (IOException e) {
log.warn("Error closing socket "+e); //$NON-NLS-1$
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void threadFinished() {
log.debug("Thread Finished"); //$NON-NLS-1$
tearDown();
if (protocolHandler != null){
protocolHandler.teardownTest();
}
}
/**
* Closes all connections, clears Map and remove thread local Map
*/
private void tearDown() {
Map<String, Object> cp = tp.get();
cp.forEach((k, v) -> {
if(k.startsWith(TCPKEY)) {
try {
((Socket)v).close();
} catch (IOException e) {
// NOOP
}
}
});
cp.clear();
tp.remove();
}
/**
* @see org.apache.jmeter.samplers.AbstractSampler#applies(org.apache.jmeter.config.ConfigTestElement)
*/
@Override
public boolean applies(ConfigTestElement configElement) {
String guiClass = configElement.getProperty(TestElement.GUI_CLASS).getStringValue();
return APPLIABLE_CONFIG_CLASSES.contains(guiClass);
}
@Override
public boolean interrupt() {
Optional<Socket> sock = Optional.ofNullable(currentSocket); // fetch in case gets nulled later
if(sock.isPresent()) {
try {
sock.get().close();
} catch (IOException e) {
// ignored
}
return true;
} else {
return false;
}
}
}