All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.luminis.jmeter.wssampler.WebsocketSampler Maven / Gradle / Ivy

Go to download

JMeter add-on that defines a number of samplers for load testing WebSocket applications.

There is a newer version: 1.2.10
Show newest version
/*
 * Copyright © 2016, 2017, 2018 Peter Doornbosch
 *
 * This file is part of JMeter-WebSocket-Samplers, a JMeter add-on for load-testing WebSocket applications.
 *
 * JMeter-WebSocket-Samplers is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * JMeter-WebSocket-Samplers is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
 * more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see .
 */
package eu.luminis.jmeter.wssampler;

import eu.luminis.websocket.*;
import org.apache.jmeter.JMeter;
import org.apache.jmeter.gui.GuiPackage;
import org.apache.jmeter.protocol.http.control.CookieManager;
import org.apache.jmeter.protocol.http.control.Header;
import org.apache.jmeter.protocol.http.control.HeaderManager;
import org.apache.jmeter.samplers.AbstractSampler;
import org.apache.jmeter.samplers.Entry;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.ThreadListener;
import org.apache.jmeter.threads.JMeterContextService;
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;

import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Base class for websocket samplers.
 * Note on synchronization: JMeter operates samplers from one thread only (the Thread-Group's sampling thread). Only
 * instantiation of these objects is done on another thread (StandardJMeterEngine main thread). Hence, only members
 * set in the constructor must be made thread safe.
 */
abstract public class WebsocketSampler extends AbstractSampler implements ThreadListener {

    private static final String VAR_WEBSOCKET_LAST_FRAME_FINAL = "websocket.last_frame_final";

    enum ThreadStopPolicy { NONE, TCPCLOSE, WSCLOSE };

    public static final String WS_THREAD_STOP_POLICY_PROPERTY = "websocket.thread.stop.policy";

    public static final int MIN_CONNECTION_TIMEOUT = 1;
    public static final int MAX_CONNECTION_TIMEOUT = 999999;
    public static final int MIN_READ_TIMEOUT = 0;
    public static final int MAX_READ_TIMEOUT = 999999;
    public static final int DEFAULT_WS_PORT = 80;

    // Control reuse of cached SSL Context in subsequent connections on the same thread
    protected static final boolean USE_CACHED_SSL_CONTEXT = JMeterUtils.getPropDefault("https.use.cached.ssl.context", true);

    protected static final ThreadLocal threadLocalCachedConnection = new ThreadLocal<>();

    protected HeaderManager headerManager;
    protected CookieManager cookieManager;
    protected List frameFilters = new ArrayList<>();
    protected int readTimeout;
    protected int connectTimeout;

    // Proxy configuration: only static proxy configuration is supported.
    private static String proxyHost;
    private static int proxyPort;
    private static List nonProxyHosts;
    private static List nonProxyWildcards;
    private static String proxyUsername;
    private static String proxyPassword;

    // Thread stop policy: what to do with connection when thread ends?
    private  static ThreadStopPolicy threadStopPolicy = ThreadStopPolicy.NONE;

    abstract protected String validateArguments();

    abstract protected WebSocketClient prepareWebSocketClient(SampleResult result);


    static {
        checkJMeterVersion();
        initProxyConfiguration();
        checkForOtherWebsocketPlugins();
        initThreadStopPolicy();
    }

    public void clearTestElementChildren() {
        frameFilters.clear();
    }

    @Override
    public SampleResult sample(Entry entry) {
        Logger log = getLogger();

        SampleResult result = new SampleResult();
        result.setSampleLabel(getName());
        String validationError = validateArguments();
        if (validationError != null) {
            result.setResponseCode("Sampler error");
            result.setResponseMessage("Sampler error: " + validationError);
            return result;
        }

        readTimeout = Integer.parseInt(getReadTimeout());
        connectTimeout = Integer.parseInt(getConnectTimeout());

        WebSocketClient wsClient = prepareWebSocketClient(result);
        if (wsClient == null)
            return result;

        if (! wsClient.isConnected()) {
            if (headerManager != null || cookieManager != null) {
                Map additionalHeaders = convertHeaders(headerManager);
                String cookieHeaderValue = getCookieHeaderValue(cookieManager, wsClient.getConnectUrl());
                if (cookieHeaderValue != null)
                    additionalHeaders.put("Cookie", cookieHeaderValue);
                wsClient.setAdditionalUpgradeRequestHeaders(additionalHeaders);
                result.setRequestHeaders(additionalHeaders.entrySet().stream().map(e -> e.getKey() + ": " + e.getValue()).collect(Collectors.joining("\n")));
            }
        }

        boolean gotNewConnection = false;
        result.sampleStart(); // Start timing
        try {
            Map responseHeaders = null;
            if (! wsClient.isConnected()) {
                if (useTLS() && !USE_CACHED_SSL_CONTEXT)
                    ((JsseSSLManager) SSLManager.getInstance()).resetContext();
                if (useProxy(wsClient.getConnectUrl().getHost()))
                    wsClient.useProxy(proxyHost, proxyPort, proxyUsername, proxyPassword);

                result.setSamplerData("Connect URL:\n" + getConnectUrl(wsClient.getConnectUrl()) + "\n");  // Ensure connect URL is reported in case of a connect error.

                WebSocketClient.HttpResult httpResult = wsClient.connect(connectTimeout, readTimeout);
                responseHeaders = httpResult.responseHeaders;
                result.connectEnd();
                result.setHeadersSize(httpResult.responseSize);
                result.setSentBytes(httpResult.requestSize);
                gotNewConnection = true;
            }
            else {
                result.setSamplerData("Connect URL:\n" + getConnectUrl(wsClient.getConnectUrl()) + "\n(using existing connection)\n");
            }
            Frame response = doSample(wsClient, result);
            result.sampleEnd(); // End timimg
            if (response != null) {
                result.setHeadersSize(result.getHeadersSize() + response.getSize() - response.getPayloadSize());
                result.setBodySize(result.getBodySize() + response.getPayloadSize());
            }

            if (gotNewConnection) {
                result.setResponseCode("101");
                result.setResponseMessage("Switching Protocols");
                result.setResponseHeaders(responseHeaders.entrySet().stream().map( header -> header.getKey() + ": " + header.getValue()).collect(Collectors.joining("\n")));
            }
            else {
                result.setResponseCodeOK();
                result.setResponseMessage("OK");
            }
            postProcessResponse(response, result);
            result.setSuccessful(true);
        }
        catch (UnexpectedFrameException e) {
            handleUnexpectedFrameException(e, result);
        }
        catch (MalformedURLException e) {
            // Impossible
            throw new RuntimeException(e);
        }
        catch (HttpUpgradeException upgradeError) {
            result.sampleEnd(); // End timimg
            getLogger().debug("Http upgrade error in sampler '" + getName() + "'.", upgradeError);
            result.setResponseCode(upgradeError.getStatusCodeAsString());
            result.setResponseMessage(upgradeError.getMessage());
        }
        catch (IOException ioExc) {
            if (result.getEndTime() == 0)
                result.sampleEnd(); // End timimg
            getLogger().debug("I/O Error in sampler '" + getName() + "'.", ioExc);
            result.setResponseCode("Websocket I/O error");
            result.setResponseMessage("WebSocket I/O error: " + ioExc.getMessage());
        }
        catch (SamplingAbortedException abort) {
            if (result.getEndTime() == 0)
                result.sampleEnd(); // End timimg
            // Error should have been handled by subclass
        }
        catch (Exception error) {
            if (result.getEndTime() == 0)
                result.sampleEnd(); // End timimg
            getLogger().error("Unhandled error in sampler '"  + getName() + "'.", error);
            result.setResponseCode("Sampler error");
            result.setResponseMessage("Sampler error: " + error);
        }

        if (gotNewConnection)
            threadLocalCachedConnection.set(wsClient);
        else {
            if (! wsClient.isConnected())
                threadLocalCachedConnection.set(null);
        }

        return result;
    }

    abstract protected Frame doSample(WebSocketClient wsClient, SampleResult result) throws IOException, UnexpectedFrameException, SamplingAbortedException;

    protected void postProcessResponse(Frame response, SampleResult result) {}

    protected void handleUnexpectedFrameException(UnexpectedFrameException e, SampleResult result) {
        result.sampleEnd(); // End timimg
        getLogger().error("Unexpected frame type received in sampler '" + getName() + "': " + e.getReceivedFrame());
        result.setResponseCode("Sampler error: unexpected frame type.");
        result.setResponseMessage("Received: " + e.getReceivedFrame());
    }

    protected void sendFrame(WebSocketClient wsClient, SampleResult result, boolean binary, String requestData, File requestDataFile) throws SamplingAbortedException, IOException {
        Frame sentFrame;
        try {
            if (binary) {
                byte[] binRequestData;
                String printableRequestData;

                try {
                    if (requestDataFile != null) {
                        binRequestData = Files.readAllBytes(requestDataFile.toPath());
                        printableRequestData = BinaryUtils.formatBinary(binRequestData, 100, "...");
                    } else {
                        binRequestData = BinaryUtils.parseBinaryString(requestData);
                        printableRequestData = requestData;
                    }
                } catch (NumberFormatException noNumber) {
                    // Thrown by BinaryUtils.parseBinaryString
                    result.sampleEnd(); // End timimg
                    getLogger().error("Sampler '" + getName() + "': request data is not binary: " + requestData);
                    result.setResponseCode("Sampler Error");
                    result.setResponseMessage("Request data is not binary: " + requestData);
                    throw new SamplingAbortedException();
                }
                // If the sendBinaryFrame method throws an IOException, some data may have been send, so we'd better register all request data
                result.setSamplerData(result.getSamplerData() + "\nRequest data:\n" + printableRequestData + "\n");
                sentFrame = wsClient.sendBinaryFrame(binRequestData);
            }
            else {
                if (requestDataFile != null) {
                    requestData = new String(Files.readAllBytes(requestDataFile.toPath()), StandardCharsets.UTF_8.name());
                }
                result.setSamplerData(result.getSamplerData() + "\nRequest data:\n" + requestData + "\n");
                sentFrame = wsClient.sendTextFrame(requestData);
            }
            result.setSentBytes(sentFrame.getSize());
        }
        catch (NoSuchFileException | AccessDeniedException fileError) {
            // Thrown by Files.readAllBytes
            result.sampleEnd(); // End timimg
            String rootCause = "";
            if (fileError instanceof NoSuchFileException)
                rootCause = "file '" + fileError.getFile() + "' not found";
            else if (fileError instanceof AccessDeniedException)
                rootCause = "file '" + fileError.getFile() + "' not readable";
            getLogger().error("Sampler '" + getName() + "': can't load request data; " + rootCause);
            result.setResponseCode("Sampler Error");
            result.setResponseMessage("Request data cannot be loaded, " + rootCause);
            throw new SamplingAbortedException();
        }
    }

    protected Frame readFrame(WebSocketClient wsClient, SampleResult result, boolean binary) throws IOException, UnexpectedFrameException {
        Frame receivedFrame;
        if (! frameFilters.isEmpty()) {
            receivedFrame = frameFilters.get(0).receiveFrame(frameFilters.subList(1, frameFilters.size()), wsClient, readTimeout, result);
            if ((binary && receivedFrame.isBinary()) || (!binary && receivedFrame.isText()))
                return receivedFrame;
            else
                throw new UnexpectedFrameException(receivedFrame);
        } else
            return binary ? wsClient.receiveBinaryData(readTimeout) : wsClient.receiveText(readTimeout);
    }

    public void addTestElement(TestElement element) {
        if (element instanceof HeaderManager) {
            headerManager = (HeaderManager) element;
        } else if (element instanceof CookieManager) {
            cookieManager = (CookieManager) element;
        } else if (element instanceof FrameFilter) {
            if (! frameFilters.contains(element)) {
                frameFilters.add((FrameFilter) element);
                getLogger().debug("Added filter " + element + " to sampler " + this + "; filter list is now " + frameFilters);
            }
            else {
                getLogger().debug("Ignoring additional filter " + element + "; already present in chain.");
            }
        } else {
            super.addTestElement(element);
        }
    }

    @Override
    public void threadStarted() {
    }

    @Override
    public void threadFinished() {
        if (threadStopPolicy != ThreadStopPolicy.NONE) {
            WebSocketClient webSocketClient = threadLocalCachedConnection.get();
            if (webSocketClient != null) {
                if (threadStopPolicy.equals(ThreadStopPolicy.WSCLOSE)) {
                    try {
                        getLogger().debug("Test thread finished: closing WebSocket connection");
                        webSocketClient.sendClose(1000, "test thread finished");
                    } catch (Exception e) {
                        getLogger().error("Closing WebSocket connection failed", e);
                    }
                }
                else {
                    getLogger().debug("Test thread finsished: closing connection");
                }
                webSocketClient.dispose();
                threadLocalCachedConnection.remove();
            }
        }
    }

    protected String getConnectUrl(URL url) {
        String path = url.getFile();
        if (! path.startsWith("/"))
            path = "/" + path;

        if ("http".equals(url.getProtocol()))
            return "ws" + "://" + url.getHost() + ":" + url.getPort() + path;
        else if ("https".equals(url.getProtocol()))
            return "wss" + "://" + url.getHost() + ":" + url.getPort() + path;
        else {
            getLogger().error("Invalid protocol in sampler '"+ getName() + "': " + url.getProtocol());
            return "";
        }
    }

    protected void processDefaultReadResponse(DataFrame response, boolean binary, SampleResult result) {
        if (binary) {
            byte[] responseData = ((BinaryFrame) response).getBinaryData();
            result.setResponseData(responseData);
            getLogger().debug("Sampler '" + getName() + "' received " + response.getTypeAsString() + " frame with data: " + BinaryUtils.formatBinary(responseData));
        }
        else {
            result.setResponseData(((TextFrame) response).getText(), StandardCharsets.UTF_8.name());
            getLogger().debug("Sampler '" + getName() + "' received " + response.getTypeAsString() + " frame with text: '" + ((TextFrame) response).getText() + "'");
        }
        result.setDataType(binary ? SampleResult.BINARY : SampleResult.TEXT);
        JMeterContextService.getContext().getVariables().put(VAR_WEBSOCKET_LAST_FRAME_FINAL, String.valueOf(response.isFinalFragment()));
    }

    protected String validatePortNumber(String value) {
        try {
            int port = Integer.parseInt(value);
            if (port <= 0 || port > 65535)
                return "Port number '" + value + "' is not valid.";
            if (port == 80 && useTLS())
                getLogger().warn("Sampler '"+ getName() + "' is using wss protocol (with TLS) on port 80; this might indicate a configuration error");
            if (port == 443 && !useTLS())
                getLogger().warn("Sampler '"+ getName() + "' is using ws protocol (without TLS) on port 443; this might indicate a configuration error");
        } catch (NumberFormatException notAnumber) {
            return "Port number '" + value + "' is not a number.";
        }
        return null;
    }

    protected String validateConnectionTimeout(String value) {
        try {
            int connectTimeout = Integer.parseInt(value);
            if (connectTimeout < RequestResponseWebSocketSampler.MIN_CONNECTION_TIMEOUT || connectTimeout > RequestResponseWebSocketSampler.MAX_CONNECTION_TIMEOUT)
                return "Connection timeout '" + connectTimeout + "' is not valid; should between " + RequestResponseWebSocketSampler.MIN_CONNECTION_TIMEOUT + " and " + RequestResponseWebSocketSampler.MAX_CONNECTION_TIMEOUT;
        } catch (NumberFormatException notAnumber) {
            return "Connection timeout '" + value + "' is not a number.";
        }
        return null;
    }

    protected String validateReadTimeout(String value) {
        try {
            int readTimeout = Integer.parseInt(value);
            if (readTimeout < RequestResponseWebSocketSampler.MIN_READ_TIMEOUT || readTimeout > RequestResponseWebSocketSampler.MAX_READ_TIMEOUT)
                return "Read timeout '" + readTimeout + "' is not valid; should between " + RequestResponseWebSocketSampler.MIN_READ_TIMEOUT + " and " + RequestResponseWebSocketSampler.MAX_READ_TIMEOUT;
        }
        catch (NumberFormatException notAnumber) {
            return "Read timeout '" + value + "' is not a number.";
        }

        return null;
    }

    protected void dispose(WebSocketClient webSocketClient) {
        if (webSocketClient != null) {
            getLogger().debug("Sampler  '"+ getName() + "': closing streams for existing websocket connection");
            webSocketClient.dispose();
        }
    }

    private Map convertHeaders(HeaderManager headerManager) {
        Map headers = new HashMap<>();
        if (headerManager != null)
            for (int i = 0; i < headerManager.size(); i++) {
                Header header = headerManager.get(i);
                if (header.getName().trim().length() > 0)
                    headers.put(header.getName(), header.getValue());
                else
                    getLogger().debug("Ignoring header with no name");
            }
        return headers;
    }

    private String getCookieHeaderValue(CookieManager cookieManager, URL url) {
        if (cookieManager != null)
            return cookieManager.getCookieHeaderForURL(url);
        else
            return null;
    }

    static void initProxyConfiguration() {
        proxyHost = System.getProperty("http.proxyHost",null);
        proxyPort = Integer.parseInt(System.getProperty("http.proxyPort","0"));
        List nonProxyHostList = Arrays.asList(System.getProperty("http.nonProxyHosts","").split("\\|"));
        nonProxyHosts = nonProxyHostList.stream().filter(h -> !h.startsWith("*")).collect(Collectors.toList());
        nonProxyWildcards = nonProxyHostList.stream().filter(h -> h.startsWith("*")).map(w -> w.substring(1)).collect(Collectors.toList());
        proxyUsername = JMeterUtils.getPropDefault(JMeter.HTTP_PROXY_USER,null);
        proxyPassword = JMeterUtils.getPropDefault(JMeter.HTTP_PROXY_PASS,null);
    }

    boolean useProxy(String host) {
        // Check for (what JMeter calls) "static" proxy
        if (proxyHost != null && proxyHost.trim().length() > 0) {
            return !nonProxyHosts.contains(host) && nonProxyWildcards.stream().filter(wildcard -> host.endsWith(wildcard)).count() == 0;
        }
        else
            return false;
    }

    static void initThreadStopPolicy() {
        String propertyValue = JMeterUtils.getPropDefault(WS_THREAD_STOP_POLICY_PROPERTY, "none");
        try {
            threadStopPolicy = ThreadStopPolicy.valueOf(propertyValue.trim().toUpperCase());
        }
        catch (IllegalArgumentException e) {
        }
    }

    protected boolean useTLS() {
        return getTLS();
    }

    public String getConnectTimeout() {
        return getPropertyAsString("connectTimeout", "" + WebSocketClient.DEFAULT_CONNECT_TIMEOUT).trim();
    }

    public void setConnectTimeout(String connectTimeout) {
        setProperty("connectTimeout", connectTimeout);
    }

    public String getReadTimeout() {
        return getPropertyAsString("readTimeout", "" + WebSocketClient.DEFAULT_READ_TIMEOUT).trim();
    }

    public void setReadTimeout(String readTimeout) {
        setProperty("readTimeout", readTimeout);
    }

    public boolean getTLS() {
        return getPropertyAsBoolean("TLS");
    }

    public void setTLS(boolean value) {
        setProperty("TLS", value);
    }

    abstract protected Logger getLogger();

    private static void checkForOtherWebsocketPlugins() {
        try {
            WebsocketSampler.class.getClassLoader().loadClass("JMeter.plugins.functional.samplers.websocket.WebSocketSampler");
            LoggingManager.getLoggerForClass().warn("Detected Maciej Zaleski's WebSocket Sampler plugin is installed too, which is not compatible with this plugin (but both can co-exist).");
        } catch (ClassNotFoundException e) {
            // Ok, it's not there.
        } catch (Exception e) {
            // Never let any exception leave this method
            LoggingManager.getLoggerForClass().error("Error while loading class", e);
        }
    }

    private static void checkJMeterVersion() {
        try {
            String jmeterVersion = JMeterUtils.getJMeterVersion();
            Matcher m = Pattern.compile("(\\d+)\\.(\\d+).*").matcher(jmeterVersion);
            if (m.matches()) {
                int major = Integer.parseInt(m.group(1));
                int minor = Integer.parseInt(m.group(2));
                if (major < 3 || (major == 3 && minor < 1))  {
                    String errorMsg = "This version of the WebSocketSamplers plugin requires JMeter 3.1 or later.";
                    if (GuiPackage.getInstance() != null) {
                        SwingUtilities.invokeLater(() -> {
                            GuiPackage.showErrorMessage(errorMsg, "Incompatible versions");
                        });
                    } else {
                        LoggingManager.getLoggerForClass().error(errorMsg);
                    }
                }
            }
        }
        catch (Exception e) {
            // Let this method never throw an exception
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy