java-topology/test/jdk/java/net/httpclient/http3/H3HeadersEncoding.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

312 lines
13 KiB
Java

/*
* Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.common.HttpServerAdapters
* jdk.test.lib.net.SimpleSSLContext
* @compile ../ReferenceTracker.java
* @run junit/othervm -Djdk.httpclient.qpack.encoderTableCapacityLimit=4096
* -Djdk.httpclient.qpack.decoderMaxTableCapacity=4096
* -Dhttp3.test.server.encoderAllowedHeaders=*
* -Dhttp3.test.server.decoderMaxTableCapacity=4096
* -Dhttp3.test.server.encoderTableCapacityLimit=4096
* -Djdk.internal.httpclient.qpack.log.level=NORMAL
* H3HeadersEncoding
* @summary this test verifies that when QPACK dynamic table is enabled multiple
* random headers can be encoded/decoded correctly
*/
import jdk.test.lib.RandomFactory;
import jdk.test.lib.net.SimpleSSLContext;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpOption.Http3DiscoveryMode;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static java.net.http.HttpClient.Version.HTTP_3;
import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY;
import static java.net.http.HttpOption.H3_DISCOVERY;
import static jdk.httpclient.test.lib.common.HttpServerAdapters.*;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
public class H3HeadersEncoding {
private static final int REQUESTS_COUNT = 500;
private static final int HEADERS_PER_REQUEST = 20;
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
private static HttpTestServer http3TestServer;
private static HeadersHandler serverHeadersHandler;
private static String http3URI;
@BeforeAll
public static void setup() throws Exception {
System.out.println("Creating servers");
http3TestServer = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext);
serverHeadersHandler = new HeadersHandler();
http3TestServer.addHandler(serverHeadersHandler, "/http3/headers");
http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/headers";
http3TestServer.start();
}
@AfterAll
public static void tearDown() {
http3TestServer.stop();
}
@Test
public void serialRequests() throws Exception {
try (HttpClient client = newClient()) {
for (int i = 0; i < REQUESTS_COUNT; i++) {
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(URI.create(http3URI))
.version(HTTP_3)
.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY);
List<TestHeader> rndHeaders = TestHeader.randomHeaders(HEADERS_PER_REQUEST);
String[] requestHeaders = rndHeaders.stream()
.flatMap(th -> Stream.of(th.name(), th.value()))
.toArray(String[]::new);
requestBuilder.headers(requestHeaders);
// Send client request headers in request body for further check
// on the server handler side
requestBuilder.POST(HttpRequest.BodyPublishers.ofString(
TestHeader.headersToBodyContent(rndHeaders)));
HttpResponse<String> response =
client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
// Headers received by the client
var serverHeadersClientSide = TestHeader.fromHttpHeaders(response.headers());
// Headers sent by the server handler
var serverHeadersServerSide = TestHeader.bodyContentToHeaders(response.body());
// Check that all headers that server sent are received on client side
checkHeaders(serverHeadersServerSide, serverHeadersClientSide, "client");
}
}
}
@Test
public void asyncRequests() throws Exception {
try (HttpClient client = newClient()) {
ArrayList<List<TestHeader>> requestsHeaders = new ArrayList<>(REQUESTS_COUNT);
ArrayList<HttpRequest> requests = new ArrayList<>(REQUESTS_COUNT);
CopyOnWriteArrayList<CompletableFuture<HttpResponse<String>>> futureReplies
= new CopyOnWriteArrayList<>();
// Prepare all requests first
for (int i = 0; i < REQUESTS_COUNT; i++) {
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(URI.create(http3URI))
.version(HTTP_3)
.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY);
List<TestHeader> rndHeaders = TestHeader.randomHeaders(HEADERS_PER_REQUEST);
String[] requestHeaders = rndHeaders.stream()
.flatMap(th -> Stream.of(th.name(), th.value()))
.toArray(String[]::new);
requestBuilder.headers(requestHeaders);
requestsHeaders.add(i, rndHeaders);
// Send client request headers in request body for further check
// on the server handler side
requestBuilder.POST(HttpRequest.BodyPublishers.ofString(
TestHeader.headersToBodyContent(rndHeaders)));
requests.add(i, requestBuilder.build());
}
// Send async request
for (int i = 0; i < REQUESTS_COUNT; i++) {
CompletableFuture<HttpResponse<String>> cf =
client.sendAsync(requests.get(i),
HttpResponse.BodyHandlers.ofString());
futureReplies.add(i, cf);
}
// Join on all CF responses
for (int i = 0; i < REQUESTS_COUNT; i++) {
futureReplies.get(i).join();
}
// Check the responses
for (int i = 0; i < REQUESTS_COUNT; i++) {
HttpResponse<String> reply = futureReplies.get(i).get();
// Headers received by the client
var serverHeadersClientSide = TestHeader.fromHttpHeaders(reply.headers());
// Headers sent by the server handler
var serverHeadersServerSide = TestHeader.bodyContentToHeaders(reply.body());
// Check that all headers that server sent are received on client side
checkHeaders(serverHeadersServerSide, serverHeadersClientSide, "client");
}
}
}
private static void checkHeaders(List<TestHeader> mustPresent,
List<TestHeader> allHeaders,
String sideDescription) {
List<TestHeader> notFound = new ArrayList<>();
for (var header : mustPresent) {
if (!allHeaders.contains(header)) {
notFound.add(header);
}
}
if (!notFound.isEmpty()) {
System.err.println("The following headers was not found on "
+ sideDescription + " side: " + notFound);
throw new RuntimeException("Headers not found: " + notFound);
}
}
HttpClient newClient() {
var builder = createClientBuilderForH3()
.sslContext(sslContext)
.version(HTTP_3)
.proxy(HttpClient.Builder.NO_PROXY);
return builder.build();
}
private static final Random RND = RandomFactory.getRandom();
record TestHeader(String name, String value) {
static TestHeader randomHeader() {
// It is better to have same id generated two or more
// times during the test run, therefore the range below
int headerId = RND.nextInt(10, 10 + HEADERS_PER_REQUEST * 3);
return new TestHeader("test_header" + headerId, "TestValue");
}
static List<TestHeader> randomHeaders(int count) {
return IntStream
.range(0, count)
.boxed()
.map(ign -> TestHeader.randomHeader())
.toList();
}
static List<TestHeader> fromHttpHeaders(HttpHeaders httpHeaders) {
var headersMap = httpHeaders.map();
return fromHeadersEntrySet(headersMap.entrySet());
}
static List<TestHeader> fromTestHttpHeaders(HttpTestRequestHeaders requestHeaders) {
var headersSet = requestHeaders.entrySet();
return fromHeadersEntrySet(headersSet);
}
private static List<TestHeader> fromHeadersEntrySet(Set<Map.Entry<String, List<String>>> entrySet) {
return entrySet.stream()
.flatMap(entry -> {
var name = entry.getKey();
return entry.getValue()
.stream()
.map(value -> new TestHeader(name, value));
}).toList();
}
public static String headersToBodyContent(List<TestHeader> rndHeaders) {
return rndHeaders.stream()
.map(TestHeader::toString)
.collect(Collectors.joining(System.lineSeparator()));
}
public static List<TestHeader> bodyContentToHeaders(String bodyContent) {
return Arrays.stream(bodyContent.split(System.lineSeparator()))
.filter(Predicate.not(String::isBlank))
.map(String::strip)
.map(TestHeader::fromBodyHeaderLine)
.toList();
}
public static TestHeader fromBodyHeaderLine(String headerLine) {
String[] parts = headerLine.split(":");
if (parts.length != 2) {
throw new RuntimeException("Internal test error");
}
return new TestHeader(parts[0], parts[1]);
}
public String toString() {
return name + ":" + value;
}
}
private static class HeadersHandler implements HttpTestHandler {
@Override
public void handle(HttpTestExchange t) throws IOException {
var clientHeadersServerSide = TestHeader.fromTestHttpHeaders(t.getRequestHeaders());
String requestBody;
try (InputStream is = t.getRequestBody()) {
byte[] body = is.readAllBytes();
requestBody = new String(body);
}
var clientHeadersClientSide = TestHeader.bodyContentToHeaders(requestBody);
// Check that all headers that client sent are received on tne server side
checkHeaders(clientHeadersClientSide, clientHeadersServerSide,
"server handler");
// Response back with a set of random headers
var responseHeaders = t.getResponseHeaders();
List<TestHeader> serverResp = new ArrayList<>();
for (TestHeader h : TestHeader.randomHeaders(HEADERS_PER_REQUEST)) {
serverResp.add(h);
responseHeaders.addHeader(h.name(), h.value());
}
String responseBody = TestHeader.headersToBodyContent(serverResp);
try (OutputStream os = t.getResponseBody()) {
byte[] responseBodyBytes = responseBody.getBytes();
t.sendResponseHeaders(200, responseBodyBytes.length);
if (!t.getRequestMethod().equals("HEAD")) {
os.write(responseBodyBytes);
}
}
}
}
}