├── src
├── main
│ ├── resources
│ │ ├── project.properties
│ │ ├── blazemeter-labs-logo.png
│ │ └── blazemeter-labs-light-logo.png
│ └── java
│ │ └── com
│ │ └── blazemeter
│ │ └── jmeter
│ │ └── http2
│ │ ├── sampler
│ │ ├── HTTP2SampleResult.java
│ │ ├── HTTP2Request.java
│ │ ├── gui
│ │ │ ├── HTTP2SamplerGui.java
│ │ │ └── HTTP2SamplerPanel.java
│ │ ├── HTTP2SamplerConverter.java
│ │ └── HTTP2Sampler.java
│ │ ├── visualizers
│ │ └── ResultCollector.java
│ │ ├── control
│ │ ├── gui
│ │ │ └── HTTP2ControllerGUI.java
│ │ └── HTTP2Controller.java
│ │ ├── Installer.java
│ │ ├── core
│ │ ├── HTTP2FutureResponseListener.java
│ │ ├── JMeterJettySslContextFactory.java
│ │ ├── JettyCacheManager.java
│ │ └── HTTP2JettyClient.java
│ │ └── proxy
│ │ └── HTTP2SampleCreator.java
└── test
│ ├── resources
│ ├── com
│ │ └── blazemeter
│ │ │ └── jmeter
│ │ │ └── http2
│ │ │ └── core
│ │ │ ├── keystore.p12
│ │ │ └── blazemeter-labs-logo.png
│ ├── jmeter
│ │ ├── testJMeter.yaml
│ │ ├── testJMeter.sh
│ │ └── HTTP2SamplerTest.jmx
│ └── log4j2.xml
│ └── java
│ └── com
│ └── blazemeter
│ └── jmeter
│ └── http2
│ ├── sampler
│ ├── JMeterTestUtils.java
│ └── HTTP2SamplerTest.java
│ ├── HTTP2TestBase.java
│ ├── core
│ ├── HTTP2FutureResponseListenerTest.java
│ └── ServerBuilder.java
│ └── control
│ └── HTTP2ControllerTest.java
├── docs
├── addHTTP2Sampler.png
├── http2Sampler-basic.png
├── blazemeter-labs-logo.png
├── http2Sampler-advanced.png
└── http2-async-controller.gif
├── .github
└── workflows
│ └── publish_to_jmeter_plugins.yaml
├── .gitignore
├── checkstyle.xml
├── pom.xml
├── LICENSE
└── README.md
/src/main/resources/project.properties:
--------------------------------------------------------------------------------
1 | version=${project.version}
2 |
--------------------------------------------------------------------------------
/docs/addHTTP2Sampler.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Blazemeter/jmeter-http2-plugin/master/docs/addHTTP2Sampler.png
--------------------------------------------------------------------------------
/docs/http2Sampler-basic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Blazemeter/jmeter-http2-plugin/master/docs/http2Sampler-basic.png
--------------------------------------------------------------------------------
/docs/blazemeter-labs-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Blazemeter/jmeter-http2-plugin/master/docs/blazemeter-labs-logo.png
--------------------------------------------------------------------------------
/docs/http2Sampler-advanced.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Blazemeter/jmeter-http2-plugin/master/docs/http2Sampler-advanced.png
--------------------------------------------------------------------------------
/docs/http2-async-controller.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Blazemeter/jmeter-http2-plugin/master/docs/http2-async-controller.gif
--------------------------------------------------------------------------------
/src/main/resources/blazemeter-labs-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Blazemeter/jmeter-http2-plugin/master/src/main/resources/blazemeter-labs-logo.png
--------------------------------------------------------------------------------
/src/main/resources/blazemeter-labs-light-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Blazemeter/jmeter-http2-plugin/master/src/main/resources/blazemeter-labs-light-logo.png
--------------------------------------------------------------------------------
/src/test/resources/com/blazemeter/jmeter/http2/core/keystore.p12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Blazemeter/jmeter-http2-plugin/master/src/test/resources/com/blazemeter/jmeter/http2/core/keystore.p12
--------------------------------------------------------------------------------
/src/test/resources/com/blazemeter/jmeter/http2/core/blazemeter-labs-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Blazemeter/jmeter-http2-plugin/master/src/test/resources/com/blazemeter/jmeter/http2/core/blazemeter-labs-logo.png
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2SampleResult.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.sampler;
2 |
3 | import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
4 |
5 | @Deprecated
6 | public class HTTP2SampleResult extends HTTPSampleResult {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/visualizers/ResultCollector.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.visualizers;
2 |
3 | //This class exist for backward compatibility purposes
4 | @Deprecated
5 | public class ResultCollector extends org.apache.jmeter.reporters.ResultCollector {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/test/resources/jmeter/testJMeter.yaml:
--------------------------------------------------------------------------------
1 | modules:
2 | jmeter:
3 | path: .jmeter/4.0
4 | version: "4.0"
5 | properties:
6 | log_level: DEBUG
7 | console:
8 | disable: true
9 |
10 | execution:
11 | - concurrency: 2
12 | scenario: http2
13 | iterations : 2
14 |
15 | scenarios:
16 | http2:
17 | script: HTTP2SamplerTest.jmx
18 |
19 | reporting:
20 | - module: passfail
21 | criteria:
22 | - failures>0%
--------------------------------------------------------------------------------
/src/test/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/test/resources/jmeter/testJMeter.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | export JMETER_VERSION=$1
5 | export JMETER_PATH=$2/$JMETER_VERSION
6 | export JMETER_TEST_PATH=${project.basedir}/target/jmeter-test
7 | export APLN_JAR=$(ls $JMETER_TEST_PATH/lib/ | grep alpn-boot)
8 | export JVM_ARGS="-Xbootclasspath/p:$JMETER_TEST_PATH/lib/alpn-boot.jar"
9 | JARS=$(ls $JMETER_TEST_PATH/lib/ | grep -v alpn-boot)
10 |
11 | cd $JMETER_TEST_PATH/lib/
12 | mkdir -p $JMETER_PATH/lib/ext/ && cp -f $JARS $JMETER_PATH/lib/ext/
13 | cd $JMETER_TEST_PATH
14 | bzt -o modules.jmeter.path=$JMETER_PATH -o modules.jmeter.version=$JMETER_VERSION testJMeter.yaml || ERROR=$?
15 | cd $JMETER_PATH/lib/ext/
16 | rm $JARS
17 | exit $ERROR
18 |
19 |
--------------------------------------------------------------------------------
/.github/workflows/publish_to_jmeter_plugins.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy to JMeter Plugins
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | changes:
7 | description: 'Release notes for the update'
8 | required: true
9 |
10 | jobs:
11 | publish:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Run Publish JMeter Plugin Action
15 | id: publish-plugin
16 | uses: abstracta/jmeter-plugin-publish-action@main
17 | with:
18 | forked-repository: https://github.com/Abstracta/jmeter-plugins.git
19 | plugin-artifact-name: jmeter-bzm-http2
20 | plugin-id: bzm-http2
21 | changes: ${{ inputs.changes }}
22 | token: ${{ secrets.GH_TOKEN }}
23 |
24 | - name: Pull Request URL
25 | run: echo ${{ steps.publish-plugin.outputs.pull_request }}
26 |
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/control/gui/HTTP2ControllerGUI.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.control.gui;
2 |
3 | import com.blazemeter.jmeter.http2.control.HTTP2Controller;
4 | import java.awt.BorderLayout;
5 | import org.apache.jmeter.control.gui.AbstractControllerGui;
6 | import org.apache.jmeter.testelement.TestElement;
7 |
8 | public class HTTP2ControllerGUI extends AbstractControllerGui {
9 | private static final long serialVersionUID = 240L;
10 |
11 | public HTTP2ControllerGUI() {
12 | init();
13 | }
14 |
15 | @Override
16 | public String getStaticLabel() {
17 | return "bzm - HTTP2 Async Controller";
18 | }
19 |
20 | @Override
21 | public TestElement createTestElement() {
22 | HTTP2Controller lc = new HTTP2Controller();
23 | configureTestElement(lc);
24 | return lc;
25 | }
26 |
27 | @Override
28 | public void modifyTestElement(TestElement el) {
29 | configureTestElement(el);
30 | }
31 |
32 | @Override
33 | public String getLabelResource() {
34 | return null;
35 | }
36 |
37 | private void init() {
38 | // WARNING: called from ctor so must not be overridden (i.e. must be private or final)
39 | setLayout(new BorderLayout());
40 | setBorder(makeBorder());
41 | add(makeTitlePanel(), BorderLayout.NORTH);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/test/java/com/blazemeter/jmeter/http2/sampler/JMeterTestUtils.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.sampler;
2 |
3 | import kg.apc.emulators.TestJMeterUtils;
4 | import org.apache.jmeter.util.JMeterUtils;
5 |
6 | public class JMeterTestUtils {
7 |
8 | private static boolean jeerEnvironmentInitialized = false;
9 |
10 | public JMeterTestUtils() {
11 | }
12 |
13 | public static void setupJmeterEnv() {
14 | if (!jeerEnvironmentInitialized) {
15 | jeerEnvironmentInitialized = true;
16 | TestJMeterUtils.createJmeterEnv();
17 | JMeterUtils.setProperty("HTTPResponse.parsers", "htmlParser wmlParser cssParser");
18 | JMeterUtils.setProperty("htmlParser.className",
19 | "org.apache.jmeter.protocol.http.parser.LagartoBasedHtmlParser");
20 | JMeterUtils.setProperty("htmlParser.types",
21 | "text/html application/xhtml+xml application/xml text/xml");
22 | JMeterUtils.setProperty("wmlParser.className",
23 | "org.apache.jmeter.protocol.http.parser.RegexpHTMLParser");
24 | JMeterUtils.setProperty("wmlParser.types", "text/vnd.wap.wml");
25 | JMeterUtils
26 | .setProperty("cssParser.className", "org.apache.jmeter.protocol.http.parser.CssParser");
27 | JMeterUtils.setProperty("cssParser.types", "text/css");
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Request.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.sampler;
2 |
3 | import org.apache.jmeter.samplers.AbstractSampler;
4 | import org.apache.jmeter.samplers.Entry;
5 | import org.apache.jmeter.samplers.SampleResult;
6 |
7 | //This class exist for backward compatibility purposes
8 | @Deprecated
9 | public class HTTP2Request extends AbstractSampler {
10 |
11 | public static final String POST_BODY_RAW = "HTTP2Request.postBodyRaw";
12 | public static final String ARGUMENTS = "HTTP2Request.Arguments";
13 | public static final String DOMAIN = "HTTP2Request.domain";
14 | public static final String PORT = "HTTPSampler.port";
15 | public static final String RESPONSE_TIMEOUT = "HTTP2Request.response_timeout";
16 | public static final String PROTOCOL = "HTTP2Request.protocol";
17 | public static final String CONTENT_ENCODING = "HTTP2Request.contentEncoding";
18 | public static final String PATH = "HTTP2Request.path";
19 | public static final String METHOD = "HTTP2Sampler.method";
20 | public static final String FOLLOW_REDIRECTS = "HTTP2Request.follow_redirects";
21 | public static final String AUTO_REDIRECTS = "HTTP2Request.auto_redirects";
22 | public static final String EMBEDDED_RESOURCES = "HTTPSampler.embedded_resources";
23 | public static final String EMBEDDED_URL_REGEX = "HTTPSampler.embedded_url_re";
24 |
25 | @Override
26 | public SampleResult sample(Entry e) {
27 | return null;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/test/java/com/blazemeter/jmeter/http2/sampler/HTTP2SamplerTest.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.sampler;
2 |
3 | import static org.mockito.ArgumentMatchers.any;
4 | import static org.mockito.ArgumentMatchers.anyBoolean;
5 | import static org.mockito.ArgumentMatchers.anyInt;
6 | import static org.mockito.ArgumentMatchers.anyString;
7 | import static org.mockito.Mockito.when;
8 |
9 | import com.blazemeter.jmeter.http2.HTTP2TestBase;
10 | import com.blazemeter.jmeter.http2.core.HTTP2JettyClient;
11 | import java.util.concurrent.TimeoutException;
12 | import org.apache.jmeter.samplers.SampleResult;
13 | import org.assertj.core.api.JUnitSoftAssertions;
14 | import org.junit.Before;
15 | import org.junit.BeforeClass;
16 | import org.junit.Rule;
17 | import org.junit.Test;
18 | import org.junit.runner.RunWith;
19 | import org.mockito.Mock;
20 | import org.mockito.junit.MockitoJUnitRunner;
21 |
22 | @RunWith(MockitoJUnitRunner.class)
23 | public class HTTP2SamplerTest extends HTTP2TestBase {
24 |
25 | @Rule
26 | public final JUnitSoftAssertions softly = new JUnitSoftAssertions();
27 | @Mock
28 | private HTTP2JettyClient client;
29 | private HTTP2Sampler sampler;
30 |
31 | @Before
32 | public void setup() {
33 | sampler = new HTTP2Sampler(() -> client);
34 | }
35 |
36 | @Test
37 | public void shouldReturnErrorMessageWhenThreadIsInterrupted() throws Exception {
38 | when(client.sample(any(), any(), anyBoolean(), anyInt()))
39 | .thenThrow(new InterruptedException());
40 | validateErrorResponse(sampler.sample(), InterruptedException.class.getName());
41 | }
42 |
43 | private void validateErrorResponse(SampleResult result, String code) {
44 | softly.assertThat(result.isSuccessful()).isEqualTo(false);
45 | softly.assertThat(result.getResponseCode()).isEqualTo("Non HTTP response code: " + code);
46 | }
47 |
48 | @Test
49 | public void shouldReturnErrorMessageWhenClientThrowException() throws Exception {
50 | when(client.sample(any(), any(), anyBoolean(), anyInt()))
51 | .thenThrow(new TimeoutException());
52 | validateErrorResponse(sampler.sample(), TimeoutException.class.getName());
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/test/java/com/blazemeter/jmeter/http2/HTTP2TestBase.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2;
2 |
3 | import com.blazemeter.jmeter.http2.sampler.JMeterTestUtils;
4 | import org.junit.BeforeClass;
5 |
6 | /**
7 | * The purpose of this class is to instantiate a JMeter environment for the whole test suite case.
8 | * It's mandatory to extend this class for every new test class created.
9 | *
10 | * Reason:
11 | * HTTP2Sampler class contains a static initializer (static {...}) which runs when the class is
12 | * loaded by the JVM. In other words it executes the static initializer once when the class is
13 | * first referenced or accessed.
14 | * As an example, the class HTTP2ControllerTest uses the HTTP2Sampler class without setting up the
15 | * JMeter environment (because it's not needed) therefore, when the static initializer code runs,
16 | * it's not possible to retrieve some JMeterProperties like ´HTTPResponse.parsers´ (result stored
17 | * in RESPONSE_PARSERS). Since the static initializer runs only once even if we set up the JMeter
18 | * environment for tests who require RESPONSE_PARSERS like HTTP2JettyClientTest it will be too
19 | * late since the static initializer already run.
20 | *
21 | * Issue manifestation:
22 | * This solution was discovered when running the whole suite of tests and some tests will fail.
23 | * Even though, running the failed tests separately or even the whole class which contains the
24 | * failing tests will actually pass. The problem was when running the whole suite of tests.
25 | *
26 | * Other possible solutions:
27 | * In case this solution is not viable in the long term:
28 | * There is another workaround for this problem. Use separated JVMs for each test class.
29 | * By modifying the sure-file plugin configuration to something like:
30 | *
31 | * org.apache.maven.plugins
32 | * maven-surefire-plugin
33 | * 2.22.2
34 | *
35 | * 1
36 | * false
37 | *
38 | *
39 | * Link below:
40 | * See Here
41 | *
42 | */
43 | public class HTTP2TestBase {
44 |
45 | @BeforeClass
46 | public static void once() {
47 | JMeterTestUtils.setupJmeterEnv();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/Installer.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2;
2 |
3 | import java.io.File;
4 | import java.util.Arrays;
5 | import java.util.List;
6 | import java.util.Objects;
7 | import java.util.regex.Matcher;
8 | import java.util.regex.Pattern;
9 | import javax.swing.JOptionPane;
10 |
11 | public class Installer {
12 |
13 | private static final List OLD_DEPENDENCIES_PREFIXES = Arrays.asList(
14 | "jetty-client", "jetty-util", "jetty-http", "jetty-io", "jetty-alpn-client",
15 | "http2-client", "http2-common", "http2-hpack", "jetty-osgi-alpn");
16 | private static final int JAVA_VERSION_REQUIRED = 11;
17 | private static final int NEW_DEPENDENCY_MAJOR_VERSION = 11;
18 |
19 | public static void main(String[] args) {
20 | if (getVersion() < JAVA_VERSION_REQUIRED) {
21 | JOptionPane.showMessageDialog(null,
22 | "The HTTP2 Plugin requires java 11 o higher, please upgrade your java version and "
23 | + "restart JMeter before using the plugin.",
24 | "Java 11 is required", JOptionPane.WARNING_MESSAGE);
25 | return;
26 | }
27 | File pluginsFolder = new File(
28 | Installer.class.getProtectionDomain().getCodeSource().getLocation()
29 | .getFile()).getParentFile();
30 | File dependencyFolder = pluginsFolder.getParentFile();
31 | if (!(pluginsFolder.canRead() && pluginsFolder.canWrite())
32 | || !(dependencyFolder.canWrite() && dependencyFolder.canRead())) {
33 | JOptionPane.showMessageDialog(null, "Read or Write permissions denied",
34 | "Permission Access", JOptionPane.WARNING_MESSAGE);
35 | return;
36 | }
37 |
38 | Arrays.stream(Objects.requireNonNull(pluginsFolder.listFiles()))
39 | .filter(f -> f.getName().matches("jmeter-bzm-http2-1[.\\d]+\\.jar"))
40 | .forEach(File::delete);
41 | Arrays.stream(Objects.requireNonNull(dependencyFolder.listFiles()))
42 | .filter(d -> OLD_DEPENDENCIES_PREFIXES
43 | .stream()
44 | .anyMatch(oldDeps -> {
45 | Pattern pattern = Pattern.compile(oldDeps + "-(\\d+)[.\\d]+(v\\d+)?.jar");
46 | Matcher matcher = pattern.matcher(d.getName());
47 | if (matcher.matches()) {
48 | return Integer.parseInt(matcher.group(1))
49 | < NEW_DEPENDENCY_MAJOR_VERSION;
50 | }
51 | return false;
52 | }))
53 | .forEach(File::delete);
54 | }
55 |
56 | private static int getVersion() {
57 | String versionString = System.getProperty("java.version");
58 | String[] versionElements = versionString.split("\\.");
59 | return versionElements[0].equals("1") ? Integer.parseInt(versionElements[1])
60 | : Integer.parseInt(versionElements[0]);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2FutureResponseListener.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.core;
2 |
3 | import java.util.concurrent.CancellationException;
4 | import java.util.concurrent.CountDownLatch;
5 | import java.util.concurrent.ExecutionException;
6 | import java.util.concurrent.Future;
7 | import java.util.concurrent.TimeUnit;
8 | import java.util.concurrent.TimeoutException;
9 | import org.eclipse.jetty.client.HttpContentResponse;
10 | import org.eclipse.jetty.client.HttpRequest;
11 | import org.eclipse.jetty.client.api.ContentResponse;
12 | import org.eclipse.jetty.client.api.Result;
13 | import org.eclipse.jetty.client.util.BufferingResponseListener;
14 | import org.slf4j.Logger;
15 | import org.slf4j.LoggerFactory;
16 |
17 | public class HTTP2FutureResponseListener extends BufferingResponseListener
18 | implements Future {
19 |
20 | protected static final Logger LOG = LoggerFactory.getLogger(HTTP2FutureResponseListener.class);
21 | private final CountDownLatch latch = new CountDownLatch(1);
22 | private HttpRequest request;
23 | private ContentResponse response;
24 | private Throwable failure;
25 | private volatile boolean cancelled;
26 | private long responseStart;
27 | private long responseEnd;
28 |
29 | public HTTP2FutureResponseListener() {
30 | this(2 * 1024 * 1024);
31 | }
32 |
33 | public HTTP2FutureResponseListener(int maxLength) {
34 | super(maxLength);
35 | setStart();
36 | }
37 |
38 | public void setRequest(HttpRequest request) {
39 | this.request = request;
40 | }
41 |
42 | public HttpRequest getRequest() {
43 | return request;
44 | }
45 |
46 | protected void setStart() {
47 | if (this.responseStart == 0) {
48 | this.responseStart = System.currentTimeMillis();
49 | }
50 | }
51 |
52 | protected void setEnd() {
53 | this.responseEnd = System.currentTimeMillis();
54 | }
55 |
56 | public long getResponseStart() {
57 | return this.responseStart;
58 | }
59 |
60 | public long getResponseEnd() {
61 | return this.responseEnd;
62 | }
63 |
64 | @Override
65 | public void onComplete(Result result) {
66 | setEnd();
67 | failure = result.getFailure();
68 | response = new HttpContentResponse(result.getResponse(), getContent(), getMediaType(),
69 | getEncoding());
70 | latch.countDown();
71 | }
72 |
73 | @Override
74 | public boolean cancel(boolean mayInterruptIfRunning) {
75 | cancelled = true;
76 | return request.abort(new CancellationException());
77 | }
78 |
79 | @Override
80 | public boolean isCancelled() {
81 | return cancelled;
82 | }
83 |
84 | @Override
85 | public boolean isDone() {
86 | return latch.getCount() == 0 || isCancelled();
87 | }
88 |
89 | @Override
90 | public ContentResponse get() throws InterruptedException, ExecutionException {
91 | setStart();
92 | latch.await();
93 | return getResult();
94 | }
95 |
96 | @Override
97 | public ContentResponse get(long timeout, TimeUnit unit)
98 | throws InterruptedException, ExecutionException,
99 | TimeoutException {
100 | setStart();
101 | boolean expired = !latch.await(timeout, unit);
102 | if (expired) {
103 | throw new TimeoutException();
104 | }
105 | return getResult();
106 | }
107 |
108 | private ContentResponse getResult() throws ExecutionException {
109 | if (isCancelled()) {
110 | throw (CancellationException) new CancellationException().initCause(failure);
111 | }
112 | if (failure != null) { // Failure and Response can coexist.
113 | if (response == null) { // Only generate exception response when an response not exist
114 | // Generated by nginx GOAWAY
115 | throw new ExecutionException(failure);
116 | } else {
117 | // It is a failure caused after obtaining the response,
118 | // analyzing what type of failure it is, and incorporating mechanisms to manage it.
119 | // Log as debug, because not is a critical exception.
120 | LOG.debug("Unexpected failure on response", failure);
121 | throw new ExecutionException(failure);
122 | }
123 | }
124 |
125 | return response;
126 | }
127 |
128 | }
129 |
--------------------------------------------------------------------------------
/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2FutureResponseListenerTest.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.core;
2 |
3 | import com.blazemeter.jmeter.http2.HTTP2TestBase;
4 | import org.eclipse.jetty.client.HttpRequest;
5 | import org.eclipse.jetty.client.HttpResponseException;
6 | import org.eclipse.jetty.client.api.ContentResponse;
7 | import org.eclipse.jetty.client.api.Result;
8 | import org.junit.Assert;
9 | import org.junit.Before;
10 | import org.junit.Test;
11 | import org.mockito.Mockito;
12 |
13 | import java.util.concurrent.CancellationException;
14 | import java.util.concurrent.ExecutionException;
15 | import java.util.concurrent.TimeUnit;
16 | import java.util.concurrent.TimeoutException;
17 |
18 | import static org.junit.Assert.*;
19 | import static org.mockito.Mockito.*;
20 |
21 | public class HTTP2FutureResponseListenerTest extends HTTP2TestBase {
22 | private HTTP2FutureResponseListener listener;
23 | private HttpRequest mockRequest;
24 |
25 | @Before
26 | public void setUp() {
27 | listener = new HTTP2FutureResponseListener();
28 | mockRequest = mock(HttpRequest.class);
29 | listener.setRequest(mockRequest);
30 | }
31 |
32 | @Test
33 | public void getRequestReturnsSetRequest() {
34 | assertEquals(mockRequest, listener.getRequest());
35 | }
36 |
37 | @Test
38 | public void getResponseStartReturnsResponseStart() {
39 | listener.setStart();
40 | long responseStart = listener.getResponseStart();
41 | assertTrue(responseStart > 0);
42 | }
43 |
44 | @Test
45 | public void getResponseEndReturnsResponseEnd() {
46 | listener.setEnd();
47 | long responseEnd = listener.getResponseEnd();
48 | assertTrue(responseEnd > 0);
49 | }
50 |
51 | @Test
52 | public void onCompleteSuccessfulResultCreatesContentResponse() throws ExecutionException, InterruptedException {
53 | Result mockResult = mock(Result.class);
54 | ContentResponse mockContentResponse = mock(ContentResponse.class);
55 | when(mockContentResponse.getContent()).thenReturn("".getBytes());
56 | when(mockResult.getResponse()).thenReturn(mockContentResponse);
57 |
58 | listener.onComplete(mockResult);
59 |
60 | ContentResponse response = listener.get();
61 | Assert.assertArrayEquals(mockContentResponse.getContent(),response.getContent());
62 | }
63 |
64 | @Test
65 | public void cancelAbortsRequest() {
66 | when(mockRequest.abort(Mockito.any(CancellationException.class))).thenReturn(true);
67 |
68 | boolean cancelled = listener.cancel(true);
69 |
70 | assertTrue(cancelled);
71 | verify(mockRequest).abort(Mockito.any(CancellationException.class));
72 | }
73 |
74 | @Test
75 | public void isCancelledReturnsCancelledFlag() {
76 | listener.cancel(true);
77 |
78 | assertTrue(listener.isCancelled());
79 | }
80 |
81 | @Test
82 | public void isDoneReturnsTrueWhenCountDownIsZero() {
83 | listener.onComplete(mock(Result.class));
84 |
85 | assertTrue(listener.isDone());
86 | }
87 |
88 | @Test
89 | public void isDoneReturnsTrueWhenCancelled() {
90 | listener.cancel(true);
91 |
92 | assertTrue(listener.isDone());
93 | }
94 |
95 | @Test
96 | public void getWithTimeoutWaitsForCompletion() throws ExecutionException, InterruptedException, TimeoutException {
97 | listener.onComplete(mock(Result.class));
98 |
99 | ContentResponse response = listener.get();
100 | long timeout = 1000; // Timeout in milliseconds
101 | TimeUnit unit = TimeUnit.MILLISECONDS;
102 |
103 | ContentResponse result = listener.get(timeout, unit);
104 |
105 | assertNotNull(result);
106 | }
107 |
108 | @Test(expected = TimeoutException.class)
109 | public void getWithTimeoutExceedsTimeoutThrowsTimeoutException()
110 | throws InterruptedException, ExecutionException, TimeoutException {
111 | // Don't call onComplete() to simulate a timeout
112 |
113 | long timeout = 1000; // Timeout in milliseconds
114 | TimeUnit unit = TimeUnit.MILLISECONDS;
115 |
116 | listener.get(timeout, unit); // Should throw TimeoutException
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/proxy/HTTP2SampleCreator.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.proxy;
2 |
3 | import static org.apache.jmeter.util.JMeterUtils.getPropDefault;
4 |
5 | import com.blazemeter.jmeter.http2.sampler.HTTP2Sampler;
6 | import com.blazemeter.jmeter.http2.sampler.gui.HTTP2SamplerGui;
7 | import java.util.ArrayList;
8 | import java.util.Arrays;
9 | import java.util.Map;
10 | import org.apache.commons.lang3.ArrayUtils;
11 | import org.apache.jmeter.protocol.http.control.Header;
12 | import org.apache.jmeter.protocol.http.control.HeaderManager;
13 | import org.apache.jmeter.protocol.http.proxy.AbstractSamplerCreator;
14 | import org.apache.jmeter.protocol.http.proxy.DefaultSamplerCreator;
15 | import org.apache.jmeter.protocol.http.proxy.HttpRequestHdr;
16 | import org.apache.jmeter.protocol.http.proxy.SamplerCreator;
17 | import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
18 | import org.apache.jmeter.testelement.TestElement;
19 | import org.slf4j.Logger;
20 | import org.slf4j.LoggerFactory;
21 |
22 | public class HTTP2SampleCreator extends AbstractSamplerCreator {
23 |
24 | private static final String PROXY_ENABLED = "HTTP2Sampler.proxy_enabled";
25 | private static final Logger LOG = LoggerFactory.getLogger(HTTP2SampleCreator.class);
26 |
27 | private static final SamplerCreator DEFAULT_SAMPLER_CREATOR = new DefaultSamplerCreator();
28 |
29 | private final boolean proxyEnabled = getPropDefault(PROXY_ENABLED, false);
30 |
31 | @Override
32 | public String[] getManagedContentTypes() {
33 | if (!proxyEnabled) {
34 | LOG.info("HTTP2 Sample disable by default for proxy recording");
35 | LOG.info("Enable HTTP2 for proxy recording with property {}=true", PROXY_ENABLED);
36 | return ArrayUtils.EMPTY_STRING_ARRAY;
37 | }
38 | LOG.debug("getManagedContentTypes()");
39 | String[] contentTypes = new String[] {
40 | "text/plain", "text/html", "text/xml", "application/xhtml+xml", "application/octet-stream",
41 | "application/x-www-form-urlencoded",
42 | "text/css", "text/javascript",
43 | "text/csv", "application/json", "application/ld+json",
44 | "application/xml", "application/atom+xml",
45 | "application/gzip", "application/zip", "application/x-7z-compressed", "application/x-tar",
46 | "image/gif", "image/bmp", "image/jpeg", "image/pn", "image/avif", "audio/aac",
47 | "image/svg+xml",
48 | "font/ttf", "font/woff", "font/woff2", "font/otf",
49 | null};
50 | String[] charsets = new String[] {"UTF-8"};
51 | // Create a list with all default content types and the combinations with different charsets
52 | ArrayList contentTypesList = new ArrayList<>();
53 | contentTypesList.addAll(Arrays.asList(contentTypes));
54 | for (String contentType : contentTypes) {
55 | for (String charset : charsets) {
56 | // With space
57 | contentTypesList.add(contentType + "; charset=" + charset);
58 | // Without space
59 | contentTypesList.add(contentType + ";charset=" + charset);
60 | }
61 | }
62 | LOG.debug(contentTypesList.toArray().toString());
63 | return contentTypesList.toArray(new String[0]);
64 | }
65 |
66 | @Override
67 | public HTTPSamplerBase createSampler(HttpRequestHdr httpRequestHdr, Map map,
68 | Map map1) {
69 | LOG.debug("createSampler()");
70 |
71 | LOG.debug(httpRequestHdr.getUrl());
72 |
73 | HTTP2Sampler sampler = new HTTP2Sampler();
74 |
75 | sampler.setProperty(TestElement.GUI_CLASS, HTTP2SamplerGui.class.getName());
76 |
77 | // Defaults
78 | sampler.setHttp1UpgradeEnabled(false);
79 | sampler.setFollowRedirects(false);
80 | sampler.setUseKeepAlive(true);
81 |
82 | return sampler;
83 | }
84 |
85 | @Override
86 | public void populateSampler(HTTPSamplerBase httpSamplerBase, HttpRequestHdr httpRequestHdr,
87 | Map map, Map map1) throws Exception {
88 | LOG.debug("populateSampler()");
89 | // Force the default sampler gui to HTTP2
90 | LOG.debug(httpRequestHdr.getUrl());
91 | LOG.debug(httpRequestHdr.getRawPostData().toString());
92 |
93 | if (httpRequestHdr.getHeaderManager() != null) {
94 | HeaderManager hm = httpRequestHdr.getHeaderManager();
95 |
96 | Header ae = hm.getFirstHeaderNamed("Accept-Encoding");
97 | if (ae != null) {
98 | String acceptEncoding = ae.getValue();
99 | acceptEncoding = acceptEncoding.replace(", br", "").replace("br", "");
100 | hm.getHeaders().remove("Accept-Encoding");
101 | Header h = new Header();
102 | h.setName("Accept-Encoding");
103 | h.setValue(acceptEncoding);
104 | hm.add(h);
105 | }
106 | }
107 |
108 | DEFAULT_SAMPLER_CREATOR.populateSampler(httpSamplerBase, httpRequestHdr, map, map1);
109 |
110 | }
111 |
112 | }
113 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/java,maven,eclipse,intellij+all,intellij+iml,macos
3 |
4 | ### Eclipse ###
5 |
6 | .metadata
7 | bin/
8 | tmp/
9 | *.tmp
10 | *.bak
11 | *.swp
12 | *~.nib
13 | local.properties
14 | .settings/
15 | .loadpath
16 | .recommenders
17 |
18 | # External tool builders
19 | .externalToolBuilders/
20 |
21 | # Locally stored "Eclipse launch configurations"
22 | *.launch
23 |
24 | # PyDev specific (Python IDE for Eclipse)
25 | *.pydevproject
26 |
27 | # CDT-specific (C/C++ Development Tooling)
28 | .cproject
29 |
30 | # Java annotation processor (APT)
31 | .factorypath
32 |
33 | # PDT-specific (PHP Development Tools)
34 | .buildpath
35 |
36 | # sbteclipse plugin
37 | .target
38 |
39 | # Tern plugin
40 | .tern-project
41 |
42 | # TeXlipse plugin
43 | .texlipse
44 |
45 | # STS (Spring Tool Suite)
46 | .springBeans
47 |
48 | # Code Recommenders
49 | .recommenders/
50 |
51 | # Scala IDE specific (Scala & Java development for Eclipse)
52 | .cache-main
53 | .scala_dependencies
54 | .worksheet
55 |
56 | ### Eclipse Patch ###
57 | # Eclipse Core
58 | .project
59 |
60 | # JDT-specific (Eclipse Java Development Tools)
61 | .classpath
62 |
63 | ### Intellij+all ###
64 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
65 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
66 |
67 | # User-specific stuff:
68 | .idea/**/workspace.xml
69 | .idea/**/tasks.xml
70 | .idea/dictionaries
71 |
72 | # Sensitive or high-churn files:
73 | .idea/**/dataSources/
74 | .idea/**/dataSources.ids
75 | .idea/**/dataSources.xml
76 | .idea/**/dataSources.local.xml
77 | .idea/**/sqlDataSources.xml
78 | .idea/**/dynamic.xml
79 | .idea/**/uiDesigner.xml
80 |
81 | # Gradle:
82 | .idea/**/gradle.xml
83 | .idea/**/libraries
84 |
85 | # CMake
86 | cmake-build-debug/
87 |
88 | # Mongo Explorer plugin:
89 | .idea/**/mongoSettings.xml
90 |
91 | ## File-based project format:
92 | *.iws
93 |
94 | ## Plugin-specific files:
95 |
96 | # IntelliJ
97 | /out/
98 |
99 | # mpeltonen/sbt-idea plugin
100 | .idea_modules/
101 |
102 | # JIRA plugin
103 | atlassian-ide-plugin.xml
104 |
105 | # Cursive Clojure plugin
106 | .idea/replstate.xml
107 |
108 | # Ruby plugin and RubyMine
109 | /.rakeTasks
110 |
111 | # Crashlytics plugin (for Android Studio and IntelliJ)
112 | com_crashlytics_export_strings.xml
113 | crashlytics.properties
114 | crashlytics-build.properties
115 | fabric.properties
116 |
117 | ### Intellij+all Patch ###
118 | # Ignores the whole idea folder
119 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
120 |
121 | .idea/
122 |
123 | ### Intellij+iml ###
124 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
125 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
126 |
127 | # User-specific stuff:
128 |
129 | # Sensitive or high-churn files:
130 |
131 | # Gradle:
132 |
133 | # CMake
134 |
135 | # Mongo Explorer plugin:
136 |
137 | ## File-based project format:
138 |
139 | ## Plugin-specific files:
140 |
141 | # IntelliJ
142 |
143 | # mpeltonen/sbt-idea plugin
144 |
145 | # JIRA plugin
146 |
147 | # Cursive Clojure plugin
148 |
149 | # Ruby plugin and RubyMine
150 |
151 | # Crashlytics plugin (for Android Studio and IntelliJ)
152 |
153 | ### Intellij+iml Patch ###
154 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
155 |
156 | *.iml
157 | modules.xml
158 | .idea/misc.xml
159 | *.ipr
160 |
161 | ### Java ###
162 | # Compiled class file
163 | *.class
164 |
165 | # Log file
166 | *.log
167 |
168 | # BlueJ files
169 | *.ctxt
170 |
171 | # Mobile Tools for Java (J2ME)
172 | .mtj.tmp/
173 |
174 | # Package Files #
175 | *.jar
176 | *.war
177 | *.ear
178 | *.zip
179 | *.tar.gz
180 | *.rar
181 |
182 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
183 | hs_err_pid*
184 |
185 | ### macOS ###
186 | *.DS_Store
187 | .AppleDouble
188 | .LSOverride
189 |
190 | # Icon must end with two \r
191 | Icon
192 |
193 | # Thumbnails
194 | ._*
195 |
196 | # Files that might appear in the root of a volume
197 | .DocumentRevisions-V100
198 | .fseventsd
199 | .Spotlight-V100
200 | .TemporaryItems
201 | .Trashes
202 | .VolumeIcon.icns
203 | .com.apple.timemachine.donotpresent
204 |
205 | # Directories potentially created on remote AFP share
206 | .AppleDB
207 | .AppleDesktop
208 | Network Trash Folder
209 | Temporary Items
210 | .apdisk
211 |
212 | ### Maven ###
213 | target/
214 | pom.xml.tag
215 | pom.xml.releaseBackup
216 | pom.xml.versionsBackup
217 | pom.xml.next
218 | release.properties
219 | dependency-reduced-pom.xml
220 | buildNumber.properties
221 | .mvn/timing.properties
222 |
223 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
224 | !/.mvn/wrapper/maven-wrapper.jar
225 |
226 |
227 | # End of https://www.gitignore.io/api/java,maven,eclipse,intellij+all,intellij+iml,macos
228 |
229 | # Directories used in gitlab environment
230 | .m2
231 | .jmeter
232 |
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/control/HTTP2Controller.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.control;
2 |
3 | import com.blazemeter.jmeter.http2.core.HTTP2FutureResponseListener;
4 | import com.blazemeter.jmeter.http2.sampler.HTTP2Sampler;
5 | import java.io.Serializable;
6 | import java.util.ArrayList;
7 | import java.util.List;
8 | import java.util.Objects;
9 | import org.apache.jmeter.control.GenericController;
10 | import org.apache.jmeter.control.NextIsNullException;
11 | import org.apache.jmeter.testelement.TestElement;
12 | import org.apache.jmeter.util.JMeterUtils;
13 | import org.slf4j.Logger;
14 | import org.slf4j.LoggerFactory;
15 |
16 | public class HTTP2Controller extends GenericController implements Serializable {
17 |
18 | private static final Logger LOG = LoggerFactory.getLogger(HTTP2Controller.class);
19 |
20 | private int maxConcurrentAsyncInController = 1000;
21 |
22 | private transient List http2SamplesSync = new ArrayList<>();
23 | private transient List subControllersAndSamplersBackup = new ArrayList();
24 |
25 | public HTTP2Controller() {
26 | super();
27 | maxConcurrentAsyncInController = Integer
28 | .parseInt(JMeterUtils.getPropDefault("httpJettyClient.maxConcurrentAsyncInController",
29 | String.valueOf(maxConcurrentAsyncInController)));
30 | }
31 |
32 | private HTTP2Sampler waitForDoneHTTP2() {
33 | boolean interrupted = false;
34 | // Try to check if the first request finish to return again that element first
35 | if (http2SamplesSync.size() > 0) {
36 | HTTP2Sampler http2Sam = http2SamplesSync.get(0);
37 | HTTP2FutureResponseListener http2FListener =
38 | http2Sam.geFutureResponseListener();
39 | while (!interrupted && (http2FListener != null)) {
40 | if (http2FListener.isDone() || http2FListener.isCancelled()) {
41 | String urlProcesed = http2FListener.getRequest().getURI().toString();
42 | LOG.debug("HTTP2 Future Finished, retrying the sample with that data {}", urlProcesed);
43 | http2SamplesSync.remove(0); // Remove the sample
44 | return http2Sam; // The second attempt take the data from the finished listener
45 | }
46 | try {
47 | Thread.sleep(10);
48 | } catch (InterruptedException e) {
49 | http2SamplesSync.clear();
50 | interrupted = true;
51 | }
52 | }
53 | }
54 | if (interrupted) {
55 | Thread.currentThread().interrupt();
56 | }
57 | return null;
58 | }
59 |
60 | @Override
61 | protected void setDone(boolean done) {
62 | // NOPE, dont allow to set the Done on there
63 | LOG.debug("Set Done:" + done);
64 | super.setDone(done);
65 | }
66 |
67 | @Override
68 | public boolean isDone() {
69 | boolean done = super.isDone();
70 | LOG.debug("isDone? " + done);
71 | return done;
72 | }
73 |
74 | @Override
75 | protected TestElement getCurrentElement() throws NextIsNullException {
76 | LOG.debug("Current {} Size {}", current, subControllersAndSamplers.size());
77 |
78 | if (current == 0) {
79 | // The first time, backup the original sub elements
80 | // On iteration, we need to recover the original list
81 | if (subControllersAndSamplersBackup.size() == 0) {
82 | subControllersAndSamplersBackup.addAll(subControllersAndSamplers);
83 | } else {
84 | subControllersAndSamplers.clear();
85 | subControllersAndSamplers.addAll(subControllersAndSamplersBackup);
86 | }
87 | }
88 |
89 | if (http2SamplesSync.size() > maxConcurrentAsyncInController) {
90 | HTTP2Sampler http2samDone = waitForDoneHTTP2();
91 | if (!Objects.isNull(http2samDone)) {
92 | subControllersAndSamplers.add(current, http2samDone);
93 | return http2samDone;
94 | }
95 | }
96 |
97 | if (current < subControllersAndSamplers.size()) {
98 | TestElement sam = subControllersAndSamplers.get(current);
99 | if (sam instanceof HTTP2Sampler) {
100 | HTTP2Sampler http2Sam = ((HTTP2Sampler) sam);
101 | http2Sam.setSyncRequest(false); // Force to run async the first time
102 | LOG.debug("Convert http2 sample to Async and add to wait list");
103 | http2SamplesSync.add(http2Sam);
104 | return http2Sam;
105 | } else { // Another type of element, use that for checkpoint mark
106 | HTTP2Sampler http2sam = waitForDoneHTTP2();
107 | if (Objects.isNull(http2sam)) {
108 | return sam;
109 | }
110 | subControllersAndSamplers.add(current, http2sam);
111 | return http2sam;
112 | }
113 | }
114 | if (current == (subControllersAndSamplers.size())) {
115 | // On the last, force a checkpoint moment
116 | LOG.debug("The last, force checkpoint");
117 | HTTP2Sampler http2samDone = waitForDoneHTTP2();
118 | if (!Objects.isNull(http2samDone)) {
119 | subControllersAndSamplers.add(current, http2samDone);
120 | return http2samDone;
121 | }
122 | if (http2SamplesSync.isEmpty()) {
123 | LOG.debug("No more elements!");
124 | }
125 | }
126 | return null;
127 | }
128 |
129 | }
130 |
131 |
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/core/JMeterJettySslContextFactory.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.core;
2 |
3 | import java.lang.reflect.InvocationTargetException;
4 | import java.lang.reflect.Method;
5 | import java.net.Socket;
6 | import java.security.KeyStore;
7 | import java.security.Principal;
8 | import java.security.PrivateKey;
9 | import java.security.cert.X509Certificate;
10 | import javax.net.ssl.KeyManager;
11 | import javax.net.ssl.SSLEngine;
12 | import javax.net.ssl.X509ExtendedKeyManager;
13 | import javax.net.ssl.X509KeyManager;
14 | import org.apache.jmeter.util.JsseSSLManager;
15 | import org.apache.jmeter.util.SSLManager;
16 | import org.apache.jmeter.util.keystore.JmeterKeyStore;
17 | import org.eclipse.jetty.util.ssl.SslContextFactory;
18 |
19 | public class JMeterJettySslContextFactory extends SslContextFactory.Client {
20 |
21 | private final JmeterKeyStore keys;
22 | private final KeyStore tustStoreKeys;
23 |
24 | public JMeterJettySslContextFactory() {
25 | setTrustAll(true);
26 | String keyStorePath = System.getProperty("javax.net.ssl.keyStore");
27 | if (keyStorePath != null && !keyStorePath.isEmpty()) {
28 | setKeyStorePath("file://" + keyStorePath);
29 | keys = getKeyStore((JsseSSLManager) SSLManager.getInstance());
30 | /*
31 | we need to set password after getting keystore since getKeystore may ask the user for the
32 | password.
33 | */
34 | setKeyStorePassword(System.getProperty("javax.net.ssl.keyStorePassword"));
35 | } else {
36 | keys = null;
37 | }
38 |
39 | String truststore = System.getProperty("javax.net.ssl.trustStore");
40 | if (truststore != null && !truststore.isEmpty()) {
41 | setTrustStorePath("file://" + truststore);
42 | tustStoreKeys = getTrustStore((JsseSSLManager) SSLManager.getInstance());
43 | /*
44 | we need to set password after getting truststore since getTrustStore may ask the user for the
45 | password.
46 | */
47 | setTrustStorePassword(System.getProperty("javax.net.ssl.trustStorePassword"));
48 | } else {
49 | tustStoreKeys = null;
50 | }
51 | }
52 |
53 | private JmeterKeyStore getKeyStore(JsseSSLManager sslManager) {
54 | try {
55 | Method keystoreMethod = SSLManager.class.getDeclaredMethod("getKeyStore");
56 | keystoreMethod.setAccessible(true);
57 | return (JmeterKeyStore) keystoreMethod.invoke(sslManager);
58 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
59 | throw new RuntimeException(e);
60 | }
61 | }
62 |
63 | private KeyStore getTrustStore(JsseSSLManager sslManager) {
64 | try {
65 | Method trustStoreMethod = SSLManager.class.getDeclaredMethod("getTrustStore");
66 | trustStoreMethod.setAccessible(true);
67 | return (KeyStore) trustStoreMethod.invoke(sslManager);
68 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
69 | throw new RuntimeException(e);
70 | }
71 | }
72 |
73 | // Overwritten to avoid warning logging
74 | @Override
75 | protected void checkTrustAll() {
76 | }
77 |
78 | // Overwritten to avoid warning logging
79 | @Override
80 | protected void checkEndPointIdentificationAlgorithm() {
81 | }
82 |
83 | // Overwritten to provide jmeter SSLManager configured keyManagers
84 | @Override
85 | protected KeyManager[] getKeyManagers(KeyStore keyStore) throws Exception {
86 | // based in logic extracted from JsseSSLManager.createContext
87 | KeyManager[] ret = super.getKeyManagers(keyStore);
88 | if (keys == null) {
89 | return ret;
90 | }
91 | for (int i = 0; i < ret.length; i++) {
92 | if (ret[i] instanceof X509KeyManager) {
93 | ret[i] = new WrappedX509KeyManager((X509KeyManager) ret[i], keys);
94 | }
95 | }
96 | return ret;
97 | }
98 |
99 | // based in logic extracted from JsseSSLManager.WrappedX509KeyManager
100 | private static class WrappedX509KeyManager extends X509ExtendedKeyManager {
101 |
102 | private final X509KeyManager manager;
103 | private final JmeterKeyStore store;
104 |
105 | private WrappedX509KeyManager(X509KeyManager parent, JmeterKeyStore ks) {
106 | this.manager = parent;
107 | this.store = ks;
108 | }
109 |
110 | @Override
111 | public String[] getClientAliases(String keyType, Principal[] issuers) {
112 | return store.getClientAliases(keyType, issuers);
113 | }
114 |
115 | @Override
116 | public String[] getServerAliases(String keyType, Principal[] issuers) {
117 | return manager.getServerAliases(keyType, issuers);
118 | }
119 |
120 | @Override
121 | public X509Certificate[] getCertificateChain(String alias) {
122 | return store.getCertificateChain(alias);
123 | }
124 |
125 | @Override
126 | public PrivateKey getPrivateKey(String alias) {
127 | return store.getPrivateKey(alias);
128 | }
129 |
130 | @Override
131 | public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
132 | return store.getAlias();
133 | }
134 |
135 | public String chooseEngineClientAlias(String[] keyType, Principal[] issuers,
136 | SSLEngine engine) {
137 | return store.getAlias();
138 | }
139 |
140 | @Override
141 | public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
142 | return this.manager.chooseServerAlias(keyType, issuers, socket);
143 | }
144 |
145 | public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
146 | return manager instanceof X509ExtendedKeyManager
147 | ? ((X509ExtendedKeyManager) manager).chooseEngineServerAlias(keyType, issuers, engine)
148 | : null;
149 | }
150 |
151 | }
152 |
153 | }
154 |
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/sampler/gui/HTTP2SamplerGui.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.sampler.gui;
2 |
3 | import com.blazemeter.jmeter.commons.BlazemeterLabsLogo;
4 | import com.blazemeter.jmeter.http2.sampler.HTTP2Sampler;
5 | import com.blazemeter.jmeter.http2.sampler.HTTP2SamplerConverter;
6 | import com.thoughtworks.xstream.XStream;
7 | import java.awt.BorderLayout;
8 | import java.io.IOException;
9 | import java.lang.reflect.Field;
10 | import java.util.Properties;
11 | import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
12 | import org.apache.jmeter.samplers.gui.AbstractSamplerGui;
13 | import org.apache.jmeter.save.SaveService;
14 | import org.apache.jmeter.testelement.TestElement;
15 | import org.slf4j.Logger;
16 | import org.slf4j.LoggerFactory;
17 |
18 | public class HTTP2SamplerGui extends AbstractSamplerGui {
19 |
20 | private static final Logger LOG = LoggerFactory.getLogger(HTTP2SamplerGui.class);
21 | private static final String PLUGIN_REPOSITORY_URL = "https://github.com/Blazemeter/jmeter-http2"
22 | + "-plugin";
23 | private final HTTP2SamplerPanel http2SamplerPanel;
24 |
25 | static {
26 | try {
27 | Field field = SaveService.class.getDeclaredField("JMXSAVER");
28 | field.setAccessible(true);
29 | XStream jmxSaver = (XStream) field.get(null);
30 | jmxSaver.registerConverter(new HTTP2SamplerConverter(jmxSaver.getMapper()),
31 | XStream.PRIORITY_VERY_HIGH);
32 | } catch (IllegalAccessException | NoSuchFieldException e) {
33 | LOG.error("Error while creating HTTP2 jmx converter", e);
34 | }
35 | }
36 |
37 | public HTTP2SamplerGui() {
38 | http2SamplerPanel = new HTTP2SamplerPanel(true);
39 | http2SamplerPanel.resetFields();
40 |
41 | setLayout(new BorderLayout(0, 5));
42 | setBorder(makeBorder());
43 |
44 | add(makeTitlePanel(), BorderLayout.NORTH);
45 | add(http2SamplerPanel, BorderLayout.CENTER);
46 | add(new BlazemeterLabsLogo(PLUGIN_REPOSITORY_URL), BorderLayout.PAGE_END);
47 | }
48 |
49 | @Override
50 | public String getStaticLabel() {
51 | return "bzm - HTTP2 Sampler";
52 | }
53 |
54 | @Override
55 | public String getLabelResource() {
56 | return null;
57 | }
58 |
59 | @Override
60 | public TestElement createTestElement() {
61 | HTTP2Sampler http2Sampler = new HTTP2Sampler();
62 | configureTestElement(http2Sampler);
63 | http2Sampler.setConcurrentDwn(true);
64 | return http2Sampler;
65 | }
66 |
67 | @Override
68 | public void modifyTestElement(TestElement testElement) {
69 | testElement.clear();
70 | configureTestElement(testElement);
71 | if (testElement instanceof HTTP2Sampler) {
72 | HTTP2Sampler http2Sampler = (HTTP2Sampler) testElement;
73 | http2Sampler.setImageParser(http2SamplerPanel.getRetrieveEmbeddedResources());
74 | http2Sampler.setConcurrentDwn(http2SamplerPanel.getConcurrentDownload());
75 | http2Sampler.setConcurrentPool(http2SamplerPanel.getConcurrentPool());
76 | http2Sampler.setEmbeddedUrlRE(http2SamplerPanel.getEmbeddedResourcesRegex());
77 | http2Sampler.setConnectTimeout(http2SamplerPanel.getConnectTimeOut());
78 | http2Sampler.setResponseTimeout(http2SamplerPanel.getResponseTimeOut());
79 | http2Sampler.setProxyHost(http2SamplerPanel.getProxyHost());
80 | http2Sampler.setProxyScheme(http2SamplerPanel.getProxyScheme());
81 | http2Sampler.setProxyPortInt(http2SamplerPanel.getProxyPort());
82 | http2Sampler.setProxyUser(http2SamplerPanel.getProxyUser());
83 | http2Sampler.setProxyPass(http2SamplerPanel.getProxyPass());
84 | http2Sampler.setProperty("version", getPluginVersion());
85 | http2SamplerPanel.getUrlConfigGui().modifyTestElement(http2Sampler);
86 | http2Sampler.setHttp1UpgradeEnabled(http2SamplerPanel.isHttp1UpgradeSelected());
87 | }
88 | }
89 |
90 | private String getPluginVersion() {
91 | try {
92 | final Properties properties = new Properties();
93 | properties.load(this.getClass().getClassLoader().getResourceAsStream("project.properties"));
94 | return properties.getProperty("version");
95 | } catch (IOException e) {
96 | LOG.warn("Could not write plugin version", e);
97 | return "";
98 | }
99 | }
100 |
101 | @Override
102 | public void configure(TestElement testElement) {
103 | super.configure(testElement);
104 | if (testElement instanceof HTTP2Sampler) {
105 | HTTP2Sampler http2Sampler = (HTTP2Sampler) testElement;
106 | http2SamplerPanel.setRetrieveEmbeddedResources(http2Sampler.isImageParser());
107 | http2SamplerPanel.setConcurrentDownload(http2Sampler.isConcurrentDwn());
108 | http2SamplerPanel.setConcurrentPool(http2Sampler.getConcurrentPool());
109 | http2SamplerPanel.setEmbeddedResourcesRegex(http2Sampler.getEmbeddedUrlRE());
110 | http2SamplerPanel
111 | .setConnectTimeOut(http2Sampler.getPropertyAsString(HTTPSamplerBase.CONNECT_TIMEOUT));
112 | http2SamplerPanel
113 | .setResponseTimeOut(http2Sampler.getPropertyAsString(HTTPSamplerBase.RESPONSE_TIMEOUT));
114 | http2SamplerPanel
115 | .setProxyScheme(http2Sampler.getPropertyAsString(HTTPSamplerBase.PROXYSCHEME));
116 | http2SamplerPanel.setProxyHost(http2Sampler.getPropertyAsString(HTTPSamplerBase.PROXYHOST));
117 | http2SamplerPanel.setProxyPort(http2Sampler.getPropertyAsString(HTTPSamplerBase.PROXYPORT));
118 | http2SamplerPanel.setProxyUser(http2Sampler.getPropertyAsString(HTTPSamplerBase.PROXYUSER));
119 | http2SamplerPanel.setProxyPass(http2Sampler.getPropertyAsString(HTTPSamplerBase.PROXYPASS));
120 | http2SamplerPanel.getUrlConfigGui().configure(http2Sampler);
121 | http2SamplerPanel.setHttp1UpgradeSelected(http2Sampler.isHttp1UpgradeEnabled());
122 | }
123 | }
124 |
125 | @Override
126 | public void clearGui() {
127 | super.clearGui();
128 | http2SamplerPanel.resetFields();
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/checkstyle.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/core/JettyCacheManager.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.core;
2 |
3 | import java.net.URI;
4 | import java.net.URISyntaxException;
5 | import java.net.URL;
6 | import org.apache.http.Header;
7 | import org.apache.http.HttpResponse;
8 | import org.apache.http.ProtocolVersion;
9 | import org.apache.http.client.methods.HttpGet;
10 | import org.apache.http.client.methods.HttpHead;
11 | import org.apache.http.client.methods.HttpOptions;
12 | import org.apache.http.client.methods.HttpPatch;
13 | import org.apache.http.client.methods.HttpPost;
14 | import org.apache.http.client.methods.HttpPut;
15 | import org.apache.http.client.methods.HttpRequestBase;
16 | import org.apache.http.client.methods.HttpTrace;
17 | import org.apache.http.message.BasicHeader;
18 | import org.apache.http.message.BasicHttpResponse;
19 | import org.apache.jmeter.protocol.http.control.CacheManager;
20 | import org.apache.jmeter.protocol.http.sampler.HTTPHC4Impl.HttpDelete;
21 | import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
22 | import org.apache.jmeter.protocol.http.sampler.HttpWebdav;
23 | import org.apache.jmeter.protocol.http.util.HTTPConstants;
24 | import org.apache.jmeter.util.JMeterUtils;
25 | import org.eclipse.jetty.client.HttpRequest;
26 | import org.eclipse.jetty.client.api.ContentResponse;
27 | import org.eclipse.jetty.http.HttpField;
28 | import org.eclipse.jetty.http.HttpFields;
29 | import org.eclipse.jetty.http.HttpVersion;
30 |
31 | public class JettyCacheManager {
32 |
33 | private final CacheManager cacheManager;
34 |
35 | private JettyCacheManager(CacheManager cacheManager) {
36 | this.cacheManager = cacheManager;
37 | }
38 |
39 | public static JettyCacheManager fromCacheManager(CacheManager cacheManager) {
40 | return cacheManager == null ? null : new JettyCacheManager(cacheManager);
41 | }
42 |
43 | public void setHeaders(URL url, HttpRequest request) throws URISyntaxException {
44 | HttpRequestBase apacheRequest = buildApacheRequest(url, request.getMethod());
45 | cacheManager.setHeaders(url, apacheRequest);
46 | setRequestHeaderFromApache(HTTPConstants.VARY, apacheRequest, request);
47 | setRequestHeaderFromApache(HTTPConstants.IF_MODIFIED_SINCE, apacheRequest, request);
48 | setRequestHeaderFromApache(HTTPConstants.IF_NONE_MATCH, apacheRequest, request);
49 | }
50 |
51 | private HttpRequestBase buildApacheRequest(URL url, String method) throws URISyntaxException {
52 | URI uri = url.toURI();
53 | switch (method) {
54 | case HTTPConstants.POST:
55 | return new HttpPost(uri);
56 | case HTTPConstants.GET:
57 | return new HttpGet(uri);
58 | case HTTPConstants.PUT:
59 | return new HttpPut(uri);
60 | case HTTPConstants.HEAD:
61 | return new HttpHead(uri);
62 | case HTTPConstants.TRACE:
63 | return new HttpTrace(uri);
64 | case HTTPConstants.OPTIONS:
65 | return new HttpOptions(uri);
66 | case HTTPConstants.DELETE:
67 | return new HttpDelete(uri);
68 | case HTTPConstants.PATCH:
69 | return new HttpPatch(uri);
70 | default:
71 | if (HttpWebdav.isWebdavMethod(method)) {
72 | return new HttpWebdav(method, uri);
73 | } else {
74 | throw new IllegalArgumentException(String.format("Unexpected method: '%s'", method));
75 | }
76 | }
77 | }
78 |
79 | private void setRequestHeaderFromApache(String headerName, HttpRequestBase apacheRequest,
80 | HttpRequest request) {
81 | Header header = apacheRequest.getFirstHeader(headerName);
82 | if (header != null) {
83 | request.addHeader(new HttpField(headerName, header.getValue()));
84 | }
85 | }
86 |
87 | public boolean inCache(URL url, HttpFields headers) {
88 | Header[] apacheHeaders = headers.stream()
89 | .map(h -> new BasicHeader(h.getName(), h.getValue()))
90 | .toArray(Header[]::new);
91 | return cacheManager.inCache(url, apacheHeaders);
92 | }
93 |
94 | public HTTPSampleResult buildCachedSampleResult(HTTPSampleResult res) {
95 | CachedResourceMode cachedResourceMode = CachedResourceMode.valueOf(
96 | JMeterUtils.getPropDefault("cache_manager.cached_resource_mode",
97 | CachedResourceMode.RETURN_NO_SAMPLE.toString()));
98 | switch (cachedResourceMode) {
99 | case RETURN_NO_SAMPLE:
100 | return null;
101 | case RETURN_200_CACHE:
102 | res.sampleEnd();
103 | res.setResponseCodeOK();
104 | res.setResponseMessage(
105 | JMeterUtils.getPropDefault("RETURN_200_CACHE.message", "(ex cache)"));
106 | res.setSuccessful(true);
107 | return res;
108 | case RETURN_CUSTOM_STATUS:
109 | res.sampleEnd();
110 | res.setResponseCode(JMeterUtils.getProperty("RETURN_CUSTOM_STATUS.code"));
111 | res.setResponseMessage(
112 | JMeterUtils.getPropDefault("RETURN_CUSTOM_STATUS.message", "(ex cache)"));
113 | res.setSuccessful(true);
114 | return res;
115 | default:
116 | throw new IllegalStateException("Unknown CACHED_RESOURCE_MODE");
117 | }
118 | }
119 |
120 | private enum CachedResourceMode {
121 | RETURN_200_CACHE,
122 | RETURN_NO_SAMPLE,
123 | RETURN_CUSTOM_STATUS
124 | }
125 |
126 | public void saveDetails(ContentResponse contentResponse, HTTPSampleResult result) {
127 | cacheManager.saveDetails(buildApacheResponse(contentResponse), result);
128 | }
129 |
130 | public HttpResponse buildApacheResponse(ContentResponse contentResponse) {
131 | HttpResponse httpResponse = new BasicHttpResponse(
132 | buildApacheVersion(contentResponse.getVersion()), contentResponse.getStatus(),
133 | contentResponse.getReason());
134 | setApacheResponseHeader(HTTPConstants.VARY, contentResponse, httpResponse);
135 | setApacheResponseHeader(HTTPConstants.LAST_MODIFIED, contentResponse, httpResponse);
136 | setApacheResponseHeader(HTTPConstants.EXPIRES, contentResponse, httpResponse);
137 | setApacheResponseHeader(HTTPConstants.ETAG, contentResponse, httpResponse);
138 | setApacheResponseHeader(HTTPConstants.CACHE_CONTROL, contentResponse, httpResponse);
139 | setApacheResponseHeader(HTTPConstants.DATE, contentResponse, httpResponse);
140 | return httpResponse;
141 | }
142 |
143 | private ProtocolVersion buildApacheVersion(HttpVersion version) {
144 | return new ProtocolVersion(version.name(), version.getVersion() / 10,
145 | version.getVersion() % 10);
146 | }
147 |
148 | private void setApacheResponseHeader(String headerName, ContentResponse response,
149 | HttpResponse apacheResponse) {
150 | apacheResponse.addHeader(headerName, response.getHeaders().get(headerName));
151 | }
152 |
153 | }
154 |
--------------------------------------------------------------------------------
/src/test/java/com/blazemeter/jmeter/http2/control/HTTP2ControllerTest.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.control;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.mockito.Mockito.when;
5 |
6 | import com.blazemeter.jmeter.http2.HTTP2TestBase;
7 | import com.blazemeter.jmeter.http2.core.HTTP2FutureResponseListener;
8 | import com.blazemeter.jmeter.http2.sampler.HTTP2Sampler;
9 | import com.blazemeter.jmeter.http2.sampler.JMeterTestUtils;
10 | import java.net.URI;
11 | import java.net.URISyntaxException;
12 | import java.util.concurrent.Executors;
13 | import java.util.concurrent.ScheduledExecutorService;
14 | import java.util.concurrent.TimeUnit;
15 | import org.apache.jmeter.control.NextIsNullException;
16 | import org.apache.jmeter.protocol.http.sampler.HTTPSampler;
17 | import org.apache.jmeter.samplers.Sampler;
18 | import org.apache.jmeter.util.JMeterUtils;
19 | import org.eclipse.jetty.client.HttpRequest;
20 | import org.junit.Before;
21 | import org.junit.Test;
22 | import org.junit.runner.RunWith;
23 | import org.mockito.Mock;
24 | import org.mockito.junit.MockitoJUnitRunner;
25 |
26 | @RunWith(MockitoJUnitRunner.class)
27 | public class HTTP2ControllerTest extends HTTP2TestBase {
28 |
29 |
30 | private static final String MAX_CONCURRENT_ASYNC_IN_CONTROLLER =
31 | "httpJettyClient.maxConcurrentAsyncInController";
32 | private HTTP2Controller http2Controller;
33 | private HTTP2Sampler firstSampler;
34 | private HTTP2Sampler secondSampler;
35 | private final HTTPSampler otherSamplerType = new HTTPSampler();
36 | @Mock
37 | private HttpRequest request;
38 | @Mock
39 | private HTTP2FutureResponseListener firstSamplerListener;
40 | @Mock
41 | private HTTP2FutureResponseListener secondSamplerListener;
42 |
43 | @Before
44 | public void setUp() {
45 | firstSampler = new HTTP2Sampler();
46 | secondSampler = new HTTP2Sampler();
47 | firstSampler.setFutureResponseListener(firstSamplerListener);
48 | secondSampler.setFutureResponseListener(secondSamplerListener);
49 | JMeterTestUtils.setupJmeterEnv();
50 | }
51 |
52 |
53 | @Test
54 | public void shouldModifyAndRetrieveSamplerToRunAsyncWhenProvideSyncSampler() throws Exception {
55 | setupHttp2Controller(false, 1);
56 | HTTP2Sampler currentElement = (HTTP2Sampler) http2Controller.next();
57 | assertThat(currentElement.isSyncRequest()).isFalse();
58 | }
59 |
60 | @Test(timeout = 7000)
61 | public void shouldBusyWaitOnlyForFirstSamplerWhenMaxConcurrentAsyncInControllerOvercome()
62 | throws Exception {
63 | setupHttp2Controller(false, 1);
64 | simulateSamplerExecution(secondSamplerListener, 80000);
65 | simulateSamplerExecution(firstSamplerListener, 3000);
66 | http2Controller.next();
67 | http2Controller.next();
68 | }
69 |
70 | private void setupHttp2Controller(boolean otherTypeOfRequest,
71 | int maxConcurrentAsyncInController) throws URISyntaxException {
72 | JMeterUtils.setProperty(MAX_CONCURRENT_ASYNC_IN_CONTROLLER,
73 | String.valueOf(maxConcurrentAsyncInController));
74 | http2Controller = new HTTP2Controller();
75 | http2Controller.addTestElement(firstSampler);
76 | http2Controller.addTestElement(secondSampler);
77 | when(request.getURI()).thenReturn(new URI("https://test.com"));
78 | when(firstSamplerListener.getRequest()).thenReturn(request);
79 | when(secondSamplerListener.getRequest()).thenReturn(request);
80 | if (otherTypeOfRequest) {
81 | http2Controller.addTestElement(otherSamplerType);
82 | }
83 | }
84 |
85 | private static void simulateSamplerExecution(HTTP2FutureResponseListener samplerListener,
86 | int delayInMillis) {
87 | //Thanks to mockito by default when(samplerListener.isDone()).thenReturn(false)
88 | //
89 | ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
90 | executorService.schedule(() -> {
91 | System.out.printf("Scheduled service with a delay of %d executed", delayInMillis);
92 | when(samplerListener.isDone()).thenReturn(true);
93 | return null;
94 | }, delayInMillis, TimeUnit.MILLISECONDS);
95 | }
96 |
97 | @Test(timeout = 10000)
98 | public void shouldBusyWaitForAsyncSamplersWhenControllerReachOtherSamplerType()
99 | throws URISyntaxException {
100 | setupHttp2Controller(true, 2);
101 | simulateSamplerExecution(secondSamplerListener, 6000);
102 | simulateSamplerExecution(firstSamplerListener, 2000);
103 | Sampler next = null;
104 | for (int i = 0; i < 5; i++) {
105 | next = http2Controller.next();
106 | }
107 | assertThat(next).isInstanceOf(HTTPSampler.class);
108 | }
109 |
110 | // TODO:
111 | // Tests using isDone are discussed because it is a misconception.
112 | //isDone is more related to the row of the entire test and not to the controller itself.
113 | //The isDone setting is removed, because it is something that apparently manages the iterators
114 | // and thread group.
115 | //Analyze if it does not require refacoring incorporating any of these to maintain the tests.
116 |
117 | /*
118 | @Test(expected = NextIsNullException.class)
119 | public void shouldThrowNextIsNullWhenNextWithoutElementsLeft() throws NextIsNullException {
120 | http2Controller = new HTTP2Controller();
121 | http2Controller.getCurrentElement();
122 | }
123 | */
124 |
125 | /*
126 | @Test
127 | public void shouldSetControllerDoneWhenNoMoreElementsToBeProcessed() {
128 | http2Controller = new HTTP2Controller();
129 | http2Controller.next();
130 | assertThat(http2Controller.isDone()).isTrue();
131 | }
132 | */
133 |
134 | /*
135 | @Test
136 | public void shouldControllerDoneWhenSamplesProcessed() throws URISyntaxException {
137 | http2Controller = new HTTP2Controller();
138 |
139 | http2Controller.addTestElement(firstSampler);
140 | http2Controller.addTestElement(secondSampler);
141 | when(request.getURI()).thenReturn(new URI("https://test.com"));
142 | when(firstSamplerListener.isDone()).thenReturn(true);
143 | when(firstSamplerListener.getRequest()).thenReturn(request);
144 | when(secondSamplerListener.isDone()).thenReturn(true);
145 | when(secondSamplerListener.getRequest()).thenReturn(request);
146 | http2Controller.initialize();
147 | int resultCount = 0;
148 | while (!http2Controller.isDone()) {
149 | Sampler next = http2Controller.next();
150 | if (next == null) {
151 | continue;
152 | }
153 | SampleResult sampleResult = next.sample(null);
154 | if (sampleResult != null) {
155 | resultCount += 1;
156 | }
157 | }
158 | // NOTE: The mocking make an error in the results, because the async execution return a value
159 | // and is expected to return null in that case, and the real response in the second sample
160 | // execution
161 | assertThat(resultCount).isEqualTo(4);
162 | }
163 | */
164 | }
165 |
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2SamplerConverter.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.sampler;
2 |
3 | import com.blazemeter.jmeter.http2.sampler.gui.HTTP2SamplerGui;
4 | import com.thoughtworks.xstream.converters.UnmarshallingContext;
5 | import com.thoughtworks.xstream.io.HierarchicalStreamReader;
6 | import com.thoughtworks.xstream.mapper.Mapper;
7 | import java.util.Arrays;
8 | import java.util.List;
9 | import javax.swing.JOptionPane;
10 | import org.apache.jmeter.config.Arguments;
11 | import org.apache.jmeter.config.ConfigTestElement;
12 | import org.apache.jmeter.protocol.http.config.gui.HttpDefaultsGui;
13 | import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
14 | import org.apache.jmeter.reporters.ResultCollector;
15 | import org.apache.jmeter.save.converters.TestElementConverter;
16 | import org.apache.jmeter.testelement.TestElement;
17 | import org.apache.jmeter.testelement.TestPlan;
18 | import org.apache.jmeter.testelement.property.StringProperty;
19 | import org.apache.jmeter.testelement.property.TestElementProperty;
20 | import org.apache.jmeter.visualizers.ViewResultsFullVisualizer;
21 |
22 | public class HTTP2SamplerConverter extends TestElementConverter {
23 |
24 | private static final String GUI_CLASS = "guiclass";
25 | private static final String TEST_PLAN_GUI_CLASS = "TestPlanGui";
26 | private static final String HTTP2_REQUEST_GUI_CLASS = "com.blazemeter.jmeter.http2.sampler.gui"
27 | + ".HTTP2RequestGui";
28 | private static final String HTTP2_DEFAULT_GUI_CLASS = "com.blazemeter.jmeter.http2.sampler.gui"
29 | + ".Http2DefaultsGui";
30 | private static final String HTTP2_VIEW_RESULTS_GUI_CLASS = "com.blazemeter.jmeter.http2"
31 | + ".visualizers.ViewResultsFullVisualizer";
32 | private boolean isConverting = false;
33 |
34 | public HTTP2SamplerConverter(Mapper arg0) {
35 | super(arg0);
36 | }
37 |
38 | @Override
39 | public boolean canConvert(Class elementClass) {
40 | return HTTP2Request.class.isAssignableFrom(elementClass) || ConfigTestElement.class
41 | .isAssignableFrom(elementClass) || ResultCollector.class.isAssignableFrom(elementClass)
42 | || TestPlan.class.isAssignableFrom(elementClass);
43 | }
44 |
45 | @Override
46 | public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
47 | String guiClassName = reader.getAttribute(GUI_CLASS);
48 | List oldGuiClasses = Arrays.asList(HTTP2_REQUEST_GUI_CLASS, HTTP2_DEFAULT_GUI_CLASS,
49 | HTTP2_VIEW_RESULTS_GUI_CLASS);
50 | if (guiClassName.equals(TEST_PLAN_GUI_CLASS)) {
51 | isConverting = false;
52 | return super.unmarshal(reader, context);
53 | } else if (oldGuiClasses.contains(guiClassName)) {
54 | if (!isConverting) {
55 | isConverting = shouldUpdateTestPlanMessage();
56 | }
57 | if (isConverting) {
58 | TestElement testElement = (TestElement) super.unmarshal(reader, context);
59 | if (guiClassName.equals(HTTP2_REQUEST_GUI_CLASS) || guiClassName
60 | .equals(HTTP2_DEFAULT_GUI_CLASS)) {
61 | return convertSampler(testElement);
62 | } else {
63 | return convertViewResult(
64 | (com.blazemeter.jmeter.http2.visualizers.ResultCollector) testElement);
65 | }
66 | } else {
67 | throw new UnsupportedOperationException("This test plan can not be loaded "
68 | + "since it is created with an old version of the HTTP2 plugins.");
69 | }
70 |
71 | }
72 | return super.unmarshal(reader, context);
73 | }
74 |
75 | private static ResultCollector convertViewResult(
76 | com.blazemeter.jmeter.http2.visualizers.ResultCollector resultCollector) {
77 | resultCollector.setProperty(TestElement.GUI_CLASS, ViewResultsFullVisualizer.class.getName());
78 | resultCollector.setProperty(TestElement.TEST_CLASS, ResultCollector.class.getName());
79 | return resultCollector;
80 |
81 | }
82 |
83 | private static TestElement convertSampler(TestElement testElement) {
84 | TestElement testElementReturn;
85 | if (testElement instanceof HTTP2Request) {
86 | testElementReturn = new HTTP2Sampler();
87 | testElementReturn
88 | .setProperty(new StringProperty(TestElement.GUI_CLASS, HTTP2SamplerGui.class.getName()));
89 | testElementReturn
90 | .setProperty(new StringProperty(TestElement.TEST_CLASS, HTTP2Sampler.class.getName()));
91 | } else if (testElement instanceof ConfigTestElement) {
92 | testElementReturn = new ConfigTestElement();
93 | testElementReturn.setProperty(TestElement.GUI_CLASS, HttpDefaultsGui.class.getName());
94 | testElementReturn.setProperty(TestElement.TEST_CLASS, ConfigTestElement.class.getName());
95 | } else {
96 | throw new UnsupportedOperationException(
97 | String.format("Error while convert class %s", testElement.getClass()));
98 | }
99 | testElementReturn.setComment(testElement.getComment());
100 | testElementReturn.setName(testElement.getName());
101 | testElementReturn.setEnabled(testElement.isEnabled());
102 | testElementReturn.setProperty(HTTPSamplerBase.POST_BODY_RAW,
103 | testElement.getPropertyAsBoolean(HTTP2Request.POST_BODY_RAW, false),
104 | HTTPSamplerBase.POST_BODY_RAW_DEFAULT);
105 | testElementReturn.setProperty(new TestElementProperty(HTTPSamplerBase.ARGUMENTS,
106 | (Arguments) testElement.getProperty(HTTP2Request.ARGUMENTS).getObjectValue()));
107 | testElementReturn.setProperty(HTTPSamplerBase.DOMAIN,
108 | testElement.getPropertyAsString(HTTP2Request.DOMAIN, ""));
109 | testElementReturn
110 | .setProperty(HTTPSamplerBase.PORT, testElement.getPropertyAsString(HTTP2Request.PORT, ""));
111 | testElementReturn.setProperty(HTTPSamplerBase.RESPONSE_TIMEOUT,
112 | testElement.getPropertyAsString(HTTP2Request.RESPONSE_TIMEOUT, ""));
113 | testElementReturn.setProperty(HTTPSamplerBase.PROTOCOL,
114 | testElement.getPropertyAsString(HTTP2Request.PROTOCOL, ""));
115 | testElementReturn.setProperty(HTTPSamplerBase.CONTENT_ENCODING,
116 | testElement.getPropertyAsString(HTTP2Request.CONTENT_ENCODING, ""));
117 | testElementReturn
118 | .setProperty(HTTPSamplerBase.PATH, testElement.getPropertyAsString(HTTP2Request.PATH, ""));
119 | testElementReturn.setProperty(HTTPSamplerBase.METHOD,
120 | testElement.getPropertyAsString(HTTP2Request.METHOD, ""));
121 | testElementReturn.setProperty(HTTPSamplerBase.FOLLOW_REDIRECTS,
122 | testElement.getPropertyAsBoolean(HTTP2Request.FOLLOW_REDIRECTS, false));
123 | testElementReturn.setProperty(HTTPSamplerBase.AUTO_REDIRECTS,
124 | testElement.getPropertyAsBoolean(HTTP2Request.AUTO_REDIRECTS, false));
125 | testElementReturn.setProperty(HTTPSamplerBase.IMAGE_PARSER,
126 | testElement.getPropertyAsBoolean(HTTP2Request.EMBEDDED_RESOURCES, false));
127 | testElementReturn.setProperty(HTTPSamplerBase.EMBEDDED_URL_RE,
128 | testElement.getPropertyAsString(HTTP2Request.EMBEDDED_URL_REGEX, ""));
129 | return testElementReturn;
130 | }
131 |
132 | private boolean shouldUpdateTestPlanMessage() {
133 | return JOptionPane.showConfirmDialog(null, "Your test plan is not compatible with this "
134 | + "version of "
135 | + "the plugin, do you want to migrate your test plan to the new version? (If you "
136 | + "migrate your test plan it will not be able to open with a lower version of the "
137 | + "plugin)",
138 | "bzm HTTP2 Sampler plugin - test plan update?", JOptionPane.YES_NO_OPTION,
139 | JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.blazemeter.jmeter
8 | jmeter-bzm-http2
9 | jar
10 | 2.0.6
11 | HTTP/2 Sampler
12 | HTTP/2 protocol sampler
13 |
14 |
15 | UTF-8
16 | UTF-8
17 | 5.4.1
18 | 11.0.15
19 |
20 |
21 |
22 |
23 | org.apache.jmeter
24 | ApacheJMeter_core
25 | ${jmeter.version}
26 | provided
27 |
28 |
29 | org.apache.jmeter
30 | ApacheJMeter_http
31 | ${jmeter.version}
32 | provided
33 |
34 |
35 | org.eclipse.jetty.http2
36 | http2-client
37 | ${jetty.version}
38 |
39 |
40 | org.eclipse.jetty
41 | jetty-client
42 | ${jetty.version}
43 |
44 |
45 | org.eclipse.jetty.http2
46 | http2-http-client-transport
47 | ${jetty.version}
48 |
49 |
50 | org.slf4j
51 | slf4j-api
52 | 1.7.26
53 | provided
54 |
55 |
56 | org.mockito
57 | mockito-core
58 | 2.28.2
59 | test
60 |
61 |
62 | org.assertj
63 | assertj-core
64 | 3.12.2
65 | test
66 |
67 |
68 | kg.apc
69 | jmeter-plugins-cmn-jmeter
70 | 0.6
71 | test
72 |
73 |
74 | com.blazemeter
75 | jmeter-bzm-commons
76 | 0.2.3
77 |
78 |
79 | kg.apc
80 | jmeter-plugins-emulators
81 | 0.4
82 | test
83 |
84 |
85 | org.apache.jmeter
86 | ApacheJMeter_tcp
87 |
88 |
89 |
90 |
91 | junit
92 | junit
93 | 4.13.1
94 | test
95 |
96 |
97 | org.eclipse.jetty
98 | jetty-alpn-server
99 | ${jetty.version}
100 | test
101 |
102 |
103 | org.eclipse.jetty
104 | jetty-alpn-java-server
105 | ${jetty.version}
106 | test
107 |
108 |
109 | org.eclipse.jetty
110 | jetty-servlet
111 | ${jetty.version}
112 | test
113 |
114 |
115 | org.eclipse.jetty.http2
116 | http2-server
117 | ${jetty.version}
118 | test
119 |
120 |
121 | org.eclipse.jetty
122 | jetty-server
123 | ${jetty.version}
124 | test
125 |
126 |
127 | org.eclipse.jetty
128 | jetty-proxy
129 | ${jetty.version}
130 | test
131 |
132 |
133 | com.google.guava
134 | guava
135 | 29.0-jre
136 | test
137 |
138 |
139 |
140 |
141 |
142 |
143 | src/main/resources
144 | true
145 |
146 |
147 |
148 |
149 | org.apache.maven.plugins
150 | maven-surefire-plugin
151 | 2.22.2
152 |
153 |
154 | false
155 |
156 |
157 |
158 | org.apache.maven.plugins
159 | maven-compiler-plugin
160 | 3.8.1
161 |
162 | 1.8
163 | 1.8
164 |
165 |
166 |
167 | org.apache.maven.plugins
168 | maven-checkstyle-plugin
169 | 3.1.0
170 |
171 |
172 | validate
173 | validate
174 |
175 | checkstyle.xml
176 | true
177 | true
178 |
179 |
180 | check
181 |
182 |
183 |
184 |
185 |
186 | org.apache.maven.plugins
187 | maven-failsafe-plugin
188 | 2.22.2
189 |
190 |
191 |
192 | integration-test
193 | verify
194 |
195 |
196 |
197 |
198 |
199 | org.apache.maven.plugins
200 | maven-dependency-plugin
201 | 3.0.2
202 |
203 |
204 | copy-jmeter-dependencies
205 | install
206 |
207 |
208 | copy-dependencies
209 |
210 |
211 |
212 |
213 | http2-client, jetty-client, http2-http-client-transport, http2-common, http2-hpack,
214 | jetty-http, jetty-alpn-client, jetty-alpn-java-client, jetty-io, jetty-util
215 |
216 | ${project.build.directory}/jmeter-test/lib
217 | true
218 |
219 |
220 |
221 | copy-plugin-to-jmeter-dependencies
222 | install
223 |
224 | copy
225 |
226 |
227 |
228 |
229 | ${project.groupId}
230 | ${project.artifactId}
231 | ${project.version}
232 | ${project.packaging}
233 |
234 |
235 | ${project.build.directory}/jmeter-test/lib
236 | true
237 |
238 |
239 |
240 |
241 |
242 | maven-resources-plugin
243 | 3.0.2
244 |
245 |
246 | copy-jmeter-resources
247 | install
248 |
249 | copy-resources
250 |
251 |
252 | ${project.build.directory}/jmeter-test
253 |
254 |
255 | src/test/resources/jmeter
256 | true
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 | release
268 |
269 |
270 |
271 | org.apache.maven.plugins
272 | maven-enforcer-plugin
273 | 3.0.0-M3
274 |
275 |
276 | enforce-no-snapshots
277 |
278 | enforce
279 |
280 |
281 |
282 |
283 | No Snapshots Allowed!
284 |
285 | org.apache.maven:maven-core
286 | org.apache.maven.plugins:*
287 |
288 |
289 |
290 | true
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/sampler/gui/HTTP2SamplerPanel.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.sampler.gui;
2 |
3 | import java.awt.BorderLayout;
4 | import java.awt.Component;
5 | import java.awt.Container;
6 | import java.awt.Dimension;
7 | import java.util.Arrays;
8 | import java.util.function.Predicate;
9 | import javax.swing.BorderFactory;
10 | import javax.swing.JCheckBox;
11 | import javax.swing.JLabel;
12 | import javax.swing.JPanel;
13 | import javax.swing.JPasswordField;
14 | import javax.swing.JTabbedPane;
15 | import javax.swing.JTextField;
16 | import javax.swing.border.Border;
17 | import org.apache.jmeter.gui.util.HorizontalPanel;
18 | import org.apache.jmeter.gui.util.VerticalPanel;
19 | import org.apache.jmeter.protocol.http.config.gui.UrlConfigGui;
20 | import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
21 | import org.apache.jmeter.util.JMeterUtils;
22 | import org.apache.jorphan.gui.JLabeledTextField;
23 |
24 | public class HTTP2SamplerPanel extends JPanel {
25 |
26 | private static final String JBOOLEAN_PROPERTY_EDITOR_CLASS_NAME = "org.apache.jmeter.gui"
27 | + ".JBooleanPropertyEditor";
28 | private UrlConfigGui urlConfigGui;
29 | private final JTextField connectTimeOutField = new JTextField(10);
30 | private final JTextField responseTimeOutField = new JTextField(10);
31 | private final JTextField proxySchemeField = new JTextField(5);
32 | private final JTextField proxyHostField = new JTextField(10);
33 | private final JTextField proxyPortField = new JTextField(10);
34 | private final JTextField proxyUserField = new JTextField(5);
35 | private final JPasswordField proxyPassField = new JPasswordField(5);
36 | private final JCheckBox retrieveEmbeddedResourcesCheckBox = new JCheckBox(
37 | JMeterUtils.getResString("web_testing_retrieve_images"));
38 | private final JCheckBox concurrentDownloadCheckBox = new JCheckBox(
39 | JMeterUtils.getResString("web_testing_concurrent_download"));
40 | private final JTextField concurrentPoolField = new JTextField(2);
41 | private final JLabeledTextField embeddedResourcesRegexField = new JLabeledTextField(
42 | JMeterUtils.getResString("web_testing_embedded_url_pattern"), 20);
43 | private final JCheckBox http1Upgrade = new JCheckBox("HTTP1 Upgrade");
44 |
45 | public HTTP2SamplerPanel(boolean isSampler) {
46 | setLayout(new BorderLayout(0, 5));
47 | setBorder(BorderFactory.createEmptyBorder());
48 | add(createTabbedConfigPane(isSampler));
49 | }
50 |
51 | private JTabbedPane createTabbedConfigPane(boolean isSampler) {
52 | final JTabbedPane tabbedPane = new JTabbedPane();
53 | urlConfigGui = new UrlConfigGui(isSampler, true, true);
54 | replaceKeepAliveCheckWithHttp1Upgrade(urlConfigGui);
55 | tabbedPane.add(JMeterUtils.getResString("web_testing_basic"), urlConfigGui);
56 | final JPanel advancedPanel = createAdvancedConfigPanel();
57 | tabbedPane.add(JMeterUtils.getResString("web_testing_advanced"), advancedPanel);
58 | return tabbedPane;
59 | }
60 |
61 | private void replaceKeepAliveCheckWithHttp1Upgrade(UrlConfigGui urlConfigGui) {
62 | JPanel optionPanel = findOptionPanel(urlConfigGui);
63 | optionPanel.remove(2);
64 |
65 | http1Upgrade.setFont(null);
66 | http1Upgrade.setSelected(false);
67 |
68 | optionPanel.add(http1Upgrade);
69 | }
70 |
71 | private JPanel findOptionPanel(Container c) {
72 | if (isContainingKeepAliveCheck(c)) {
73 | return (JPanel) c;
74 | }
75 | JPanel ret = null;
76 | int i = 0;
77 | Component[] children = c.getComponents();
78 | while (ret == null && i < c.getComponentCount()) {
79 | Component child = children[i];
80 | if (child instanceof Container) {
81 | ret = findOptionPanel((Container) child);
82 | }
83 | i++;
84 | }
85 | return ret;
86 | }
87 |
88 | private boolean isContainingKeepAliveCheck(Container c) {
89 | if (!(c instanceof JPanel) || c.getComponentCount() != 5) {
90 | return false;
91 | }
92 | Predicate> existsInPanel =
93 | (p) -> Arrays.stream(c.getComponents()).allMatch(p);
94 | boolean isJMeterV56 = JMeterUtils.getJMeterVersion().startsWith("5.6");
95 |
96 | if (isJMeterV56 && existsInPanel.test((component -> component.getClass().getName().equals(
97 | JBOOLEAN_PROPERTY_EDITOR_CLASS_NAME) || component instanceof JCheckBox))) {
98 | /*
99 | Since JMeter v5.6.2 the UrlConfigGUI changed to:
100 | autoRedirects: JCheckBox
101 | followRedirects: JCheckBox
102 | useKeepAlive: JBooleanPropertyEditor <--
103 | useMultipart: JBooleanPropertyEditor
104 | useCompatibleMultiPartMode: JBooleanPropertyEditor
105 | */
106 | return true;
107 | }
108 |
109 | return !isJMeterV56 && existsInPanel.test((checkBox) -> checkBox instanceof JCheckBox);
110 | }
111 |
112 | private Border makeBorder() {
113 | return BorderFactory.createEmptyBorder(10, 10, 5, 10);
114 | }
115 |
116 | private JPanel createAdvancedConfigPanel() {
117 | JPanel advancedPanel = new VerticalPanel();
118 | advancedPanel.setBorder(makeBorder());
119 | advancedPanel.add(createTimeOutPanel());
120 | advancedPanel.add(createProxyPanel());
121 | advancedPanel.add(createEmbeddedResourcesPanel());
122 | return advancedPanel;
123 | }
124 |
125 | private JPanel createTimeOutPanel() {
126 | JPanel timeOutPanel = new HorizontalPanel();
127 | timeOutPanel.setBorder(BorderFactory.createTitledBorder(
128 | JMeterUtils.getResString("web_server_timeout_title")));
129 | timeOutPanel.add(createPanelWithLabelForField(connectTimeOutField,
130 | JMeterUtils.getResString("web_server_timeout_connect")));
131 | timeOutPanel.add(createPanelWithLabelForField(responseTimeOutField,
132 | JMeterUtils.getResString("web_server_timeout_response")));
133 | return timeOutPanel;
134 | }
135 |
136 | private JPanel createPanelWithLabelForField(JTextField field, String labelString) {
137 | JLabel label = new JLabel(labelString);
138 | label.setLabelFor(field);
139 | JPanel panel = new JPanel(new BorderLayout(5, 0));
140 | panel.add(label, BorderLayout.WEST);
141 | panel.add(field, BorderLayout.CENTER);
142 | return panel;
143 | }
144 |
145 | private JPanel createProxyPanel() {
146 | JPanel proxyPanel = new HorizontalPanel();
147 | proxyPanel.setBorder(BorderFactory
148 | .createTitledBorder(JMeterUtils.getResString("web_proxy_server_title")));
149 | proxyPanel.add(createProxyServerPanel());
150 | return proxyPanel;
151 | }
152 |
153 | private JPanel createProxyServerPanel() {
154 | JPanel proxyServerPanel = new HorizontalPanel();
155 | proxyServerPanel.add(createPanelWithLabelForField(proxySchemeField, JMeterUtils.getResString(
156 | "web_proxy_scheme")), BorderLayout.WEST);
157 | proxyServerPanel.add(createPanelWithLabelForField(proxyHostField, JMeterUtils.getResString(
158 | "web_server_domain")), BorderLayout.CENTER);
159 | proxyServerPanel.add(createPanelWithLabelForField(proxyPortField, JMeterUtils.getResString(
160 | "web_server_port")), BorderLayout.EAST);
161 | return proxyServerPanel;
162 | }
163 |
164 | private JPanel createEmbeddedResourcesPanel() {
165 | final JPanel embeddedResourcesPanel = new HorizontalPanel();
166 | embeddedResourcesPanel.setBorder(BorderFactory
167 | .createTitledBorder(BorderFactory.createEtchedBorder(),
168 | JMeterUtils.getResString("web_testing_retrieve_title")));
169 | retrieveEmbeddedResourcesCheckBox.addItemListener(e -> updateEnableStatus());
170 | concurrentDownloadCheckBox.addItemListener(e -> updateEnableStatus());
171 | concurrentPoolField.setMinimumSize(
172 | new Dimension(10, (int) concurrentPoolField.getPreferredSize().getHeight()));
173 | concurrentPoolField.setMaximumSize(
174 | new Dimension(30, (int) concurrentPoolField.getPreferredSize().getHeight()));
175 | embeddedResourcesPanel.add(retrieveEmbeddedResourcesCheckBox);
176 | embeddedResourcesPanel.add(concurrentDownloadCheckBox);
177 | embeddedResourcesPanel.add(concurrentPoolField);
178 | embeddedResourcesPanel.add(embeddedResourcesRegexField);
179 | return embeddedResourcesPanel;
180 | }
181 |
182 | private void updateEnableStatus() {
183 | concurrentDownloadCheckBox.setEnabled(retrieveEmbeddedResourcesCheckBox.isSelected());
184 | embeddedResourcesRegexField.setEnabled(retrieveEmbeddedResourcesCheckBox.isSelected());
185 | concurrentPoolField
186 | .setEnabled(retrieveEmbeddedResourcesCheckBox.isSelected() && concurrentDownloadCheckBox
187 | .isSelected());
188 | }
189 |
190 | public void resetFields() {
191 | urlConfigGui.clear();
192 | http1Upgrade.setSelected(false);
193 | retrieveEmbeddedResourcesCheckBox.setSelected(false);
194 | concurrentDownloadCheckBox.setSelected(false);
195 | concurrentPoolField.setText(String.valueOf(HTTPSamplerBase.CONCURRENT_POOL_SIZE));
196 | updateEnableStatus();
197 | connectTimeOutField.setText("");
198 | responseTimeOutField.setText("");
199 | proxySchemeField.setText("");
200 | proxyHostField.setText("");
201 | proxyPortField.setText("");
202 | proxyUserField.setText("");
203 | proxyPassField.setText("");
204 | }
205 |
206 | public UrlConfigGui getUrlConfigGui() {
207 | return urlConfigGui;
208 | }
209 |
210 | public boolean isHttp1UpgradeSelected() {
211 | return http1Upgrade.isSelected();
212 | }
213 |
214 | public void setHttp1UpgradeSelected(boolean enabled) {
215 | http1Upgrade.setSelected(enabled);
216 | }
217 |
218 | public String getConnectTimeOut() {
219 | return connectTimeOutField.getText();
220 | }
221 |
222 | public String getResponseTimeOut() {
223 | return responseTimeOutField.getText();
224 | }
225 |
226 | public String getProxyScheme() {
227 | return proxySchemeField.getText();
228 | }
229 |
230 | public String getProxyHost() {
231 | return proxyHostField.getText();
232 | }
233 |
234 | public String getProxyPort() {
235 | return proxyPortField.getText();
236 | }
237 |
238 | public String getProxyUser() {
239 | return proxyUserField.getText();
240 | }
241 |
242 | public String getProxyPass() {
243 | return new String(proxyPassField.getPassword());
244 | }
245 |
246 | public boolean getRetrieveEmbeddedResources() {
247 | return retrieveEmbeddedResourcesCheckBox.isSelected();
248 | }
249 |
250 | public boolean getConcurrentDownload() {
251 | return concurrentDownloadCheckBox.isSelected();
252 | }
253 |
254 | public String getConcurrentPool() {
255 | return concurrentPoolField.getText();
256 | }
257 |
258 | public String getEmbeddedResourcesRegex() {
259 | return embeddedResourcesRegexField.getText();
260 | }
261 |
262 | public void setConnectTimeOut(String connectTimeOut) {
263 | this.connectTimeOutField.setText(connectTimeOut);
264 | }
265 |
266 | public void setResponseTimeOut(String responseTimeOut) {
267 | this.responseTimeOutField.setText(responseTimeOut);
268 | }
269 |
270 | public void setProxyScheme(String proxyScheme) {
271 | this.proxySchemeField.setText(proxyScheme);
272 | }
273 |
274 | public void setProxyHost(String proxyHost) {
275 | this.proxyHostField.setText(proxyHost);
276 | }
277 |
278 | public void setProxyPort(String proxyPort) {
279 | this.proxyPortField.setText(proxyPort);
280 | }
281 |
282 | public void setProxyUser(String proxyUser) {
283 | this.proxyUserField.setText(proxyUser);
284 | }
285 |
286 | public void setProxyPass(String proxyPass) {
287 | this.proxyPassField.setText(proxyPass);
288 | }
289 |
290 | public void setRetrieveEmbeddedResources(boolean retrieveEmbeddedResources) {
291 | this.retrieveEmbeddedResourcesCheckBox.setSelected(retrieveEmbeddedResources);
292 | }
293 |
294 | public void setConcurrentDownload(boolean concurrentDownload) {
295 | this.concurrentDownloadCheckBox.setSelected(concurrentDownload);
296 | }
297 |
298 | public void setConcurrentPool(String concurrentPool) {
299 | this.concurrentPoolField.setText(concurrentPool);
300 | }
301 |
302 | public void setEmbeddedResourcesRegex(String embeddedResourcesRegex) {
303 | this.embeddedResourcesRegexField.setText(embeddedResourcesRegex);
304 | }
305 | }
306 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HTTP2 Plugin for JMeter
2 |
3 | ---
4 |
5 |
6 |
7 |
8 |
9 | This plugin provides an HTTP2 Sampler and a HTTP2 Controller in order to test you HTTP/2 endpoint.
10 |
11 | _**IMPORTANT:** Java 11 required_
12 |
13 | ## To create your test:
14 |
15 | 1. Install the HTTP/2 plugin from the [plugins manager](https://www.blazemeter.com/blog/how-install-jmeter-plugins-manager).
16 |
17 | 2. Create a Thread Group.
18 |
19 | 3. Add the HTTP2 Sampler (Add-> Sampler-> bzm - HTTP2 Sampler).
20 |
21 | 
22 |
23 | After that you can add timers, assertions, listeners, etc.
24 |
25 | ## Configuring the HTTP2 Sampler:
26 |
27 | Let's explain the HTTP2 Sampler fields:
28 |
29 | ### Basic tab:
30 |
31 | 
32 |
33 | | **Field** | **Description** | **Default** |
34 | |----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
35 | | Protocol | Choose HTTP or HTTPS | HTTP |
36 | | Server name or IP | The domain name or IP address of the web server. *[Do not include the http:// prefix.]*. | |
37 | | Port number | The port the web server is listening to | 80 |
38 | | Method | GET, POST, PUT, PATCH, DELETE and OPTIONS are the ones supported at the moment. | |
39 | | Path | The path to resource (For example: `/servlets/myServlet`). | |
40 | | Content Encoding | Content encoding to be used (for POST, PUT, PATCH and FILE). This is the character encoding to be used, and is not related to the Content-Encoding HTTP header. | |
41 | | Redirect Automatically | Sets the underlying HTTP protocol handler to automatically follow redirects, so they are not seen by JMeter, and therefore will not appear as samples. | |
42 | | Follow Redirects | If set, the JMeter sampler will check if the response is a redirect and will follow it. The initial redirect and further responses will appear as additional samples. | |
43 | | Use multipart/form-data | Use a `multipart/form-data` or `application/x-www-form-urlencoded` post request | |
44 | | HTTP1 Upgrade | Enables the usage of the Upgrade header for HTTP1 request. (Not enabling this sets HTTP2 as default). | |
45 |
46 | ### Advanced tab:
47 |
48 | 
49 |
50 | | **Field** | **Description** | **Default** |
51 | |-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
52 | | **Timeouts (milliseconds):** | | |
53 | | Connect | Number of milliseconds to wait for a connection to open. | |
54 | | Response | The number of milliseconds to wait for a response. | |
55 | | **Proxy Server:** | | |
56 | | Scheme | The scheme identifies the protocol to be used to access the resource on the Internet. | http |
57 | | Server name or IP | Hostname or IP address of a proxy server to perform request. *[Do not include the http:// prefix]*. | |
58 | | Port Number | Port the proxy server is listening to. | |
59 | | Retrieve All Embedded Resources | Allows JMeter to parse the HTML file and send HTTP/HTTPS requests for all images, Java applets, JavaScript files, CSSs, etc. referenced in the file. | |
60 | | Parallel downloads | This feature allows the settings of a concurrent connection pool for retrieving embedded resources as part of the HTTP sampler. | |
61 | | URLs must match | Enables to filter the download of embedded resources that don't match the **regular expression** set on it. For example, setting this regex `http:\/\/example\.invalid\/.*`, will only download the embedded resources that comes from `http://example.invalid/`. | |
62 |
63 | ## HTTP/1 Upgrade
64 | When HTTP/1 Upgrade is selected means that the first request made by the plugin will contain all required headers for http1 upgrade.
65 |
66 | However, there is a known issue which will send the upgrade when receiving an HTTP1 response even thought the option of HTTP1 Upgrade is not selected.
67 | Additionally after the first request which may contain upgrade headers or not (depending on user selection) afterwards all HTTP/1.1 requests made as long as the server responds in HTTP/1.1 will contain upgrade headers.
68 |
69 | ## Buffer capacity
70 | By default, the size of the downloaded resources is set to 2 MB (2097152 bytes) but, the limit can be increased by adding the `httpJettyClient.maxBufferSize` property on the jmeter.properties file in bytes.
71 |
72 | ## Multiplexing
73 | One of the main features that were incorporated in HTTP2 was the multiplexing capability.
74 | Multiplexing in HTTP2 allows for multiple concurrent requests and responses to be transmitted over a single connection, improving efficiency and reducing latency.
75 |
76 | Currently, the plugin does not handle any limit for the asynchronous requests. However, it could be limited by setting the `maxConcurrentAsyncInController` property or it's also possible to tune the maximum number of requests per connection
77 | `httpJettyClient.maxRequestsPerConnection` which is 100 by default.
78 |
79 | > IMPORTANT: All HTTP2 requests outside a HTTP2 Async Controller will run synchronous (multiplexing disabled)
80 |
81 | ## HTTP2 Async Controller
82 |
83 | All HTTP2 samplers embedded in a HTTP2 Async controller will run asynchronous.
84 |
85 | 
86 | **Considerations**:
87 | 1. The amount of asynchronous requests will be determined by the JMeter property `httpJettyClient.maxConcurrentAsyncInController` which by default is `1000`
88 |
89 | 1. If there are any elements within the Async Controller that are not HTTP2Samplers, they will function as a synchronization point for all asynchronous requests that occur before those elements. This means that before executing the different element, the controller will wait for all previous requests to complete.
90 |
91 | 1. Listeners such as View Result Tree will process the elements that finish first, so the order in which they display results may not necessarily follow the TestPlan order.
92 |
93 |
94 | ## ALPN
95 |
96 | The HTTP2 plugin offers Application-Layer Protocol Negotiation (ALPN) support, which facilitates the negotiation of application-layer protocols within the TLS handshake. This enables smooth communication between the client and server by allowing them to agree upon the protocols to be used.
97 |
98 | The plugin supports the following protocols for ALPN negotiation:
99 |
100 | - HTTP/1.1
101 | - HTTP/2 (over TLS)
102 | - HTTP/2 (over cleartext, h2c)
103 |
104 | For TLS/SSL configuration please refer to [SSL Manager](https://jmeter.apache.org/usermanual/component_reference.html#SSL_Manager) and [Keystore Configuration](https://jmeter.apache.org/usermanual/component_reference.html#Keystore_Configuration)
105 |
106 | ## Auth Manager
107 | Currently, we only give support to the Basic and Digest authentication mechanism.
108 | To make use of Basic preemptive authentication results, make sure to create and set the property `httpJettyClient.auth.preemptive`
109 | to true in the jmeter.properties file.
110 |
111 | ## Embedded Resources
112 |
113 | To retrieve all embedded resources asynchronously, you simply need to choose "Parallel Downloads" as the option, unless the value is equal to 1.
114 | If you would like to download embedded resources in a synchronous way choose "Parallel Downloads" and set the number to 1.
115 |
116 | ## Properties
117 | This document describes JMeter properties. The properties present in jmeter.properties also should be set in the user.properties file. These properties are only taken into account after restarting JMeter as they are usually resolved when the class is loaded.
118 |
119 | | **Attribute** | **Description** | **Default** |
120 | |-----------------------------------------------------|----------------------------------------------------------------------------------|-------------|
121 | | **httpJettyClient.maxBufferSize** | Maximum size of the downloaded resources in bytes | 2097152 |
122 | | **httpJettyClient.minThreads** | Minimum number of threads per http client | 1 |
123 | | **httpJettyClient.maxThreads** | Maximum number of threads per http client | 5 |
124 | | **httpJettyClient.maxRequestsQueuedPerDestination** | Maximum number of requests that may be queued to a destination | 32767 |
125 | | **httpJettyClient.maxConnectionsPerDestination** | Sets the max number of connections to open to each destinations | 1 |
126 | | **httpJettyClient.byteBufferPoolFactor** | Factor number used in the allocation of memory in the buffer of http client | 4 |
127 | | **httpJettyClient.strictEventOrdering** | Force request events ordering | false |
128 | | **httpJettyClient.removeIdleDestinations** | Whether destinations that have no connections should be removed | true |
129 | | **httpJettyClient.idleTimeout** | the max time, in milliseconds, a connection can be idle | 30000 |
130 | | **httpJettyClient.auth.preemptive** | Use of Basic preemptive authentication results | false |
131 | | **httpJettyClient.maxConcurrentPushedStreams** | Sets the maximum number of server push streams that is allowed to concurrently receive from a server | 100 |
132 | | **httpJettyClient.maxConcurrentAsyncInController** | Maximum number of concurrent http2 samplers inside a HTTP2 Async Controller | 1000 |
133 | | **HTTPSampler.response_timeout** | Maximum waiting time of request without timeout defined, in milliseconds | 0 |
134 | | **http.post_add_content_type_if_missing** | Add to POST a Header Content-type: application/x-www-form-urlencoded if missing? | false |
135 |
--------------------------------------------------------------------------------
/src/test/java/com/blazemeter/jmeter/http2/core/ServerBuilder.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.core;
2 |
3 | import jakarta.servlet.http.HttpServlet;
4 | import jakarta.servlet.http.HttpServletRequest;
5 | import jakarta.servlet.http.HttpServletResponse;
6 | import java.io.File;
7 | import java.io.IOException;
8 | import java.net.URISyntaxException;
9 | import java.nio.charset.StandardCharsets;
10 | import java.util.ArrayList;
11 | import java.util.Arrays;
12 | import java.util.Collections;
13 | import java.util.List;
14 | import java.util.function.Consumer;
15 | import java.util.stream.Collectors;
16 | import java.util.zip.GZIPOutputStream;
17 | import jodd.net.MimeTypes;
18 | import org.apache.jmeter.protocol.http.util.HTTPConstants;
19 | import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
20 | import org.eclipse.jetty.http.HttpStatus;
21 | import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
22 | import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
23 | import org.eclipse.jetty.security.Authenticator;
24 | import org.eclipse.jetty.security.ConstraintMapping;
25 | import org.eclipse.jetty.security.ConstraintSecurityHandler;
26 | import org.eclipse.jetty.security.HashLoginService;
27 | import org.eclipse.jetty.security.UserStore;
28 | import org.eclipse.jetty.security.authentication.BasicAuthenticator;
29 | import org.eclipse.jetty.security.authentication.DigestAuthenticator;
30 | import org.eclipse.jetty.server.ConnectionFactory;
31 | import org.eclipse.jetty.server.HttpConfiguration;
32 | import org.eclipse.jetty.server.HttpConnectionFactory;
33 | import org.eclipse.jetty.server.SecureRequestCustomizer;
34 | import org.eclipse.jetty.server.Server;
35 | import org.eclipse.jetty.server.ServerConnector;
36 | import org.eclipse.jetty.server.SslConnectionFactory;
37 | import org.eclipse.jetty.servlet.ServletContextHandler;
38 | import org.eclipse.jetty.servlet.ServletHolder;
39 | import org.eclipse.jetty.util.component.LifeCycle;
40 | import org.eclipse.jetty.util.security.Constraint;
41 | import org.eclipse.jetty.util.security.Password;
42 | import org.eclipse.jetty.util.ssl.SslContextFactory;
43 |
44 | public class ServerBuilder {
45 |
46 | public static final String HOST_NAME = "localhost";
47 | public static final int SERVER_PORT = 6666;
48 | public static final String SERVER_RESPONSE = "Hello World!";
49 | public static final String SERVER_IMAGE = "/test/image.png";
50 | public static final String SERVER_PATH = "/test";
51 | public static final String SERVER_PATH_SET_COOKIES = "/test/set-cookies";
52 | public static final String SERVER_PATH_USE_COOKIES = "/test/use-cookies";
53 | public static final String RESPONSE_DATA_COOKIES = "testCookie=test";
54 | public static final String RESPONSE_DATA_COOKIES2 = "testCookie2=test";
55 | public static final String SERVER_PATH_200 = "/test/200";
56 | public static final String SERVER_PATH_SLOW = "/test/slow";
57 | public static final String SERVER_PATH_200_GZIP = "/test/gzip";
58 | public static final String SERVER_PATH_200_EMBEDDED = "/test/embedded";
59 | public static final String SERVER_PATH_200_FILE_SENT = "/test/file";
60 | public static final String SERVER_PATH_BIG_RESPONSE = "/test/big-response";
61 | public static final String SERVER_PATH_400 = "/test/400";
62 | public static final String SERVER_PATH_302 = "/test/302";
63 | public static final String SERVER_PATH_200_WITH_BODY = "/test/body";
64 | public static final String SERVER_PATH_DELETE_DATA = "/test/delete";
65 | public static final String BASIC_HTML_TEMPLATE = "Page "
66 | + "Title
";
67 | public static final byte[] BINARY_RESPONSE_BODY = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
68 | public static final String AUTH_USERNAME = "username";
69 | public static final String AUTH_PASSWORD = "password";
70 | public static final String AUTH_REALM = "realm";
71 | public static final String KEYSTORE_PASSWORD = "storepwd";
72 | public static final int BIG_BUFFER_SIZE = 4 * 1024 * 1024;
73 |
74 | private boolean ALPN;
75 | private final TeardownableServer server = new TeardownableServer();
76 | private final HttpConfiguration httpsConfig = new HttpConfiguration();
77 | private boolean withSSL;
78 | private HTTP2ServerConnectionFactory http2ConnectionFactory;
79 | private HttpConnectionFactory http1ConnectionFactory;
80 | private HTTP2CServerConnectionFactory http2cConnectionFactory;
81 | private boolean clientAuth;
82 | private boolean isBasicAuth;
83 | private boolean isDigestAuth;
84 |
85 | public ServerBuilder() {
86 | httpsConfig.addCustomizer(new SecureRequestCustomizer());
87 | }
88 |
89 | public ServerBuilder withHTTP1() {
90 | http1ConnectionFactory = new HttpConnectionFactory(httpsConfig);
91 | return this;
92 | }
93 |
94 | public ServerBuilder withHTTP2C() {
95 | http2cConnectionFactory = new HTTP2CServerConnectionFactory(httpsConfig);
96 | return this;
97 | }
98 |
99 | public ServerBuilder withHTTP2() {
100 | http2ConnectionFactory = new HTTP2ServerConnectionFactory(httpsConfig);
101 | return withHTTP1();//For handshake proposes
102 | }
103 |
104 | public ServerBuilder withSSL() {
105 | this.withSSL = true;
106 | return this;
107 | }
108 |
109 | public ServerBuilder withALPN() {
110 | this.ALPN = true;
111 | return this;
112 | }
113 |
114 | public ServerBuilder withNeedClientAuth() {
115 | this.clientAuth = true;
116 | return this;
117 | }
118 |
119 | public ServerBuilder withBasicAuth() {
120 | this.isBasicAuth = true;
121 | return this;
122 | }
123 |
124 | public ServerBuilder withDigestAuth() {
125 | this.isDigestAuth = true;
126 | return this;
127 | }
128 |
129 | public TeardownableServer buildServer() {
130 |
131 | ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();
132 | alpn.setDefaultProtocol(http1ConnectionFactory.getProtocol());
133 | ServerConnector http1UpgradedConnector = null;
134 | if (http2cConnectionFactory != null && http1ConnectionFactory != null) {
135 | http1UpgradedConnector = new ServerConnector(server, http1ConnectionFactory,
136 | http2cConnectionFactory);
137 | http1UpgradedConnector.setReusePort(false);
138 | }
139 | ConnectionFactory ssl = setupSslConnectionFactory(alpn, http1UpgradedConnector);
140 | List connectionFactories = new ArrayList<>();
141 | if (withSSL) {
142 | connectionFactories.add(ssl);
143 | }
144 | if (ALPN) {
145 | connectionFactories.add(alpn);
146 | }
147 | connectionFactories.addAll(buildConnectionFactories());
148 |
149 | ServerConnector connector = buildServerConnector(connectionFactories);
150 |
151 | server.addConnector(connector);
152 |
153 | ServletContextHandler context = new ServletContextHandler(server, "/", true, false);
154 | context.addServlet(new ServletHolder(buildServlet()), SERVER_PATH + "/*");
155 |
156 | if (isBasicAuth) {
157 | configureAuthHandler(server, new BasicAuthenticator(), Constraint.__BASIC_AUTH);
158 | return server;
159 | }
160 | if (isDigestAuth) {
161 | configureAuthHandler(server, new DigestAuthenticator(), Constraint.__DIGEST_AUTH);
162 | }
163 | return server;
164 | }
165 |
166 | private ConnectionFactory setupSslConnectionFactory(ALPNServerConnectionFactory alpn,
167 | ServerConnector http1UpgradedConnector) {
168 | SslContextFactory.Server sslFactory = buildServerSslContextFactory();
169 | sslFactory.setNeedClientAuth(clientAuth);
170 | return new SslConnectionFactory(sslFactory,
171 | ALPN ? alpn.getDefaultProtocol()
172 | : http1UpgradedConnector != null ? http1UpgradedConnector.getDefaultProtocol()
173 | : http1ConnectionFactory.getProtocol());
174 | }
175 |
176 | private ServerConnector buildServerConnector(List connectionFactories) {
177 | ServerConnector connector =
178 | new ServerConnector(server, 1, 1,
179 | connectionFactories.toArray(new ConnectionFactory[0]));
180 | connector.setPort(SERVER_PORT);
181 | connector.setReusePort(false);
182 | return connector;
183 | }
184 |
185 | private List buildConnectionFactories() {
186 | List connectionFactories = new ArrayList<>();
187 | Consumer factoryAppender = (c) -> {
188 | if (c != null) {
189 | connectionFactories.add(c);
190 | }
191 | };
192 | factoryAppender.accept(http1ConnectionFactory);
193 | factoryAppender.accept(http2cConnectionFactory);
194 | factoryAppender.accept(http2ConnectionFactory);
195 | return connectionFactories;
196 | }
197 |
198 | private SslContextFactory.Server buildServerSslContextFactory() {
199 | SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
200 | sslContextFactory.setKeyStorePath(getKeyStorePathAsUriPathInSSLContextFactoryFormat());
201 | sslContextFactory.setKeyStorePassword(KEYSTORE_PASSWORD);
202 | return sslContextFactory;
203 | }
204 |
205 | private String getKeyStorePathAsUriPathInSSLContextFactoryFormat() {
206 | try {
207 | // Generate a absolute path in URI format with compatibility with Windows
208 | // IMPORTANT: SSLContextFactory need a URI in relative format, internally they concat the path
209 | return new File("./").toURI().relativize(getClass().getResource("keystore.p12").toURI())
210 | .getPath();
211 | } catch (URISyntaxException e) {
212 | throw new RuntimeException(e);
213 | }
214 | }
215 |
216 | private HttpServlet buildServlet() {
217 | return new HttpServlet() {
218 |
219 | @Override
220 | protected void service(HttpServletRequest req, HttpServletResponse resp)
221 | throws IOException {
222 | switch (req.getServletPath() + req.getPathInfo()) {
223 | case SERVER_PATH_200:
224 | resp.setStatus(HttpStatus.OK_200);
225 | resp.setContentType(MimeTypes.MIME_TEXT_HTML + ";" + StandardCharsets.UTF_8.name());
226 | resp.getWriter().write(SERVER_RESPONSE);
227 | break;
228 | case SERVER_PATH_SLOW:
229 | try {
230 | Thread.sleep(10000);
231 | } catch (InterruptedException e) {
232 | e.printStackTrace();
233 | }
234 | resp.setStatus(HttpStatus.OK_200);
235 | break;
236 | case SERVER_PATH_400:
237 | resp.setStatus(HttpStatus.BAD_REQUEST_400);
238 | break;
239 | case SERVER_PATH_302:
240 | resp.addHeader(HTTPConstants.HEADER_LOCATION,
241 | "https://localhost:" + SERVER_PORT + SERVER_PATH_200);
242 | resp.setStatus(HttpStatus.FOUND_302);
243 | break;
244 | case SERVER_PATH_200_WITH_BODY:
245 | String bodyRequest = req.getReader().lines().collect(Collectors.joining());
246 | resp.getWriter().write(bodyRequest);
247 | break;
248 | case SERVER_PATH_SET_COOKIES:
249 | resp.addHeader(HTTPConstants.HEADER_SET_COOKIE,
250 | RESPONSE_DATA_COOKIES);
251 | resp.addHeader(HTTPConstants.HEADER_SET_COOKIE,
252 | RESPONSE_DATA_COOKIES2);
253 | break;
254 | case SERVER_PATH_USE_COOKIES:
255 | String cookie = req.getHeader(HTTPConstants.HEADER_COOKIE);
256 | resp.getWriter().write(cookie);
257 | break;
258 | case SERVER_PATH_200_EMBEDDED:
259 | resp.setContentType(MimeTypes.MIME_TEXT_HTML + ";" + StandardCharsets.UTF_8.name());
260 | resp.getWriter().write(BASIC_HTML_TEMPLATE);
261 | resp.addHeader(HTTPConstants.EXPIRES,
262 | "Sat, 25 Sep 2041 00:00:00 GMT");
263 | break;
264 | case SERVER_IMAGE:
265 | resp.getOutputStream().write(new byte[] {1, 2, 3, 4, 5});
266 | case SERVER_PATH_200_FILE_SENT:
267 | resp.setContentType("image/png");
268 | byte[] requestBody = req.getInputStream().readAllBytes();
269 | resp.getOutputStream().write(requestBody);
270 | break;
271 | case SERVER_PATH_200_GZIP:
272 | resp.addHeader("Content-Encoding", "gzip");
273 | GZIPOutputStream gzipOutputStream = new GZIPOutputStream(resp.getOutputStream());
274 | gzipOutputStream.write(BINARY_RESPONSE_BODY);
275 | gzipOutputStream.close();
276 | break;
277 | case SERVER_PATH_DELETE_DATA:
278 | resp.setStatus(HttpStatus.OK_200);
279 | break;
280 | case SERVER_PATH_BIG_RESPONSE:
281 | resp.getOutputStream().write(new byte[(int) BIG_BUFFER_SIZE]);
282 | resp.setContentType("image/jpg");
283 | break;
284 | }
285 | }
286 | };
287 | }
288 |
289 |
290 | private void configureAuthHandler(Server server, Authenticator authenticator,
291 | String mechanism) {
292 | ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
293 | String[] roles = new String[] {"can-access"};
294 | securityHandler.setAuthenticator(authenticator);
295 | securityHandler.setConstraintMappings(
296 | Collections.singletonList(buildConstraintMapping(mechanism, roles)));
297 | securityHandler.setRealmName(AUTH_REALM);
298 | securityHandler.setLoginService(buildLoginService(roles));
299 | securityHandler.setHandler(server.getHandler());
300 | server.setHandler(securityHandler);
301 | }
302 |
303 | private ConstraintMapping buildConstraintMapping(String mechanism, String[] roles) {
304 | Constraint constraint = new Constraint();
305 | constraint.setName(mechanism);
306 | constraint.setAuthenticate(true);
307 | constraint.setRoles(roles);
308 |
309 | ConstraintMapping ret = new ConstraintMapping();
310 | ret.setPathSpec("/*");
311 | ret.setConstraint(constraint);
312 | return ret;
313 | }
314 |
315 | private HashLoginService buildLoginService(String[] roles) {
316 | UserStore userStore = new UserStore();
317 | userStore.addUser(AUTH_USERNAME, new Password(AUTH_PASSWORD), roles);
318 |
319 | HashLoginService ret = new HashLoginService();
320 | ret.setName(AUTH_REALM);
321 | ret.setUserStore(userStore);
322 | return ret;
323 | }
324 |
325 | public static class TeardownableServer extends Server {
326 |
327 | @Override
328 | protected void doStop() throws Exception {
329 | Arrays.stream(this.getConnectors()).forEach(
330 | serverConnector -> {
331 | try {
332 | serverConnector.stop();
333 | } catch (Exception e) {
334 | e.printStackTrace();
335 | }
336 | });
337 | super.doStop();
338 | }
339 |
340 | @Override
341 | protected void stop(LifeCycle l) throws Exception {
342 | Arrays.stream(this.getConnectors()).forEach(
343 | serverConnector -> {
344 | try {
345 | serverConnector.stop();
346 | } catch (Exception e) {
347 | e.printStackTrace();
348 | }
349 | });
350 | l.stop();
351 | super.stop(l);
352 | }
353 | }
354 | }
355 |
356 |
357 |
358 |
359 |
--------------------------------------------------------------------------------
/src/test/resources/jmeter/HTTP2SamplerTest.jmx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | false
7 | true
8 | false
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | continue
17 |
18 | false
19 | 2
20 |
21 | 2
22 | 1
23 | false
24 |
25 |
26 | 1527189498000
27 | 1527189498000
28 |
29 |
30 |
31 |
32 |
33 |
34 | webtide.com
35 | 443
36 |
37 | https
38 |
39 |
40 |
41 |
42 |
43 |
44 | false
45 |
46 |
47 |
48 | true
49 |
50 |
51 |
52 | false
53 |
54 | =
55 |
56 |
57 |
58 | http2cdn.cdnsun.com
59 | 443
60 | 5000
61 | https
62 |
63 |
64 | GET
65 | true
66 | false
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | header1
75 | header1
76 |
77 |
78 | header2
79 | header2
80 |
81 |
82 |
83 |
84 |
85 |
86 | etty and CometD Ex
87 |
88 | Assertion.response_data
89 | false
90 | 2
91 |
92 |
93 |
94 |
95 |
96 | true
97 |
98 |
99 |
100 | false
101 |
102 | =
103 |
104 |
105 |
106 | webtide.com
107 |
108 | 5000
109 |
110 |
111 | /why-choose-webtide/
112 | GET
113 | true
114 | false
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | header1
123 | header1
124 |
125 |
126 | header2
127 | header2
128 |
129 |
130 |
131 |
132 |
133 |
134 | hy Choose Webtid
135 |
136 | Assertion.response_data
137 | false
138 | 2
139 |
140 |
141 |
142 |
143 |
144 | true
145 |
146 |
147 |
148 | false
149 | {
150 | "testProp" : "test"
151 | }
152 | =
153 |
154 |
155 |
156 | nghttp2.org
157 | 443
158 |
159 | https
160 |
161 | /httpbin/post
162 | POST
163 | true
164 | false
165 |
166 | true
167 |
168 |
169 |
170 |
171 |
172 |
173 | header1
174 | header1
175 |
176 |
177 | header2
178 | header2
179 |
180 |
181 |
182 |
183 |
184 |
185 | testProp
186 |
187 | Assertion.response_data
188 | false
189 | 2
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 | nghttp2.org
199 |
200 |
201 |
202 |
203 | /httpbin/get
204 | GET
205 | true
206 | false
207 |
208 |
209 |
210 |
211 |
212 | false
213 |
214 | saveConfig
215 |
216 |
217 | true
218 | true
219 | true
220 |
221 | true
222 | true
223 | true
224 | true
225 | false
226 | true
227 | true
228 | false
229 | false
230 | false
231 | true
232 | false
233 | false
234 | false
235 | true
236 | 0
237 | true
238 | true
239 | true
240 | true
241 | true
242 |
243 |
244 |
245 |
246 |
247 |
248 | false
249 |
250 | saveConfig
251 |
252 |
253 | true
254 | true
255 | true
256 |
257 | true
258 | true
259 | true
260 | true
261 | false
262 | true
263 | true
264 | false
265 | false
266 | false
267 | true
268 | false
269 | false
270 | false
271 | true
272 | 0
273 | true
274 | true
275 | true
276 | true
277 | true
278 | true
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
--------------------------------------------------------------------------------
/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java:
--------------------------------------------------------------------------------
1 | package com.blazemeter.jmeter.http2.sampler;
2 |
3 | import static org.apache.jmeter.util.JMeterUtils.getPropDefault;
4 |
5 | import com.blazemeter.jmeter.http2.core.HTTP2FutureResponseListener;
6 | import com.blazemeter.jmeter.http2.core.HTTP2JettyClient;
7 | import com.github.benmanes.caffeine.cache.Caffeine;
8 | import com.github.benmanes.caffeine.cache.LoadingCache;
9 | import com.helger.commons.annotation.VisibleForTesting;
10 | import java.net.MalformedURLException;
11 | import java.net.URISyntaxException;
12 | import java.net.URL;
13 | import java.util.ArrayList;
14 | import java.util.HashMap;
15 | import java.util.Iterator;
16 | import java.util.List;
17 | import java.util.Map;
18 | import java.util.Objects;
19 | import java.util.concurrent.Callable;
20 | import java.util.concurrent.ConcurrentHashMap;
21 | import java.util.function.Predicate;
22 | import java.util.regex.PatternSyntaxException;
23 | import org.apache.commons.lang3.StringUtils;
24 | import org.apache.commons.lang3.tuple.Pair;
25 | import org.apache.jmeter.engine.event.LoopIterationEvent;
26 | import org.apache.jmeter.engine.event.LoopIterationListener;
27 | import org.apache.jmeter.protocol.http.parser.BaseParser;
28 | import org.apache.jmeter.protocol.http.parser.LinkExtractorParseException;
29 | import org.apache.jmeter.protocol.http.parser.LinkExtractorParser;
30 | import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
31 | import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
32 | import org.apache.jmeter.protocol.http.util.ConversionUtils;
33 | import org.apache.jmeter.protocol.http.util.HTTPConstants;
34 | import org.apache.jmeter.samplers.SampleResult;
35 | import org.apache.jmeter.testelement.TestElement;
36 | import org.apache.jmeter.testelement.ThreadListener;
37 | import org.apache.jmeter.threads.JMeterContextService;
38 | import org.apache.jmeter.threads.JMeterVariables;
39 | import org.apache.jmeter.util.JMeterUtils;
40 | import org.apache.jorphan.util.JOrphanUtils;
41 | import org.apache.oro.text.MalformedCachePatternException;
42 | import org.apache.oro.text.regex.Pattern;
43 | import org.apache.oro.text.regex.Perl5Matcher;
44 | import org.eclipse.jetty.client.HttpRequest;
45 | import org.slf4j.Logger;
46 | import org.slf4j.LoggerFactory;
47 |
48 | public class HTTP2Sampler extends HTTPSamplerBase implements LoopIterationListener, ThreadListener {
49 |
50 | private static class OwnInheritableThreadLocal
51 | extends InheritableThreadLocal