├── 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 | ![](docs/addHTTP2Sampler.png) 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 | ![](docs/http2Sampler-basic.png) 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 | ![](docs/http2Sampler-advanced.png) 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 | ![HTTP2 Controller Demostration](docs/http2-async-controller.gif) 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> { 52 | @Override 53 | protected Map initialValue() { 54 | return new HashMap<>(); 55 | } 56 | 57 | @Override 58 | protected Map childValue( 59 | Map parentValue) { 60 | return parentValue; 61 | } 62 | } 63 | 64 | public static final String SYNC_REQUEST = "HTTP2Sampler.sync_request"; 65 | private static final Logger LOG = LoggerFactory.getLogger(HTTP2Sampler.class); 66 | /* 67 | private static final ThreadLocal> CONNECTIONS = 68 | ThreadLocal 69 | .withInitial(HashMap::new); 70 | */ 71 | private static final OwnInheritableThreadLocal CONNECTIONS = new OwnInheritableThreadLocal(); 72 | 73 | private static final boolean IGNORE_FAILED_EMBEDDED_RESOURCES = 74 | getPropDefault( 75 | "httpsampler.ignore_failed_embedded_resources", false); // $NON-NLS-1$ 76 | 77 | private static final String HTTP1_UPGRADE_PROPERTY = "HTTP2Sampler.http1_upgrade"; 78 | // Derive the mapping of content types to parsers 79 | private static final Map PARSERS_FOR_CONTENT_TYPE = new ConcurrentHashMap<>(); 80 | private static final String USER_AGENT = "User-Agent"; // $NON-NLS-1$ 81 | private static final boolean USE_JAVA_REGEX = !getPropDefault( 82 | "jmeter.regex.engine", "oro").equalsIgnoreCase("oro"); 83 | private static final String RESPONSE_PARSERS = // list of parsers 84 | JMeterUtils.getProperty("HTTPResponse.parsers"); //$NON-NLS-1$ 85 | 86 | static { 87 | String[] parsers = 88 | JOrphanUtils.split(RESPONSE_PARSERS, " ", true); // returns empty array for null 89 | for (final String parser : parsers) { 90 | String classname = JMeterUtils.getProperty(parser + ".className"); //$NON-NLS-1$ 91 | if (classname == null) { 92 | LOG.error("Cannot find .className property for {}, ensure you set property: '{}.className'", 93 | parser, parser); 94 | continue; 95 | } 96 | String typeList = JMeterUtils.getProperty(parser + ".types"); //$NON-NLS-1$ 97 | if (typeList != null) { 98 | String[] types = JOrphanUtils.split(typeList, " ", true); 99 | for (final String type : types) { 100 | registerParser(type, classname); 101 | } 102 | } else { 103 | LOG.warn( 104 | "Cannot find .types property for {}, as a consequence parser " + 105 | "will not be used, to make it usable, define property:'{}.types'", 106 | parser, parser); 107 | } 108 | } 109 | } 110 | 111 | private final transient Callable clientFactory; 112 | private final boolean dumpAtThreadEnd = getPropDefault( 113 | "httpJettyClient.DumpAtThreadEnd", false); 114 | private boolean syncRequest = true; 115 | private HTTP2FutureResponseListener asyncListener; 116 | private int maxBufferSize; 117 | private int requestTimeout; 118 | private HTTPSampleResult result; 119 | 120 | public HTTP2Sampler() { 121 | setName("HTTP2 Sampler"); 122 | setMethod(HTTPConstants.GET); 123 | clientFactory = this::getClient; 124 | this.syncRequest = getPropertyAsBoolean(SYNC_REQUEST, true); 125 | } 126 | 127 | @VisibleForTesting 128 | public HTTP2Sampler(Callable clientFactory) { 129 | this.clientFactory = clientFactory; 130 | } 131 | 132 | public void setSyncRequest(boolean sync) { 133 | this.syncRequest = sync; 134 | } 135 | 136 | @VisibleForTesting 137 | public boolean isSyncRequest() { 138 | return this.syncRequest; 139 | } 140 | 141 | public HTTP2FutureResponseListener geFutureResponseListener() { 142 | return asyncListener; 143 | } 144 | 145 | @VisibleForTesting 146 | public void setFutureResponseListener(HTTP2FutureResponseListener listener) { 147 | this.asyncListener = listener; 148 | } 149 | 150 | public void setHttp1UpgradeEnabled(boolean http1UpgradeSelected) { 151 | setProperty(HTTP1_UPGRADE_PROPERTY, http1UpgradeSelected); 152 | } 153 | 154 | public boolean isHttp1UpgradeEnabled() { 155 | return getPropertyAsBoolean(HTTP1_UPGRADE_PROPERTY); 156 | } 157 | 158 | @Override 159 | protected HTTPSampleResult sample(URL url, String method, boolean areFollowingRedirect, 160 | int depth) { 161 | try { 162 | HTTP2JettyClient client = clientFactory.call(); 163 | this.maxBufferSize = client.getMaxBufferSize(); 164 | this.requestTimeout = client.getRequestTimeout(); 165 | if (!isSyncRequest()) { 166 | if (Objects.isNull(this.asyncListener)) { 167 | this.result = buildResult(url, method); // Save the main result for next step 168 | HTTP2FutureResponseListener listener = 169 | new HTTP2FutureResponseListener(client.getMaxBufferSize()); 170 | this.asyncListener = listener; 171 | HttpRequest req = client.sampleAsync(this, this.result, listener); 172 | req.send(listener); // Fire the Async 173 | return null; 174 | } else { 175 | // If there is a listener, it is processed using the result it had 176 | this.result = sampleFromListener( 177 | this.result, areFollowingRedirect, depth, this.asyncListener); 178 | return this.result; 179 | } 180 | } else { 181 | this.result = buildResult(url, method); 182 | return client.sample(this, this.result, areFollowingRedirect, depth); 183 | } 184 | } catch (InterruptedException e) { 185 | Thread.currentThread().interrupt(); 186 | if (Objects.isNull(this.result)) { 187 | this.result = buildResult(url, method); 188 | } 189 | return buildErrorResult(e, this.result); 190 | } catch (Exception e) { 191 | if (Objects.isNull(this.result)) { 192 | this.result = buildResult(url, method); 193 | } 194 | return buildErrorResult(e, this.result); 195 | } 196 | } 197 | 198 | protected HttpRequest sampleAsync(HTTPSampleResult result, HTTP2FutureResponseListener listener) 199 | throws Exception { 200 | 201 | HTTP2JettyClient client = clientFactory.call(); 202 | return client.sampleAsync(this, result, listener); 203 | 204 | } 205 | 206 | protected HTTPSampleResult sampleFromListener(HTTPSampleResult result, 207 | boolean areFollowingRedirect, 208 | int depth, HTTP2FutureResponseListener listener) 209 | throws Exception { 210 | HTTP2JettyClient client = clientFactory.call(); 211 | return client.sampleFromListener(this, result, areFollowingRedirect, 212 | depth, listener); 213 | } 214 | 215 | private HTTPSampleResult buildResult(URL url, String method) { 216 | HTTPSampleResult result = new HTTPSampleResult(); 217 | result.setSampleLabel(SampleResult.isRenameSampleLabel() ? getName() : url.toString()); 218 | result.setHTTPMethod(method); 219 | result.setURL(url); 220 | return result; 221 | } 222 | 223 | private HTTPSampleResult buildErrorResult(Exception e, HTTPSampleResult result) { 224 | if (result.getStartTime() == 0) { 225 | result.sampleStart(); 226 | } 227 | if (result.getEndTime() == 0) { 228 | if (!Objects.isNull(this.asyncListener) && this.asyncListener.getResponseEnd() != 0) { 229 | result.setEndTime(this.asyncListener.getResponseEnd()); 230 | } else { 231 | result.sampleEnd(); 232 | } 233 | } 234 | return errorResult(e, result); 235 | } 236 | 237 | private HTTP2JettyClient buildClient() throws Exception { 238 | HTTP2ClientKey connectionKey = buildConnectionKey(); 239 | HTTP2JettyClient client = new HTTP2JettyClient(isHttp1UpgradeEnabled(), 240 | "http2[" + connectionKey.target + ":" + Thread.currentThread().getId() + "]"); 241 | client.start(); 242 | CONNECTIONS.get().put(connectionKey, client); 243 | return client; 244 | } 245 | 246 | private HTTP2ClientKey buildConnectionKey() throws MalformedURLException { 247 | return new HTTP2ClientKey(getUrl(), !getProxyHost().isEmpty(), getProxyScheme(), getProxyHost(), 248 | getProxyPortInt()); 249 | } 250 | 251 | private HTTP2JettyClient getClient() throws Exception { 252 | Map clients = CONNECTIONS.get(); 253 | HTTP2ClientKey key = buildConnectionKey(); 254 | return clients.containsKey(key) ? clients.get(key) 255 | : buildClient(); 256 | } 257 | 258 | public HTTPSampleResult resultProcessing(final boolean pAreFollowingRedirect, 259 | final int frameDepth, final HTTPSampleResult pRes) { 260 | return super.resultProcessing(pAreFollowingRedirect, frameDepth, pRes); 261 | } 262 | 263 | static void registerParser(String contentType, String className) { 264 | LOG.info("Parser for {} is {}", contentType, className); 265 | PARSERS_FOR_CONTENT_TYPE.put(contentType, className); 266 | } 267 | 268 | private LinkExtractorParser getParser(HTTPSampleResult res) 269 | throws LinkExtractorParseException { 270 | String parserClassName = 271 | PARSERS_FOR_CONTENT_TYPE.get(res.getMediaType()); 272 | if (!StringUtils.isEmpty(parserClassName)) { 273 | return BaseParser.getParser(parserClassName); 274 | } 275 | return null; 276 | } 277 | 278 | private String getUserAgent(HTTPSampleResult sampleResult) { 279 | String res = sampleResult.getRequestHeaders(); 280 | int index = res.indexOf(USER_AGENT); 281 | if (index >= 0) { 282 | // see HTTPHC3Impl#getConnectionHeaders 283 | // see HTTPHC4Impl#getConnectionHeaders 284 | // see HTTPJavaImpl#getConnectionHeaders 285 | //': ' is used by JMeter to fill-in requestHeaders, see getConnectionHeaders 286 | final String userAgentPrefix = USER_AGENT + ": "; 287 | String userAgentHdr = res.substring( 288 | index + userAgentPrefix.length(), 289 | res.indexOf( 290 | '\n', 291 | // '\n' is used by JMeter to fill-in requestHeaders, see getConnectionHeaders 292 | index + userAgentPrefix.length() + 1)); 293 | return userAgentHdr.trim(); 294 | } else { 295 | if (LOG.isDebugEnabled()) { 296 | LOG.debug("No user agent extracted from requestHeaders:{}", res); 297 | } 298 | return null; 299 | } 300 | } 301 | 302 | private void setParentSampleSuccess(HTTPSampleResult res, boolean initialValue) { 303 | if (!IGNORE_FAILED_EMBEDDED_RESOURCES) { 304 | res.setSuccessful(initialValue); 305 | if (!initialValue) { 306 | StringBuilder detailedMessage = new StringBuilder(80); 307 | detailedMessage.append("Embedded resource download error:"); //$NON-NLS-1$ 308 | for (SampleResult subResult : res.getSubResults()) { 309 | HTTPSampleResult httpSampleResult = (HTTPSampleResult) subResult; 310 | if (!httpSampleResult.isSuccessful()) { 311 | detailedMessage.append(httpSampleResult.getURL()) 312 | .append(" code:") //$NON-NLS-1$ 313 | .append(httpSampleResult.getResponseCode()) 314 | .append(" message:") //$NON-NLS-1$ 315 | .append(httpSampleResult.getResponseMessage()) 316 | .append(", "); //$NON-NLS-1$ 317 | } 318 | } 319 | res.setResponseMessage(detailedMessage.toString()); //$NON-NLS-1$ 320 | } 321 | } 322 | } 323 | 324 | private static final class LazyJavaPatternCacheHolder { 325 | 326 | public static final LoadingCache, java.util.regex.Pattern> INSTANCE = 327 | Caffeine 328 | .newBuilder() 329 | .maximumSize(getPropDefault("jmeter.regex.patterncache.size", 1000)) 330 | .build(key -> { 331 | //noinspection MagicConstant 332 | return java.util.regex.Pattern.compile(key.getLeft(), key.getRight().intValue()); 333 | }); 334 | 335 | private LazyJavaPatternCacheHolder() { 336 | super(); 337 | } 338 | 339 | } 340 | 341 | public static java.util.regex.Pattern compilePattern(String expression) { 342 | return compilePattern(expression, 0); 343 | } 344 | 345 | public static java.util.regex.Pattern compilePattern(String expression, int flags) { 346 | return LazyJavaPatternCacheHolder.INSTANCE.get(Pair.of(expression, Integer.valueOf(flags))); 347 | } 348 | 349 | private Predicate generateMatcherPredicate(String regex, String explanation, 350 | boolean defaultAnswer) { 351 | if (StringUtils.isEmpty(regex)) { 352 | return s -> defaultAnswer; 353 | } 354 | if (USE_JAVA_REGEX) { 355 | try { 356 | java.util.regex.Pattern pattern = compilePattern(regex); 357 | return s -> pattern.matcher(s.toString()).matches(); 358 | } catch (PatternSyntaxException e) { 359 | LOG.warn("Ignoring embedded URL {} string: {}", explanation, e.getMessage()); 360 | return s -> defaultAnswer; 361 | } 362 | } 363 | try { 364 | Pattern pattern = JMeterUtils.getPattern(regex); 365 | Perl5Matcher matcher = JMeterUtils.getMatcher(); 366 | return s -> matcher.matches(s.toString(), pattern); 367 | } catch (MalformedCachePatternException e) { // NOSONAR 368 | LOG.warn("Ignoring embedded URL {} string: {}", explanation, e.getMessage()); 369 | return s -> defaultAnswer; 370 | } 371 | } 372 | 373 | private URL escapeIllegalURLCharacters(java.net.URL url) { 374 | if (url == null || "file".equals(url.getProtocol())) { 375 | return url; 376 | } 377 | try { 378 | return ConversionUtils.sanitizeUrl(url).toURL(); 379 | } catch (Exception ex) { // NOSONAR 380 | LOG.error("Error escaping URL:'{}', message:{}", url, ex.getMessage()); 381 | return url; 382 | } 383 | } 384 | 385 | @Override 386 | protected HTTPSampleResult downloadPageResources(final HTTPSampleResult pRes, 387 | final HTTPSampleResult container, 388 | final int frameDepth) { 389 | 390 | boolean orgSyncRequest = isSyncRequest(); 391 | boolean interrupted = false; 392 | HTTPSampleResult res = pRes; 393 | Iterator urls = null; 394 | List samplers = new ArrayList(); 395 | 396 | try { 397 | final byte[] responseData = res.getResponseData(); 398 | if (responseData.length > 0) { // Bug 39205 399 | final LinkExtractorParser parser = getParser(res); 400 | if (parser != null) { 401 | String userAgent = getUserAgent(res); 402 | urls = parser.getEmbeddedResourceURLs(userAgent, responseData, res.getURL(), 403 | res.getDataEncodingWithDefault()); 404 | } 405 | } 406 | } catch (LinkExtractorParseException e) { 407 | e.printStackTrace(System.err); 408 | // Don't break the world just because this failed: 409 | res.addSubResult(errorResult(e, new HTTPSampleResult(res))); 410 | setParentSampleSuccess(res, false); 411 | } 412 | 413 | HTTPSampleResult lContainer = container; 414 | // Iterate through the URLs and download each image: 415 | if (urls != null && urls.hasNext()) { 416 | if (lContainer == null) { 417 | lContainer = new HTTPSampleResult(res); 418 | lContainer.addRawSubResult(res); 419 | } 420 | final HTTPSampleResult subres = lContainer; 421 | res = subres; 422 | 423 | // Get the URL matcher 424 | String allowRegex = ""; 425 | String excludeRegex = ""; 426 | Predicate allowPredicate = null; 427 | Predicate excludePredicate = null; 428 | try { 429 | allowRegex = getEmbeddedUrlRE(); 430 | allowPredicate = generateMatcherPredicate(allowRegex, "allow", true); 431 | excludeRegex = getEmbededUrlExcludeRE(); 432 | excludePredicate = 433 | generateMatcherPredicate(excludeRegex, "exclude", false); 434 | } catch (Exception ex) { 435 | ex.printStackTrace(System.err); 436 | throw ex; 437 | } 438 | // For concurrent get resources 439 | int maxConcurrentDownloads = CONCURRENT_POOL_SIZE; // init with default value 440 | boolean isConcurrentDwn = isConcurrentDwn(); 441 | 442 | if (isConcurrentDwn) { 443 | try { 444 | maxConcurrentDownloads = Integer.parseInt(getConcurrentPool()); 445 | } catch (NumberFormatException nfe) { 446 | LOG.warn("Concurrent download resources selected, "// $NON-NLS-1$ 447 | + "but pool size value is bad. Use default value"); // $NON-NLS-1$ 448 | } 449 | 450 | // if the user choose a number of parallel downloads of 1 451 | // no need to use another thread, do the sample on the current thread 452 | if (maxConcurrentDownloads == 1) { 453 | LOG.warn("Number of parallel downloads set to 1, (sampler name={})", getName()); 454 | isConcurrentDwn = false; 455 | } 456 | } 457 | 458 | setSyncRequest(!isConcurrentDwn); // Change default from main request based on sub request 459 | 460 | while (urls.hasNext()) { 461 | Object binURL = urls.next(); // See catch clause below 462 | try { 463 | URL url = (URL) binURL; 464 | if (url == null) { 465 | LOG.warn("Null URL detected (should not happen)"); 466 | } else { 467 | try { 468 | url = escapeIllegalURLCharacters(url); 469 | } catch (Exception e) { // NOSONAR 470 | subres.addSubResult( 471 | errorResult(new Exception(url.toString() + " is not a correct URI", e), 472 | new HTTPSampleResult(res))); 473 | setParentSampleSuccess(subres, false); 474 | continue; 475 | } 476 | if (!allowPredicate.test(url)) { 477 | continue; // we have a pattern and the URL does not match, so skip it 478 | } 479 | if (excludePredicate.test(url)) { 480 | continue; // we have a pattern and the URL does not match, so skip it 481 | } 482 | try { 483 | url = url.toURI().normalize().toURL(); 484 | } catch (MalformedURLException | URISyntaxException e) { 485 | subres.addSubResult( 486 | errorResult(new Exception(url.toString() + " URI can not be normalized", e), 487 | new HTTPSampleResult(subres))); 488 | setParentSampleSuccess(subres, false); 489 | continue; 490 | } 491 | 492 | HTTP2Sampler h2s = new HTTP2Sampler(); 493 | h2s.setMethod("GET"); 494 | h2s.setSyncRequest(!isConcurrentDwn); 495 | h2s.setProtocol(url.getProtocol()); 496 | h2s.setDomain(url.getHost()); 497 | h2s.setPort(url.getPort()); 498 | h2s.setHttp1UpgradeEnabled(isHttp1UpgradeEnabled()); 499 | h2s.setFollowRedirects(true); 500 | h2s.setAutoRedirects(true); 501 | if (url.getQuery() == null) { 502 | h2s.setPath(url.getPath()); 503 | } else { 504 | h2s.setPath(url.getPath() + url.getQuery()); 505 | } 506 | 507 | // Set proxy 508 | h2s.setProxyHost(this.getProxyHost()); 509 | h2s.setProxyPortInt(String.valueOf(this.getProxyPortInt())); 510 | h2s.setProxyScheme(this.getProxyScheme()); 511 | h2s.setProxyUser(this.getProxyUser()); 512 | h2s.setProxyPass(this.getProxyPass()); 513 | // Set Managers 514 | h2s.setHeaderManager(getHeaderManager()); 515 | h2s.setAuthManager(this.getAuthManager()); 516 | h2s.setCookieManager(this.getCookieManager()); 517 | 518 | HTTPSampleResult binRes = h2s.sample( 519 | url, HTTPConstants.GET, false, frameDepth + 1); 520 | 521 | if (isConcurrentDwn) { 522 | // if concurrent download emb. resources, add to a list for async gets later 523 | samplers.add(h2s); 524 | } else { 525 | // default: serial download embedded resources 526 | subres.addSubResult(binRes); 527 | setParentSampleSuccess(subres, 528 | subres.isSuccessful() && (binRes == null || binRes.isSuccessful())); 529 | try { 530 | Thread.sleep(10); 531 | } catch (InterruptedException e) { 532 | interrupted = true; 533 | } 534 | } 535 | } 536 | } catch (ClassCastException e) { // NOSONAR 537 | subres.addSubResult(errorResult(new Exception(binURL + " is not a correct URI", e), 538 | new HTTPSampleResult(subres))); 539 | setParentSampleSuccess(subres, false); 540 | } 541 | if (interrupted) { 542 | break; 543 | } 544 | } 545 | int embeddedTimeout = 0; // Use the same timeout for response 546 | if (this.getResponseTimeout() > 0) { 547 | embeddedTimeout = this.getResponseTimeout(); 548 | } else { 549 | embeddedTimeout = this.requestTimeout; 550 | } 551 | long start = System.currentTimeMillis(); 552 | 553 | // IF for download concurrent embedded resources 554 | if (isConcurrentDwn && !samplers.isEmpty()) { 555 | 556 | while (!samplers.isEmpty()) { 557 | 558 | HTTP2Sampler http2Sam = (HTTP2Sampler) samplers.get(0); 559 | 560 | HTTP2FutureResponseListener http2FListener = 561 | http2Sam.geFutureResponseListener(); 562 | while (!interrupted && (http2FListener != null)) { 563 | if (http2FListener.isDone() || http2FListener.isCancelled()) { 564 | String urlProcesed = http2FListener.getRequest().getURI().toString(); 565 | LOG.debug("HTTP2 Future Finished, retrying the sample with that data {}", 566 | urlProcesed); 567 | 568 | HTTPSampleResult binRes = (HTTPSampleResult) http2Sam.sample(); 569 | samplers.remove(0); // Remove the sample 570 | LOG.debug("OnComplete " + urlProcesed); 571 | 572 | subres.addSubResult(binRes); 573 | setParentSampleSuccess(subres, 574 | subres.isSuccessful() && (binRes == null 575 | || binRes.isSuccessful())); 576 | break; 577 | } 578 | try { 579 | Thread.sleep(10); 580 | } catch (InterruptedException e) { 581 | samplers.clear(); 582 | interrupted = true; 583 | } 584 | if (embeddedTimeout > 0 && (System.currentTimeMillis() - start) >= embeddedTimeout) { 585 | // TODO: This doesn't stop the async execution, only allow to don't lock execution 586 | LOG.debug("Timeout on Wait!"); 587 | subres.addSubResult(errorResult(new Exception( 588 | "Error downloading embedded resources, execution timeout"), 589 | new HTTPSampleResult(subres))); 590 | setParentSampleSuccess(subres, false); 591 | break; 592 | } 593 | } 594 | } 595 | } 596 | } 597 | setSyncRequest(orgSyncRequest); // Restore the default setting to main request 598 | 599 | if (interrupted) { 600 | Thread.currentThread().interrupt(); 601 | } 602 | return res; 603 | } 604 | 605 | @Override 606 | public void iterationStart(LoopIterationEvent iterEvent) { 607 | this.asyncListener = null; 608 | JMeterVariables jMeterVariables = JMeterContextService.getContext().getVariables(); 609 | if (!jMeterVariables.isSameUserOnNextIteration()) { 610 | clearUserStores(); 611 | } 612 | } 613 | 614 | private void closeConnections() { 615 | Map clients = CONNECTIONS.get(); 616 | for (HTTP2JettyClient client : clients.values()) { 617 | try { 618 | client.stop(); 619 | } catch (Exception e) { 620 | LOG.error("Error while closing connection", e); 621 | } 622 | } 623 | clients.clear(); 624 | } 625 | 626 | private void dump() { 627 | Map clients = CONNECTIONS.get(); 628 | for (HTTP2JettyClient client : clients.values()) { 629 | try { 630 | LOG.debug(client.dump()); 631 | } catch (Exception e) { 632 | LOG.error("Error while dump HTTP2JettyClient", e); 633 | } 634 | } 635 | } 636 | 637 | @Override 638 | public void testEnded() { 639 | super.testEnded(); 640 | System.gc(); // Force free memory 641 | } 642 | 643 | @Override 644 | public void threadFinished() { 645 | if (dumpAtThreadEnd) { 646 | dump(); 647 | } 648 | closeConnections(); 649 | } 650 | 651 | private void clearUserStores() { 652 | Map clients = CONNECTIONS.get(); 653 | for (HTTP2JettyClient client : clients.values()) { 654 | try { 655 | client.clearCookies(); 656 | client.clearAuthenticationResults(); 657 | } catch (Exception e) { 658 | LOG.error("Error while cleaning user store", e); 659 | } 660 | } 661 | } 662 | 663 | private static final class HTTP2ClientKey { 664 | 665 | private final String target; 666 | private final boolean hasProxy; 667 | private final String proxyScheme; 668 | private final String proxyHost; 669 | private final int proxyPort; 670 | 671 | private HTTP2ClientKey(URL url, boolean hasProxy, String proxyScheme, String proxyHost, 672 | int proxyPort) { 673 | this.target = url.getProtocol() + "://" + url.getAuthority(); 674 | this.hasProxy = hasProxy; 675 | this.proxyScheme = proxyScheme; 676 | this.proxyHost = proxyHost; 677 | this.proxyPort = proxyPort; 678 | } 679 | 680 | @Override 681 | public boolean equals(Object o) { 682 | if (this == o) { 683 | return true; 684 | } 685 | if (o == null || getClass() != o.getClass()) { 686 | return false; 687 | } 688 | HTTP2ClientKey that = (HTTP2ClientKey) o; 689 | return hasProxy == that.hasProxy && 690 | proxyPort == that.proxyPort && 691 | target.equals(that.target) && 692 | proxyScheme.equals(that.proxyScheme) && 693 | proxyHost.equals(that.proxyHost); 694 | } 695 | 696 | @Override 697 | public int hashCode() { 698 | return Objects.hash(target, hasProxy, proxyScheme, proxyHost, proxyPort); 699 | } 700 | } 701 | } 702 | -------------------------------------------------------------------------------- /src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java: -------------------------------------------------------------------------------- 1 | package com.blazemeter.jmeter.http2.core; 2 | 3 | import com.blazemeter.jmeter.http2.sampler.HTTP2Sampler; 4 | import java.io.ByteArrayInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.UnsupportedEncodingException; 8 | import java.net.MalformedURLException; 9 | import java.net.URI; 10 | import java.net.URISyntaxException; 11 | import java.net.URL; 12 | import java.net.URLDecoder; 13 | import java.nio.ByteBuffer; 14 | import java.nio.charset.Charset; 15 | import java.nio.charset.StandardCharsets; 16 | import java.nio.file.Path; 17 | import java.nio.file.Paths; 18 | import java.util.Arrays; 19 | import java.util.HashSet; 20 | import java.util.List; 21 | import java.util.Set; 22 | import java.util.concurrent.ExecutionException; 23 | import java.util.concurrent.TimeUnit; 24 | import java.util.concurrent.TimeoutException; 25 | import java.util.regex.Pattern; 26 | import java.util.stream.StreamSupport; 27 | import org.apache.commons.lang3.StringUtils; 28 | import org.apache.jmeter.protocol.http.control.AuthManager; 29 | import org.apache.jmeter.protocol.http.control.Authorization; 30 | import org.apache.jmeter.protocol.http.control.CookieManager; 31 | import org.apache.jmeter.protocol.http.control.Header; 32 | import org.apache.jmeter.protocol.http.control.HeaderManager; 33 | import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult; 34 | import org.apache.jmeter.protocol.http.util.HTTPArgument; 35 | import org.apache.jmeter.protocol.http.util.HTTPConstants; 36 | import org.apache.jmeter.protocol.http.util.HTTPFileArg; 37 | import org.apache.jmeter.testelement.property.JMeterProperty; 38 | import org.apache.jmeter.util.JMeterUtils; 39 | import org.eclipse.jetty.client.GZIPContentDecoder; 40 | import org.eclipse.jetty.client.HttpClient; 41 | import org.eclipse.jetty.client.HttpClientTransport; 42 | import org.eclipse.jetty.client.HttpProxy; 43 | import org.eclipse.jetty.client.HttpRequest; 44 | import org.eclipse.jetty.client.MultiplexConnectionPool; 45 | import org.eclipse.jetty.client.Origin.Address; 46 | import org.eclipse.jetty.client.api.AuthenticationStore; 47 | import org.eclipse.jetty.client.api.ContentResponse; 48 | import org.eclipse.jetty.client.api.Request.Content; 49 | import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; 50 | import org.eclipse.jetty.client.http.HttpClientConnectionFactory; 51 | import org.eclipse.jetty.client.util.AbstractAuthentication; 52 | import org.eclipse.jetty.client.util.BasicAuthentication; 53 | import org.eclipse.jetty.client.util.DigestAuthentication; 54 | import org.eclipse.jetty.client.util.FormRequestContent; 55 | import org.eclipse.jetty.client.util.MultiPartRequestContent; 56 | import org.eclipse.jetty.client.util.PathRequestContent; 57 | import org.eclipse.jetty.client.util.StringRequestContent; 58 | import org.eclipse.jetty.http.HttpField; 59 | import org.eclipse.jetty.http.HttpFields; 60 | import org.eclipse.jetty.http.HttpFields.Mutable; 61 | import org.eclipse.jetty.http.HttpHeader; 62 | import org.eclipse.jetty.http.HttpStatus; 63 | import org.eclipse.jetty.http.HttpVersion; 64 | import org.eclipse.jetty.http2.client.HTTP2Client; 65 | import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2; 66 | import org.eclipse.jetty.io.ClientConnectionFactory; 67 | import org.eclipse.jetty.io.ClientConnector; 68 | import org.eclipse.jetty.io.MappedByteBufferPool; 69 | import org.eclipse.jetty.util.Fields; 70 | import org.eclipse.jetty.util.ssl.SslContextFactory; 71 | import org.eclipse.jetty.util.thread.QueuedThreadPool; 72 | import org.slf4j.Logger; 73 | import org.slf4j.LoggerFactory; 74 | 75 | public class HTTP2JettyClient { 76 | 77 | private static final Logger LOG = LoggerFactory.getLogger(HTTP2JettyClient.class); 78 | private static final Set SUPPORTED_METHODS = new HashSet<>(Arrays 79 | .asList(HTTPConstants.GET, HTTPConstants.POST, HTTPConstants.PUT, HTTPConstants.PATCH, 80 | HTTPConstants.OPTIONS, HTTPConstants.DELETE)); 81 | private static final Set METHODS_WITH_BODY = new HashSet<>(Arrays 82 | .asList(HTTPConstants.POST, HTTPConstants.PUT, HTTPConstants.PATCH)); 83 | private static final boolean ADD_CONTENT_TYPE_TO_POST_IF_MISSING = JMeterUtils.getPropDefault( 84 | "http.post_add_content_type_if_missing", false); 85 | private static final Pattern PORT_PATTERN = Pattern.compile("\\d+"); 86 | private static final String MULTI_PART_SEPARATOR = "--"; 87 | private static final String LINE_SEPARATOR = "\r\n"; 88 | private static final String DEFAULT_FILE_MIME_TYPE = "application/octet-stream"; 89 | private int requestTimeout = 0; 90 | private int maxBufferSize = 2 * 1024 * 1024; 91 | private int maxThreads = 5; 92 | private int minThreads = 1; 93 | private int maxRequestsQueuedPerDestination = Short.MAX_VALUE; 94 | private int maxConnectionsPerDestination = 1; 95 | 96 | private int byteBufferPoolFactor = 4; 97 | private int maxConcurrentPushedStreams = 100; 98 | private int maxRequestsPerConnection = 100; 99 | 100 | private boolean strictEventOrdering = false; 101 | private boolean removeIdleDestinations = true; 102 | private int idleTimeout = 30000; 103 | private final HttpClient httpClient; 104 | private boolean http1UpgradeRequired; 105 | 106 | private MappedByteBufferPool bufferPool; 107 | 108 | private class CustomMappedByteBufferPool extends MappedByteBufferPool { 109 | 110 | CustomMappedByteBufferPool(int byteBufferPoolFactor) { 111 | super(byteBufferPoolFactor, -1); 112 | } 113 | 114 | @Override 115 | public ByteBuffer acquire(int size, boolean direct) { 116 | try { 117 | return super.acquire(size, direct); 118 | } catch (java.lang.OutOfMemoryError e) { 119 | return super.acquire(size, false); 120 | } 121 | } 122 | 123 | @Override 124 | protected void releaseMemory(boolean direct) { 125 | super.releaseMemory(direct); 126 | if (direct) { 127 | super.releaseMemory(false); // Force to free also the no direct 128 | } 129 | } 130 | } 131 | 132 | public HTTP2JettyClient(boolean http1UpgradeRequired, String name) { 133 | loadProperties(); 134 | 135 | bufferPool = new CustomMappedByteBufferPool(byteBufferPoolFactor); 136 | 137 | ClientConnector clientConnector = new ClientConnector(); 138 | clientConnector.setSelectors(1); 139 | clientConnector.setConnectBlocking(false); 140 | 141 | SslContextFactory.Client sslContextFactory = new JMeterJettySslContextFactory(); 142 | clientConnector.setSslContextFactory(sslContextFactory); 143 | 144 | QueuedThreadPool queuedThreadPool = new QueuedThreadPool(maxThreads); 145 | queuedThreadPool.setMinThreads(minThreads); 146 | queuedThreadPool.setName(name); 147 | 148 | clientConnector.setExecutor(queuedThreadPool); 149 | 150 | ClientConnectionFactory.Info http11 = HttpClientConnectionFactory.HTTP11; 151 | 152 | HTTP2Client http2Client = new HTTP2Client(clientConnector); 153 | ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2( 154 | http2Client); 155 | 156 | http2Client.setMaxConcurrentPushedStreams(maxConcurrentPushedStreams); 157 | http2Client.setUseALPN(true); 158 | http2Client.setProtocols(List.of("h2", "h2c", "http/1.1")); 159 | 160 | // If ALPN could not negotiate HTTP2, it tries in the order of protocols indicated 161 | HttpClientTransport transport = new HttpClientTransportDynamic( 162 | clientConnector, http11, http2); 163 | 164 | transport.setConnectionPoolFactory((destination) -> { 165 | MultiplexConnectionPool mcp = new MultiplexConnectionPool(destination, 166 | destination.getHttpClient().getMaxConnectionsPerDestination(), destination, 1); 167 | mcp.setMaxUsageCount(maxRequestsPerConnection); 168 | // TODO: This generate a problem with HTTP1 destinations, 169 | // we need to analyze and create a better way to do it. 170 | //mcp.setMaxMultiplex(maxRequestsPerConnection); 171 | return mcp; 172 | }); 173 | 174 | this.httpClient = new HttpClient(transport); 175 | this.httpClient.setUserAgentField(null); // No set UA header 176 | this.httpClient.setByteBufferPool(this.bufferPool); 177 | this.httpClient.setMaxRequestsQueuedPerDestination(maxRequestsQueuedPerDestination); 178 | this.httpClient.setMaxConnectionsPerDestination(maxConnectionsPerDestination); 179 | this.httpClient.setStrictEventOrdering(strictEventOrdering); 180 | this.httpClient.setRemoveIdleDestinations(removeIdleDestinations); 181 | this.httpClient.setIdleTimeout(idleTimeout); 182 | this.http1UpgradeRequired = http1UpgradeRequired; 183 | this.httpClient.setName(name); 184 | 185 | // TODO: Research 186 | //this.httpClient.setRequestBufferSize(); 187 | //clientConnector.setByteBufferPool(); 188 | } 189 | 190 | public HTTP2JettyClient() { 191 | this(false, "HttpClient"); 192 | } 193 | 194 | public void clearBufferPool() { 195 | bufferPool.clear(); 196 | } 197 | 198 | public MappedByteBufferPool getBufferPool() { 199 | return bufferPool; 200 | } 201 | 202 | public HttpClient getHttpClient() { 203 | return httpClient; 204 | } 205 | 206 | public int getMaxBufferSize() { 207 | return maxBufferSize; 208 | } 209 | 210 | public int getRequestTimeout() { 211 | return requestTimeout; 212 | } 213 | 214 | public void loadProperties() { 215 | requestTimeout = JMeterUtils.getPropDefault("HTTPSampler.response_timeout", 0); 216 | byteBufferPoolFactor = 217 | JMeterUtils.getPropDefault("httpJettyClient.byteBufferPoolFactor", byteBufferPoolFactor); 218 | maxBufferSize = 219 | Integer.parseInt(JMeterUtils.getPropDefault("httpJettyClient.maxBufferSize", 220 | String.valueOf(2 * 1024 * 1024))); 221 | minThreads = Integer 222 | .parseInt(JMeterUtils.getPropDefault("httpJettyClient.minThreads", 223 | String.valueOf(minThreads))); 224 | maxThreads = Integer 225 | .parseInt(JMeterUtils.getPropDefault("httpJettyClient.maxThreads", 226 | String.valueOf(maxThreads))); 227 | maxRequestsQueuedPerDestination = Integer 228 | .parseInt(JMeterUtils.getPropDefault("httpJettyClient.maxRequestsQueuedPerDestination", 229 | String.valueOf(maxRequestsQueuedPerDestination))); 230 | maxRequestsPerConnection = Integer 231 | .parseInt(JMeterUtils.getPropDefault("httpJettyClient.maxRequestsPerConnection", 232 | String.valueOf(maxRequestsPerConnection))); 233 | maxConcurrentPushedStreams = Integer 234 | .parseInt(JMeterUtils.getPropDefault("httpJettyClient.maxConcurrentPushedStreams", 235 | String.valueOf(maxConcurrentPushedStreams))); 236 | maxConnectionsPerDestination = 237 | Integer.parseInt( 238 | JMeterUtils.getPropDefault("httpJettyClient.maxConnectionsPerDestination", 239 | String.valueOf(maxConnectionsPerDestination))); 240 | strictEventOrdering = 241 | Boolean.parseBoolean(JMeterUtils.getPropDefault("httpJettyClient.strictEventOrdering", 242 | String.valueOf(strictEventOrdering))); 243 | removeIdleDestinations = 244 | Boolean.parseBoolean(JMeterUtils.getPropDefault("httpJettyClient.removeIdleDestinations", 245 | String.valueOf(removeIdleDestinations))); 246 | idleTimeout = 247 | Integer.parseInt(JMeterUtils.getPropDefault("httpJettyClient.idleTimeout", 248 | String.valueOf(idleTimeout))); 249 | } 250 | 251 | public void start() throws Exception { 252 | if (!httpClient.isStarted()) { 253 | httpClient.start(); 254 | httpClient.getContentDecoderFactories().clear(); // Clear default headers 255 | } 256 | } 257 | 258 | public void stop() throws Exception { 259 | httpClient.stop(); 260 | clearBufferPool(); 261 | } 262 | 263 | private void samplePrepareRequest(HttpRequest request, 264 | HTTP2Sampler sampler, 265 | HTTPSampleResult result) throws IOException { 266 | 267 | URL url = result.getURL(); 268 | setTimeouts(sampler, request); 269 | request.followRedirects(sampler.getAutoRedirects()); 270 | String method = result.getHTTPMethod(); 271 | request.method(method); 272 | setHeaders(request, url, sampler.getHeaderManager()); 273 | 274 | CookieManager cookieManager = sampler.getCookieManager(); 275 | if (cookieManager != null) { 276 | result.setCookies(buildCookies(request, url, cookieManager)); 277 | } 278 | 279 | if (!sampler.getProxyHost().isEmpty()) { 280 | setProxy(sampler.getProxyHost(), sampler.getProxyPortInt(), sampler.getProxyScheme()); 281 | } 282 | result.sampleStart(); 283 | 284 | setBody(request, sampler, result); 285 | 286 | } 287 | 288 | private boolean requestInCache(JettyCacheManager cacheManager, 289 | HttpRequest request) 290 | throws URISyntaxException, MalformedURLException { 291 | URL url = request.getURI().toURL(); 292 | String method = request.getMethod(); 293 | if (cacheManager != null) { 294 | cacheManager.setHeaders(url, request); 295 | if (HTTPConstants.GET.equalsIgnoreCase(method) && cacheManager.inCache(url, 296 | request.getHeaders())) { 297 | return true; 298 | } 299 | } 300 | return false; 301 | } 302 | 303 | private void postContentResponse(HTTP2Sampler sampler, HttpRequest request, 304 | HTTPSampleResult result, 305 | ContentResponse contentResponse, 306 | JettyCacheManager cacheManager) 307 | throws IOException { 308 | http1UpgradeRequired = contentResponse.getVersion() != HttpVersion.HTTP_2; 309 | result.setRequestHeaders(buildHeadersString(request.getHeaders())); 310 | setResultContentResponse(result, contentResponse, sampler); 311 | saveCookiesInCookieManager(contentResponse, request.getURI().toURL(), 312 | sampler.getCookieManager()); 313 | 314 | if (cacheManager != null) { 315 | cacheManager.saveDetails(contentResponse, result); 316 | } 317 | } 318 | 319 | public HttpRequest sampleAsync(HTTP2Sampler sampler, 320 | HTTPSampleResult result, 321 | HTTP2FutureResponseListener listener) 322 | throws Exception { 323 | errorWhenNotSupportedMethod(result.getHTTPMethod()); 324 | setAuthManager(sampler); 325 | HttpRequest request = buildRequest(result); 326 | samplePrepareRequest(request, sampler, result); 327 | listener.setRequest(request); 328 | return request; 329 | 330 | } 331 | 332 | private void errorWhenNotSupportedMethod(String method) throws UnsupportedOperationException { 333 | if (!isSupportedMethod(method)) { 334 | LOG.error(String.format("Method %s is not supported", 335 | method)); 336 | throw new UnsupportedOperationException(String.format("Method %s is not supported", 337 | method)); 338 | } 339 | } 340 | 341 | public HTTPSampleResult sample(HTTP2Sampler sampler, HTTPSampleResult result, 342 | boolean areFollowingRedirect, int depth) throws Exception { 343 | 344 | errorWhenNotSupportedMethod(result.getHTTPMethod()); 345 | setAuthManager(sampler); 346 | HttpRequest request = buildRequest(result); 347 | 348 | samplePrepareRequest(request, sampler, result); 349 | 350 | JettyCacheManager cacheManager = 351 | JettyCacheManager.fromCacheManager(sampler.getCacheManager()); 352 | if (requestInCache(cacheManager, request)) { 353 | return cacheManager.buildCachedSampleResult(result); 354 | } 355 | HTTP2FutureResponseListener listener = new HTTP2FutureResponseListener(maxBufferSize); 356 | ContentResponse contentResponse = send(request, listener); 357 | 358 | postContentResponse(sampler, request, result, contentResponse, cacheManager); 359 | result.setEndTime(listener.getResponseEnd()); 360 | 361 | return sampler.resultProcessing(areFollowingRedirect, depth, result); 362 | } 363 | 364 | public HTTPSampleResult sampleFromListener(HTTP2Sampler sampler, HTTPSampleResult result, 365 | boolean areFollowingRedirect, int depth, 366 | HTTP2FutureResponseListener listener 367 | ) throws Exception { 368 | 369 | ContentResponse contentResponse = getContent(listener); 370 | JettyCacheManager cacheManager = 371 | JettyCacheManager.fromCacheManager(sampler.getCacheManager()); 372 | HttpRequest request = listener.getRequest(); 373 | postContentResponse(sampler, request, result, contentResponse, cacheManager); 374 | result.setEndTime(listener.getResponseEnd()); 375 | 376 | return sampler.resultProcessing(areFollowingRedirect, depth, result); 377 | } 378 | 379 | public ContentResponse send(HttpRequest request, HTTP2FutureResponseListener listener) 380 | throws InterruptedException, 381 | TimeoutException, ExecutionException { 382 | if (request.getHeaders().contains("Accept-Encoding") && 383 | request.getHeaders().get("Accept-Encoding").contains("gzip")) { 384 | httpClient.getContentDecoderFactories() 385 | .add(new GZIPContentDecoder.Factory(httpClient.getByteBufferPool())); 386 | } else { 387 | httpClient.getContentDecoderFactories().clear(); 388 | } 389 | if (request.getHeaders() != null) { 390 | HttpFields hm = request.getHeaders(); 391 | String ae = hm.get("Accept-Encoding"); 392 | if (ae != null && ae.contains("br")) { 393 | Mutable headers = ((Mutable) request.getHeaders()); 394 | headers.remove("Accept-Encoding"); 395 | String acceptEncoding = ae; 396 | acceptEncoding = acceptEncoding.replace(", br", "").replace("br", ""); 397 | HttpField aef = new HttpField("Accept-Encoding", acceptEncoding); 398 | request.addHeader(aef); 399 | } 400 | } 401 | request.send(listener); 402 | return getContent(listener); 403 | } 404 | 405 | public ContentResponse getContent(HTTP2FutureResponseListener listener) 406 | throws InterruptedException, TimeoutException, ExecutionException { 407 | long getStart = System.currentTimeMillis(); 408 | try { 409 | if (requestTimeout > 0) { 410 | int extraTime = 2000; 411 | return listener.get(requestTimeout + extraTime, TimeUnit.MILLISECONDS); 412 | } else { 413 | return listener.get(); 414 | } 415 | } catch (TimeoutException e) { 416 | long endGet = System.currentTimeMillis(); 417 | throw new TimeoutException("The request took more than " + (endGet - getStart) 418 | + " milliseconds to complete"); 419 | } catch (ExecutionException e) { 420 | if (e.getCause() != null && e.getCause() instanceof TimeoutException) { 421 | throw (TimeoutException) e.getCause(); 422 | } else if (e.getCause() != null && e.getCause() instanceof IllegalArgumentException) { 423 | throw (IllegalArgumentException) e.getCause(); 424 | } 425 | throw e; 426 | } 427 | } 428 | 429 | private void setAuthManager(HTTP2Sampler sampler) { 430 | AuthManager authManager = sampler.getAuthManager(); 431 | if (authManager != null) { 432 | StreamSupport.stream(authManager.getAuthObjects().spliterator(), false) 433 | .map(j -> (Authorization) j.getObjectValue()) 434 | .filter(auth -> isSupportedMechanism(auth) && !StringUtils.isEmpty(auth.getURL())) 435 | .forEach(this::addAuthenticationToJettyClient); 436 | } 437 | } 438 | 439 | private boolean isSupportedMechanism(Authorization auth) { 440 | String authName = auth.getMechanism().name(); 441 | return authName.equals(AuthManager.Mechanism.BASIC.name()) 442 | || authName.equals(AuthManager.Mechanism.DIGEST.name()); 443 | } 444 | 445 | private void addAuthenticationToJettyClient(Authorization auth) { 446 | AuthenticationStore authenticationStore = httpClient.getAuthenticationStore(); 447 | String authName = auth.getMechanism().name(); 448 | if (authName.equals(AuthManager.Mechanism.BASIC.name()) && JMeterUtils.getPropDefault( 449 | "httpJettyClient.auth.preemptive", false)) { 450 | authenticationStore.addAuthenticationResult( 451 | new BasicAuthentication.BasicResult(URI.create(auth.getURL()), auth.getUser(), 452 | auth.getPass())); 453 | } else { 454 | AbstractAuthentication authentication = 455 | authName.equals(AuthManager.Mechanism.BASIC.name()) ? new BasicAuthentication( 456 | URI.create(auth.getURL()), auth.getRealm(), auth.getUser(), auth.getPass()) 457 | : 458 | new DigestAuthentication(URI.create(auth.getURL()), auth.getRealm(), 459 | auth.getUser(), 460 | auth.getPass()); 461 | authenticationStore.addAuthentication(authentication); 462 | } 463 | } 464 | 465 | private HttpRequest buildRequest(HTTPSampleResult result) throws URISyntaxException, 466 | IllegalArgumentException { 467 | URL url = result.getURL(); 468 | HttpRequest request = (HttpRequest) httpClient.newRequest(url.toURI()); 469 | request.onRequestBegin(r -> result.connectEnd()); 470 | request.onRequestContent( 471 | (r, c) -> result.setSentBytes(result.getSentBytes() + c.limit())); 472 | request.onResponseBegin(r -> result.latencyEnd()); 473 | return request; 474 | } 475 | 476 | private void setTimeouts(HTTP2Sampler sampler, HttpRequest request) { 477 | if (sampler.getConnectTimeout() > 0) { 478 | httpClient.setConnectTimeout(sampler.getConnectTimeout()); 479 | } 480 | if (sampler.getResponseTimeout() > 0) { 481 | request.timeout(sampler.getResponseTimeout(), TimeUnit.MILLISECONDS); 482 | } else if (requestTimeout > 0) { 483 | request.timeout(requestTimeout, TimeUnit.MILLISECONDS); 484 | } 485 | } 486 | 487 | private void setHeaders(HttpRequest request, URL url, HeaderManager headerManager) { 488 | if (headerManager != null) { 489 | StreamSupport.stream(headerManager.getHeaders().spliterator(), false) 490 | .map(prop -> (Header) prop.getObjectValue()) 491 | .filter(header -> (!header.getName().isEmpty()) && (!HTTPConstants.HEADER_CONTENT_LENGTH 492 | .equalsIgnoreCase(header.getName()))) 493 | .forEach(header -> request.addHeader(createJettyHeader(header, url))); 494 | } 495 | //Should review this implementation for solely HTTP1 connections. 496 | //Everytime a HTTP1 request is sent it will include headers for upgrade. 497 | if (http1UpgradeRequired) { 498 | Mutable headers = ((Mutable) request.getHeaders()); 499 | addHeaderIfMissing(HttpHeader.UPGRADE, "h2c", headers); 500 | addHeaderIfMissing(HttpHeader.HTTP2_SETTINGS, "", headers); 501 | addHeaderIfMissing(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings", headers); 502 | } 503 | } 504 | 505 | private void addHeaderIfMissing(HttpHeader header, String value, Mutable headers) { 506 | if (!headers.contains(header)) { 507 | headers.put(header, value); 508 | } 509 | } 510 | 511 | private HttpField createJettyHeader(Header header, URL url) { 512 | String headerName = header.getName(); 513 | String headerValue = header.getValue(); 514 | if (HTTPConstants.HEADER_HOST.equalsIgnoreCase(headerName)) { 515 | int port = getPortFromHostHeader(headerValue, url.getPort()); 516 | // remove any port specification 517 | headerValue = headerValue.replaceFirst(":\\d+$", ""); 518 | if (port != -1 && port == url.getDefaultPort()) { 519 | // no need to specify the port if it is the default 520 | port = -1; 521 | } 522 | return port == -1 ? new HttpField(HTTPConstants.HEADER_HOST, headerValue) 523 | : new HttpField(HTTPConstants.HEADER_HOST, headerValue + ":" + port); 524 | } else { 525 | return new HttpField(headerName, headerValue); 526 | } 527 | } 528 | 529 | private int getPortFromHostHeader(String hostHeaderValue, int defaultValue) { 530 | String[] hostParts = hostHeaderValue.split(":"); 531 | if (hostParts.length > 1) { 532 | String portString = hostParts[hostParts.length - 1]; 533 | if (PORT_PATTERN.matcher(portString).matches()) { 534 | return Integer.parseInt(portString); 535 | } 536 | } 537 | return defaultValue; 538 | } 539 | 540 | private String buildCookies(HttpRequest request, URL url, CookieManager cookieManager) { 541 | if (cookieManager == null) { 542 | return null; 543 | } 544 | String cookieString = cookieManager.getCookieHeaderForURL(url); 545 | if (cookieString != null) { 546 | HttpField cookieHeader = new HttpField(HTTPConstants.HEADER_COOKIE, cookieString); 547 | request.addHeader(cookieHeader); 548 | } 549 | return cookieString; 550 | } 551 | 552 | private void setProxy(String host, int port, String protocol) { 553 | HttpProxy proxy = 554 | new HttpProxy(new Address(host, port), HTTPConstants.PROTOCOL_HTTPS.equals(protocol)); 555 | // It is not allowed to change the running proxy. 556 | // Only the first assigned is used. 557 | if (httpClient.getProxyConfiguration().getProxies().isEmpty()) { 558 | httpClient.getProxyConfiguration().getProxies().add(proxy); 559 | } 560 | } 561 | 562 | private void setBody(HttpRequest request, HTTP2Sampler sampler, HTTPSampleResult result) 563 | throws IOException { 564 | String contentEncoding = sampler.getContentEncoding(); 565 | String contentTypeHeader = 566 | request.getHeaders() != null ? request.getHeaders().get(HTTPConstants.HEADER_CONTENT_TYPE) 567 | : null; 568 | boolean hasContentTypeHeader = contentTypeHeader != null && contentTypeHeader.isEmpty(); 569 | StringBuilder postBody = new StringBuilder(); 570 | if (sampler.getUseMultipart()) { 571 | MultiPartRequestContent multipartEntityBuilder = new MultiPartRequestContent(); 572 | String boundary = extractMultipartBoundary(multipartEntityBuilder); 573 | Charset contentCharset = 574 | buildCharsetOrDefault(contentEncoding, StandardCharsets.US_ASCII); 575 | for (JMeterProperty jMeterProperty : sampler.getArguments()) { 576 | HTTPArgument arg = (HTTPArgument) jMeterProperty.getObjectValue(); 577 | String parameterName = arg.getName(); 578 | if (!arg.isSkippable(parameterName)) { 579 | postBody.append( 580 | buildArgumentPartRequestBody(arg, contentCharset, contentEncoding, boundary)); 581 | multipartEntityBuilder.addFieldPart(parameterName, 582 | new StringRequestContent(contentTypeHeader, arg.getValue(), contentCharset), null); 583 | } 584 | } 585 | Content[] fileBodies = new PathRequestContent[sampler 586 | .getHTTPFiles().length]; 587 | // Cannot retrieve parts once added 588 | for (int i = 0; i < sampler.getHTTPFiles().length; i++) { 589 | final HTTPFileArg file = sampler.getHTTPFiles()[i]; 590 | if (StringUtils.isBlank(file.getParamName())) { 591 | throw new IllegalStateException("Param name is blank"); 592 | } 593 | String mimeTypeFile = extractFileMimeType(hasContentTypeHeader, file); 594 | fileBodies[i] = new PathRequestContent(mimeTypeFile, Path.of(file.getPath())); 595 | String fileName = Paths.get((file.getPath())).getFileName().toString(); 596 | postBody.append(buildFilePartRequestBody(file, fileName, boundary)); 597 | multipartEntityBuilder.addFilePart(file.getParamName(), fileName, fileBodies[i], 598 | null); 599 | } 600 | postBody.append(MULTI_PART_SEPARATOR).append(boundary).append(MULTI_PART_SEPARATOR) 601 | .append(LINE_SEPARATOR); 602 | multipartEntityBuilder.close(); 603 | request.body(multipartEntityBuilder); 604 | } else { 605 | if (!sampler.hasArguments() && sampler.getSendFileAsPostBody()) { 606 | // Only one File support in not multipart scenario 607 | final HTTPFileArg file = sampler.getHTTPFiles()[0]; 608 | if (sampler.getHTTPFiles().length > 1) { 609 | LOG.info("Send multiples files is not currently supported, only first file will be " 610 | + "sending"); 611 | } 612 | 613 | String mimeTypeFile = extractFileMimeType(hasContentTypeHeader, file); 614 | if (!DEFAULT_FILE_MIME_TYPE.equals(mimeTypeFile)) { 615 | request.addHeader(new HttpField(HTTPConstants.HEADER_CONTENT_TYPE, mimeTypeFile)); 616 | } 617 | Content requestContent = new PathRequestContent(mimeTypeFile, Path.of(file.getPath())); 618 | request.body(requestContent); 619 | postBody.append(""); 620 | } else { 621 | if (!hasContentTypeHeader && ADD_CONTENT_TYPE_TO_POST_IF_MISSING) { 622 | request.addHeader(new HttpField(HTTPConstants.HEADER_CONTENT_TYPE, 623 | HTTPConstants.APPLICATION_X_WWW_FORM_URLENCODED)); 624 | } 625 | Charset contentCharset = buildCharsetOrDefault(contentEncoding, StandardCharsets.UTF_8); 626 | if (sampler.getSendParameterValuesAsPostBody()) { 627 | for (JMeterProperty jMeterProperty : sampler.getArguments()) { 628 | HTTPArgument arg = (HTTPArgument) jMeterProperty.getObjectValue(); 629 | postBody.append(arg.getEncodedValue(contentCharset.name())); 630 | } 631 | Content requestContent = 632 | new StringRequestContent(contentTypeHeader, postBody.toString(), 633 | contentCharset); 634 | request.body(requestContent); 635 | } else if (isMethodWithBody(sampler.getMethod())) { 636 | Fields fields = new Fields(); 637 | for (JMeterProperty p : sampler.getArguments()) { 638 | HTTPArgument arg = (HTTPArgument) p.getObjectValue(); 639 | String parameterName = arg.getName(); 640 | if (!arg.isSkippable(parameterName)) { 641 | String parameterValue = arg.getValue(); 642 | if (!arg.isAlwaysEncoded()) { 643 | // The FormRequestContent always urlencodes both name and value, in this case the 644 | // value is already encoded by the user so is needed to decode the value now, so 645 | // that when the httpclient encodes it, we end up with the same value as the user 646 | // had entered. 647 | parameterName = URLDecoder.decode(parameterName, contentCharset.name()); 648 | parameterValue = URLDecoder.decode(parameterValue, contentCharset.name()); 649 | } 650 | fields.add(parameterName, parameterValue); 651 | } 652 | } 653 | postBody.append(FormRequestContent.convert(fields)); 654 | request.body(new FormRequestContent(fields, contentCharset)); 655 | } 656 | } 657 | } 658 | result.setQueryString(postBody.toString()); 659 | } 660 | 661 | private String extractMultipartBoundary(MultiPartRequestContent multipartEntityBuilder) { 662 | String contentType = multipartEntityBuilder.getContentType(); 663 | String boundaryParam = contentType.substring(contentType.indexOf(" ") + 1); 664 | return boundaryParam.substring(boundaryParam.indexOf("=") + 1); 665 | } 666 | 667 | private Charset buildCharsetOrDefault(String contentEncoding, Charset defaultCharset) { 668 | return !contentEncoding.isEmpty() ? Charset.forName(contentEncoding) : defaultCharset; 669 | } 670 | 671 | private String buildArgumentPartRequestBody(HTTPArgument arg, Charset contentCharset, 672 | String contentEncoding, String boundary) 673 | throws UnsupportedEncodingException { 674 | String disposition = "name=\"" + arg.getEncodedName() + "\""; 675 | String contentType = arg.getContentType() + "; charset=" + contentCharset.name(); 676 | String encoding = StringUtils.isNotBlank(contentEncoding) ? contentEncoding : "8bit"; 677 | return buildPartBody(boundary, disposition, contentType, encoding, 678 | arg.getEncodedValue(contentCharset.name())); 679 | } 680 | 681 | private String buildPartBody(String boundary, String disposition, String contentType, 682 | String encoding, String value) { 683 | return MULTI_PART_SEPARATOR + boundary + LINE_SEPARATOR + 684 | HttpFields.build() 685 | .add("Content-Disposition", "form-data; " + disposition) 686 | .add(HttpHeader.CONTENT_TYPE.toString(), contentType) 687 | .add("Content-Transfer-Encoding", encoding) 688 | .toString() 689 | + value + LINE_SEPARATOR; 690 | } 691 | 692 | private String buildFilePartRequestBody(HTTPFileArg file, String fileName, String boundary) { 693 | String disposition = "name=\"" + file.getParamName() + "\"; filename=\"" + fileName + "\""; 694 | return buildPartBody(boundary, disposition, file.getMimeType(), "binary", 695 | ""); 696 | } 697 | 698 | private String extractFileMimeType(boolean hasContentTypeHeader, HTTPFileArg file) { 699 | String ret = null; 700 | if (!hasContentTypeHeader) { 701 | if (file.getMimeType() != null && !file.getMimeType().isEmpty()) { 702 | ret = file.getMimeType(); 703 | } else if (ADD_CONTENT_TYPE_TO_POST_IF_MISSING) { 704 | ret = HTTPConstants.APPLICATION_X_WWW_FORM_URLENCODED; 705 | } 706 | } 707 | return ret == null ? DEFAULT_FILE_MIME_TYPE : ret; 708 | } 709 | 710 | private boolean isMethodWithBody(String method) { 711 | return METHODS_WITH_BODY.contains(method); 712 | } 713 | 714 | private boolean isSupportedMethod(String method) { 715 | return SUPPORTED_METHODS.contains(method); 716 | } 717 | 718 | private String buildHeadersString(HttpFields headers) { 719 | if (headers == null) { 720 | return ""; 721 | } else { 722 | String ret = HttpFields.build(headers).remove(HTTPConstants.HEADER_COOKIE).toString() 723 | .replace("\r\n", "\n"); 724 | return ret.substring(0, 725 | ret.length() - 1); // removing final separator not included in jmeter headers 726 | } 727 | } 728 | 729 | private void setResultContentResponse(HTTPSampleResult result, ContentResponse contentResponse, 730 | HTTP2Sampler sampler) throws IOException { 731 | String contentType = contentResponse.getHeaders() != null 732 | ? contentResponse.getHeaders().get(HTTPConstants.HEADER_CONTENT_TYPE) 733 | : null; 734 | if (contentType != null) { 735 | result.setContentType(contentType); 736 | result.setEncodingAndType(contentType); 737 | } 738 | 739 | // When a resource is cached, the sample result is empty 740 | InputStream inputStream = new ByteArrayInputStream(contentResponse.getContent()); 741 | result.setResponseData(sampler.readResponse(result, inputStream, 742 | contentResponse.getContent().length)); 743 | 744 | if (result.getEndTime() == 0) { 745 | result.sampleEnd(); 746 | } else { 747 | result.setEndTime(result.currentTimeInMillis()); 748 | } 749 | 750 | result.setResponseCode(String.valueOf(contentResponse.getStatus())); 751 | String responseMessage = contentResponse.getReason() != null ? contentResponse.getReason() 752 | : HttpStatus.getMessage(contentResponse.getStatus()); 753 | result.setResponseMessage(responseMessage); 754 | result.setSuccessful( 755 | contentResponse.getStatus() >= 200 && contentResponse.getStatus() <= 399); 756 | result.setResponseHeaders(extractResponseHeaders(contentResponse, responseMessage)); 757 | if (result.isRedirect()) { 758 | result.setRedirectLocation(extractRedirectLocation(contentResponse)); 759 | } 760 | 761 | if (sampler.getAutoRedirects()) { 762 | result.setURL(contentResponse.getRequest().getURI().toURL()); 763 | } 764 | 765 | long headerBytes = 766 | (long) result.getResponseHeaders().length() // condensed length (without \r) 767 | + (long) contentResponse.getHeaders().asString().length() // Add \r for each header 768 | + 1L // Add \r for initial header 769 | + 2L; // final \r\n before data 770 | result.setHeadersSize((int) headerBytes); 771 | } 772 | 773 | private String extractResponseHeaders(ContentResponse contentResponse, 774 | String message) { 775 | return contentResponse.getVersion() + " " + contentResponse.getStatus() + " " + message + "\n" 776 | + buildHeadersString(contentResponse.getHeaders()); 777 | } 778 | 779 | private String extractRedirectLocation(ContentResponse contentResponse) { 780 | String redirectLocation = contentResponse.getHeaders() != null 781 | ? contentResponse.getHeaders().get(HTTPConstants.HEADER_LOCATION) 782 | : null; 783 | if (redirectLocation == null) { 784 | throw new IllegalArgumentException("Missing location header in redirect"); 785 | } 786 | return redirectLocation; 787 | } 788 | 789 | private void saveCookiesInCookieManager(ContentResponse response, URL url, 790 | CookieManager cookieManager) { 791 | if (cookieManager == null) { 792 | return; 793 | } 794 | for (HttpField field : response.getHeaders()) { 795 | if (field.is(HTTPConstants.HEADER_SET_COOKIE)) { 796 | String cookieHeader = field.getValue(); 797 | if (cookieHeader != null) { 798 | cookieManager.addCookieFromHeader(cookieHeader, url); 799 | } 800 | } 801 | } 802 | } 803 | 804 | public void clearCookies() { 805 | httpClient.getCookieStore().removeAll(); 806 | } 807 | 808 | public void clearAuthenticationResults() { 809 | httpClient.getAuthenticationStore().clearAuthenticationResults(); 810 | } 811 | 812 | public String dump() { 813 | return httpClient.dump(); 814 | } 815 | 816 | } 817 | 818 | 819 | 820 | --------------------------------------------------------------------------------