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.
This commit is contained in:
commit
0a580b313d
70422 changed files with 17213626 additions and 0 deletions
334
test/jdk/java/net/httpclient/http2/BadHeadersTest.java
Normal file
334
test/jdk/java/net/httpclient/http2/BadHeadersTest.java
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
/*
|
||||
* Copyright (c) 2018, 2025, 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
|
||||
* @bug 8303965 8354276
|
||||
* @summary This test verifies the behaviour of the HttpClient when presented
|
||||
* with a HEADERS frame followed by CONTINUATION frames, and when presented
|
||||
* with bad header fields.
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext
|
||||
* @run junit/othervm -Djdk.internal.httpclient.debug=true BadHeadersTest
|
||||
*/
|
||||
|
||||
import jdk.internal.net.http.common.HttpHeadersBuilder;
|
||||
import jdk.internal.net.http.frame.ContinuationFrame;
|
||||
import jdk.internal.net.http.frame.HeaderFrame;
|
||||
import jdk.internal.net.http.frame.HeadersFrame;
|
||||
import jdk.internal.net.http.frame.Http2Frame;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
import jdk.httpclient.test.lib.http2.BodyOutputStream;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
|
||||
import static java.util.List.of;
|
||||
import static java.util.Map.entry;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
// Code copied from ContinuationFrameTest
|
||||
public class BadHeadersTest {
|
||||
|
||||
private static final List<List<Entry<String, String>>> BAD_HEADERS = of(
|
||||
of(entry(":status", "200"), entry(":hello", "GET")), // Unknown pseudo-header
|
||||
of(entry(":status", "200"), entry("hell o", "value")), // Space in the name
|
||||
of(entry(":status", "200"), entry("hello", "line1\r\n line2\r\n")), // Multiline value
|
||||
of(entry(":status", "200"), entry("hello", "DE" + ((char) 0x7F) + "L")), // Bad byte in value
|
||||
of(entry(":status", "200"), entry("connection", "close")), // Prohibited connection-specific header
|
||||
of(entry(":status", "200"), entry(":scheme", "https")), // Request pseudo-header in response
|
||||
of(entry("hello", "world!"), entry(":status", "200")) // Pseudo header is not the first one
|
||||
);
|
||||
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
private static Http2TestServer http2TestServer; // HTTP/2 ( h2c )
|
||||
private static Http2TestServer https2TestServer; // HTTP/2 ( h2 )
|
||||
private static String http2URI;
|
||||
private static String https2URI;
|
||||
|
||||
/**
|
||||
* A function that returns a list of 1) one HEADERS frame ( with an empty
|
||||
* payload ), and 2) a CONTINUATION frame with the actual headers.
|
||||
*/
|
||||
static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> oneContinuation =
|
||||
(Integer streamid, List<ByteBuffer> encodedHeaders) -> {
|
||||
List<ByteBuffer> empty = of(ByteBuffer.wrap(new byte[0]));
|
||||
HeadersFrame hf = new HeadersFrame(streamid, 0, empty);
|
||||
ContinuationFrame cf = new ContinuationFrame(streamid,
|
||||
HeaderFrame.END_HEADERS,
|
||||
encodedHeaders);
|
||||
return of(hf, cf);
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that returns a list of one HEADERS frame followed by a number of
|
||||
* CONTINUATION frames. Each frame contains just a single byte of payload.
|
||||
*/
|
||||
static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> byteAtATime =
|
||||
(Integer streamid, List<ByteBuffer> encodedHeaders) -> {
|
||||
assert encodedHeaders.get(0).hasRemaining();
|
||||
List<Http2Frame> frames = new ArrayList<>();
|
||||
ByteBuffer hb = ByteBuffer.wrap(new byte[] {encodedHeaders.get(0).get()});
|
||||
HeadersFrame hf = new HeadersFrame(streamid, 0, hb);
|
||||
frames.add(hf);
|
||||
for (ByteBuffer bb : encodedHeaders) {
|
||||
while (bb.hasRemaining()) {
|
||||
List<ByteBuffer> data = of(ByteBuffer.wrap(new byte[] {bb.get()}));
|
||||
ContinuationFrame cf = new ContinuationFrame(streamid, 0, data);
|
||||
frames.add(cf);
|
||||
}
|
||||
}
|
||||
frames.get(frames.size() - 1).setFlag(HeaderFrame.END_HEADERS);
|
||||
return frames;
|
||||
};
|
||||
|
||||
static Object[][] variants() {
|
||||
return new Object[][] {
|
||||
{ http2URI, false, oneContinuation },
|
||||
{ https2URI, false, oneContinuation },
|
||||
{ http2URI, true, oneContinuation },
|
||||
{ https2URI, true, oneContinuation },
|
||||
|
||||
{ http2URI, false, byteAtATime },
|
||||
{ https2URI, false, byteAtATime },
|
||||
{ http2URI, true, byteAtATime },
|
||||
{ https2URI, true, byteAtATime },
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("variants")
|
||||
void test(String uri,
|
||||
boolean sameClient,
|
||||
BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)
|
||||
throws Exception
|
||||
{
|
||||
CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier);
|
||||
|
||||
HttpClient client = null;
|
||||
for (int i=0; i< BAD_HEADERS.size(); i++) {
|
||||
if (!sameClient || client == null)
|
||||
client = HttpClient.newBuilder().sslContext(sslContext).build();
|
||||
|
||||
URI uriWithQuery = URI.create(uri + "?BAD_HEADERS=" + i);
|
||||
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
|
||||
.POST(BodyPublishers.ofString("Hello there!"))
|
||||
.build();
|
||||
System.out.println("\nSending request:" + uriWithQuery);
|
||||
final HttpClient cc = client;
|
||||
try {
|
||||
HttpResponse<String> response = cc.send(request, BodyHandlers.ofString());
|
||||
fail("Expected exception, got :" + response + ", " + response.body());
|
||||
} catch (IOException ioe) {
|
||||
System.out.println("Got EXPECTED: " + ioe);
|
||||
assertDetailMessage(ioe, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("variants")
|
||||
void testAsync(String uri,
|
||||
boolean sameClient,
|
||||
BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)
|
||||
{
|
||||
CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier);
|
||||
|
||||
HttpClient client = null;
|
||||
for (int i=0; i< BAD_HEADERS.size(); i++) {
|
||||
if (!sameClient || client == null)
|
||||
client = HttpClient.newBuilder().sslContext(sslContext).build();
|
||||
|
||||
URI uriWithQuery = URI.create(uri + "?BAD_HEADERS=" + i);
|
||||
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
|
||||
.POST(BodyPublishers.ofString("Hello there!"))
|
||||
.build();
|
||||
System.out.println("\nSending request:" + uriWithQuery);
|
||||
final HttpClient cc = client;
|
||||
|
||||
Throwable t = null;
|
||||
try {
|
||||
HttpResponse<String> response = cc.sendAsync(request, BodyHandlers.ofString()).get();
|
||||
fail("Expected exception, got :" + response + ", " + response.body());
|
||||
} catch (Exception t0) {
|
||||
System.out.println("Got EXPECTED: " + t0);
|
||||
if (t0 instanceof ExecutionException) {
|
||||
t = t0.getCause();
|
||||
} else {
|
||||
t = t0;
|
||||
}
|
||||
}
|
||||
assertDetailMessage(t, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Assertions based on implementation specific detail messages. Keep in
|
||||
// sync with implementation.
|
||||
static void assertDetailMessage(Throwable throwable, int iterationIndex) {
|
||||
try {
|
||||
assertInstanceOf(ProtocolException.class, throwable, "Expected ProtocolException, got " + throwable);
|
||||
assertTrue(throwable.getMessage().contains("malformed response"),
|
||||
"Expected \"malformed response\" in: " + throwable.getMessage());
|
||||
|
||||
if (iterationIndex == 0) { // unknown
|
||||
assertTrue(throwable.getMessage().contains("Unknown pseudo-header"),
|
||||
"Expected \"Unknown pseudo-header\" in: " + throwable.getMessage());
|
||||
} else if (iterationIndex == 4) { // prohibited
|
||||
assertTrue(throwable.getMessage().contains("Prohibited header name"),
|
||||
"Expected \"Prohibited header name\" in: " + throwable.getMessage());
|
||||
} else if (iterationIndex == 5) { // unexpected type
|
||||
assertTrue(throwable.getMessage().contains("not valid in context"),
|
||||
"Expected \"not valid in context\" in: " + throwable.getMessage());
|
||||
} else if (iterationIndex == 6) { // unexpected sequence
|
||||
assertTrue(throwable.getMessage().contains(" Unexpected pseudo-header"),
|
||||
"Expected \" Unexpected pseudo-header\" in: " + throwable.getMessage());
|
||||
} else {
|
||||
assertTrue(throwable.getMessage().contains("Bad header"),
|
||||
"Expected \"Bad header\" in: " + throwable.getMessage());
|
||||
}
|
||||
} catch (AssertionError e) {
|
||||
System.out.println("Exception does not match expectation: " + throwable);
|
||||
throwable.printStackTrace(System.out);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws Exception {
|
||||
http2TestServer = new Http2TestServer("localhost", false, 0);
|
||||
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
|
||||
int port = http2TestServer.getAddress().getPort();
|
||||
http2URI = "http://localhost:" + port + "/http2/echo";
|
||||
|
||||
https2TestServer = new Http2TestServer("localhost", true, sslContext);
|
||||
https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
|
||||
port = https2TestServer.getAddress().getPort();
|
||||
https2URI = "https://localhost:" + port + "/https2/echo";
|
||||
|
||||
// Override the default exchange supplier with a custom one to enable
|
||||
// particular test scenarios
|
||||
http2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new);
|
||||
https2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new);
|
||||
|
||||
http2TestServer.start();
|
||||
https2TestServer.start();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void teardown() throws Exception {
|
||||
http2TestServer.stop();
|
||||
https2TestServer.stop();
|
||||
}
|
||||
|
||||
static class Http2EchoHandler implements Http2Handler {
|
||||
|
||||
@Override
|
||||
public void handle(Http2TestExchange t) throws IOException {
|
||||
try (InputStream is = t.getRequestBody();
|
||||
OutputStream os = t.getResponseBody()) {
|
||||
byte[] bytes = is.readAllBytes();
|
||||
// Note: strictly ordered response headers will be added within
|
||||
// the custom sendResponseHeaders implementation, based upon the
|
||||
// query parameter
|
||||
t.sendResponseHeaders(200, bytes.length);
|
||||
os.write(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A custom Http2TestExchangeImpl that overrides sendResponseHeaders to
|
||||
// allow headers to be sent with a number of CONTINUATION frames.
|
||||
static class CFTHttp2TestExchange extends Http2TestExchangeImpl {
|
||||
static volatile BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFrameSupplier;
|
||||
volatile int badHeadersIndex = -1;
|
||||
|
||||
static void setHeaderFrameSupplier(BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> hfs) {
|
||||
headerFrameSupplier = hfs;
|
||||
}
|
||||
|
||||
CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
|
||||
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
|
||||
SSLSession sslSession, BodyOutputStream os,
|
||||
Http2TestServerConnection conn, boolean pushAllowed) {
|
||||
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession,
|
||||
os, conn, pushAllowed);
|
||||
String query = uri.getQuery();
|
||||
badHeadersIndex = Integer.parseInt(query.substring(query.indexOf("=") + 1));
|
||||
assert badHeadersIndex >= 0 && badHeadersIndex < BAD_HEADERS.size() :
|
||||
"Unexpected badHeadersIndex value: " + badHeadersIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
|
||||
assert rspheadersBuilder.build().map().size() == 0;
|
||||
assert badHeadersIndex >= 0 && badHeadersIndex < BAD_HEADERS.size() :
|
||||
"Unexpected badHeadersIndex value: " + badHeadersIndex;
|
||||
|
||||
List<Entry<String,String>> headers = BAD_HEADERS.get(badHeadersIndex);
|
||||
System.out.println("Server replying with bad headers: " + headers);
|
||||
List<ByteBuffer> encodeHeaders = conn.encodeHeadersOrdered(headers);
|
||||
|
||||
List<Http2Frame> headerFrames = headerFrameSupplier.apply(streamid, encodeHeaders);
|
||||
assert headerFrames.size() > 0; // there must always be at least 1
|
||||
|
||||
if (responseLength < 0) {
|
||||
headerFrames.get(headerFrames.size() -1).setFlag(HeadersFrame.END_STREAM);
|
||||
os.markClosed();
|
||||
}
|
||||
|
||||
for (Http2Frame f : headerFrames) {
|
||||
conn.addToOutputQ(f);
|
||||
}
|
||||
|
||||
os.goodToGo();
|
||||
System.err.println("Sent response headers " + rCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
181
test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java
Normal file
181
test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright (c) 2025, 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
|
||||
* @bug 8354276
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* @run junit/othervm
|
||||
* -Djdk.internal.httpclient.debug=true
|
||||
* -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace
|
||||
* BadPushPromiseTest
|
||||
*/
|
||||
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.net.http.HttpResponse.PushPromiseHandler;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.List.of;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class BadPushPromiseTest {
|
||||
|
||||
private static final List<Map<String, List<String>>> BAD_HEADERS = of(
|
||||
Map.of(":hello", of("GET")), // Unknown pseudo-header
|
||||
Map.of("hell o", of("value")), // Space in the name
|
||||
Map.of("hello", of("line1\r\n line2\r\n")), // Multiline value
|
||||
Map.of("hello", of("DE" + ((char) 0x7F) + "L")), // Bad byte in value
|
||||
Map.of(":status", of("200")) // Response pseudo-header in request
|
||||
);
|
||||
|
||||
static final String MAIN_RESPONSE_BODY = "the main response body";
|
||||
|
||||
static HttpServerAdapters.HttpTestServer server;
|
||||
static URI uri;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws Exception {
|
||||
server = HttpServerAdapters.HttpTestServer.create(HTTP_2);
|
||||
HttpServerAdapters.HttpTestHandler handler = new ServerPushHandler(MAIN_RESPONSE_BODY);
|
||||
server.addHandler(handler, "/");
|
||||
server.start();
|
||||
String authority = server.serverAuthority();
|
||||
System.err.println("Server listening on address " + authority);
|
||||
uri = new URI("http://" + authority + "/foo/a/b/c");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void teardown() {
|
||||
server.stop();
|
||||
}
|
||||
|
||||
/*
|
||||
* Malformed push promise headers should kill the connection
|
||||
*/
|
||||
@Test
|
||||
void test() {
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
for (int i=0; i< BAD_HEADERS.size(); i++) {
|
||||
URI uriWithQuery = URI.create(uri + "?BAD_HEADERS=" + i);
|
||||
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
|
||||
.build();
|
||||
System.out.println("\nSending request:" + uriWithQuery);
|
||||
final HttpClient cc = client;
|
||||
try {
|
||||
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>> promises
|
||||
= new ConcurrentHashMap<>();
|
||||
PushPromiseHandler<String> pph = PushPromiseHandler
|
||||
.of((r) -> BodyHandlers.ofString(), promises);
|
||||
HttpResponse<String> response = cc.sendAsync(request, BodyHandlers.ofString(), pph).join();
|
||||
fail("Expected exception, got :" + response + ", " + response.body());
|
||||
} catch (CompletionException ce) {
|
||||
System.out.println("Got EXPECTED: " + ce);
|
||||
assertDetailMessage(ce.getCause(), i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assertions based on implementation specific detail messages. Keep in
|
||||
// sync with implementation.
|
||||
static void assertDetailMessage(Throwable throwable, int iterationIndex) {
|
||||
try {
|
||||
assertInstanceOf(ProtocolException.class, throwable, "Expected ProtocolException, got " + throwable);
|
||||
|
||||
if (iterationIndex == 0) { // unknown
|
||||
assertTrue(throwable.getMessage().contains("Unknown pseudo-header"),
|
||||
"Expected \"Unknown pseudo-header\" in: " + throwable.getMessage());
|
||||
} else if (iterationIndex == 4) { // unexpected type
|
||||
assertTrue(throwable.getMessage().contains("not valid in context"),
|
||||
"Expected \"not valid in context\" in: " + throwable.getMessage());
|
||||
} else {
|
||||
assertTrue(throwable.getMessage().contains("Bad header"),
|
||||
"Expected \"Bad header\" in: " + throwable.getMessage());
|
||||
}
|
||||
} catch (AssertionError e) {
|
||||
System.out.println("Exception does not match expectation: " + throwable);
|
||||
throwable.printStackTrace(System.out);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// --- server push handler ---
|
||||
static class ServerPushHandler implements HttpServerAdapters.HttpTestHandler {
|
||||
|
||||
private final String mainResponseBody;
|
||||
|
||||
public ServerPushHandler(String mainResponseBody) {
|
||||
this.mainResponseBody = mainResponseBody;
|
||||
}
|
||||
|
||||
public void handle(HttpServerAdapters.HttpTestExchange exchange) throws IOException {
|
||||
System.err.println("Server: handle " + exchange);
|
||||
try (InputStream is = exchange.getRequestBody()) {
|
||||
is.readAllBytes();
|
||||
}
|
||||
|
||||
pushPromise(exchange);
|
||||
|
||||
// response data for the main response
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
byte[] bytes = mainResponseBody.getBytes(UTF_8);
|
||||
exchange.sendResponseHeaders(200, bytes.length);
|
||||
os.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private void pushPromise(HttpServerAdapters.HttpTestExchange exchange) throws IOException {
|
||||
URI requestURI = exchange.getRequestURI();
|
||||
String query = exchange.getRequestURI().getQuery();
|
||||
int badHeadersIndex = Integer.parseInt(query.substring(query.indexOf("=") + 1));
|
||||
URI uri = requestURI.resolve("/push/"+badHeadersIndex);
|
||||
InputStream is = new ByteArrayInputStream(mainResponseBody.getBytes(UTF_8));
|
||||
HttpHeaders headers = HttpHeaders.of(BAD_HEADERS.get(badHeadersIndex), (x, y) -> true);
|
||||
exchange.serverPush(uri, headers, is);
|
||||
System.err.println("Server: push sent");
|
||||
}
|
||||
}
|
||||
}
|
||||
291
test/jdk/java/net/httpclient/http2/BasicTest.java
Normal file
291
test/jdk/java/net/httpclient/http2/BasicTest.java
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
* Copyright (c) 2015, 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
|
||||
* @bug 8087112
|
||||
* @library /test/jdk/java/net/httpclient/lib
|
||||
* /test/lib
|
||||
* @build jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* jdk.test.lib.Asserts
|
||||
* jdk.test.lib.Utils
|
||||
* jdk.test.lib.net.SimpleSSLContext
|
||||
* @run junit/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors BasicTest
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import javax.net.ssl.*;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.file.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
import static jdk.test.lib.Asserts.assertFileContentsEqual;
|
||||
import static jdk.test.lib.Utils.createTempFile;
|
||||
import static jdk.test.lib.Utils.createTempFileOfSize;
|
||||
|
||||
public class BasicTest implements HttpServerAdapters {
|
||||
|
||||
private static final String TEMP_FILE_PREFIX =
|
||||
HttpClient.class.getPackageName() + '-' + BasicTest.class.getSimpleName() + '-';
|
||||
|
||||
static int httpPort, httpsPort;
|
||||
static HttpTestServer httpServer, httpsServer;
|
||||
static HttpClient client = null;
|
||||
static ExecutorService clientExec;
|
||||
static ExecutorService serverExec;
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
|
||||
static String pingURIString, httpURIString, httpsURIString;
|
||||
|
||||
static void initialize() throws Exception {
|
||||
try {
|
||||
client = getClient();
|
||||
httpServer = HttpTestServer.of(
|
||||
new Http2TestServer(false, 0, serverExec, sslContext));
|
||||
httpServer.addHandler(new HttpTestFileEchoHandler(), "/");
|
||||
httpServer.addHandler(new EchoWithPingHandler(), "/ping");
|
||||
httpPort = httpServer.getAddress().getPort();
|
||||
|
||||
httpsServer = HttpTestServer.of(
|
||||
new Http2TestServer(true, 0, serverExec, sslContext));
|
||||
httpsServer.addHandler(new HttpTestFileEchoHandler(), "/");
|
||||
|
||||
httpsPort = httpsServer.getAddress().getPort();
|
||||
httpURIString = "http://" + httpServer.serverAuthority() + "/foo/";
|
||||
pingURIString = "http://" + httpServer.serverAuthority() + "/ping/";
|
||||
httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/";
|
||||
|
||||
httpServer.start();
|
||||
httpsServer.start();
|
||||
} catch (Throwable e) {
|
||||
System.err.println("Throwing now");
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
static List<CompletableFuture<Long>> cfs = Collections
|
||||
.synchronizedList( new LinkedList<>());
|
||||
|
||||
static CompletableFuture<Long> currentCF;
|
||||
|
||||
static class EchoWithPingHandler extends HttpTestFileEchoHandler {
|
||||
private final Object lock = new Object();
|
||||
|
||||
@Override
|
||||
public void handle(HttpTestExchange exchange) throws IOException {
|
||||
// for now only one ping active at a time. don't want to saturate
|
||||
synchronized(lock) {
|
||||
CompletableFuture<Long> cf = currentCF;
|
||||
if (cf == null || cf.isDone()) {
|
||||
cf = exchange.sendPing();
|
||||
assert cf != null;
|
||||
cfs.add(cf);
|
||||
currentCF = cf;
|
||||
}
|
||||
}
|
||||
super.handle(exchange);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test() throws Exception {
|
||||
try {
|
||||
initialize();
|
||||
warmup(false);
|
||||
warmup(true);
|
||||
simpleTest(false, false);
|
||||
simpleTest(false, true);
|
||||
simpleTest(true, false);
|
||||
streamTest(false);
|
||||
streamTest(true);
|
||||
paramsTest();
|
||||
CompletableFuture.allOf(cfs.toArray(new CompletableFuture[0])).join();
|
||||
synchronized (cfs) {
|
||||
for (CompletableFuture<Long> cf : cfs) {
|
||||
System.out.printf("Ping ack received in %d millisec\n", cf.get());
|
||||
}
|
||||
}
|
||||
} catch (Throwable tt) {
|
||||
System.err.println("tt caught");
|
||||
tt.printStackTrace();
|
||||
throw tt;
|
||||
} finally {
|
||||
httpServer.stop();
|
||||
httpsServer.stop();
|
||||
//clientExec.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
static HttpClient getClient() {
|
||||
if (client == null) {
|
||||
serverExec = Executors.newCachedThreadPool();
|
||||
clientExec = Executors.newCachedThreadPool();
|
||||
client = HttpClient.newBuilder()
|
||||
.executor(clientExec)
|
||||
.sslContext(sslContext)
|
||||
.version(HTTP_2)
|
||||
.build();
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
static URI getURI(boolean secure) {
|
||||
return getURI(secure, false);
|
||||
}
|
||||
|
||||
static URI getURI(boolean secure, boolean ping) {
|
||||
if (secure)
|
||||
return URI.create(httpsURIString);
|
||||
else
|
||||
return URI.create(ping ? pingURIString: httpURIString);
|
||||
}
|
||||
|
||||
static void checkStatus(int expected, int found) throws Exception {
|
||||
if (expected != found) {
|
||||
System.err.printf ("Test failed: wrong status code %d/%d\n",
|
||||
expected, found);
|
||||
throw new RuntimeException("Test failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void checkStrings(String expected, String found) throws Exception {
|
||||
if (!expected.equals(found)) {
|
||||
System.err.printf ("Test failed: wrong string %s/%s\n",
|
||||
expected, found);
|
||||
throw new RuntimeException("Test failed");
|
||||
}
|
||||
}
|
||||
|
||||
static final String SIMPLE_STRING = "Hello world Goodbye world";
|
||||
|
||||
static final int LOOPS = 13;
|
||||
static final int FILESIZE = 64 * 1024 + 200;
|
||||
|
||||
static void streamTest(boolean secure) throws Exception {
|
||||
URI uri = getURI(secure);
|
||||
System.err.printf("streamTest %b to %s\n" , secure, uri);
|
||||
|
||||
HttpClient client = getClient();
|
||||
Path src = createTempFileOfSize(TEMP_FILE_PREFIX, null, FILESIZE * 4);
|
||||
HttpRequest req = HttpRequest.newBuilder(uri)
|
||||
.POST(BodyPublishers.ofFile(src))
|
||||
.build();
|
||||
|
||||
Path dest = Paths.get("streamtest.txt");
|
||||
dest.toFile().delete();
|
||||
CompletableFuture<Path> response = client.sendAsync(req, BodyHandlers.ofFile(dest))
|
||||
.thenApply(resp -> {
|
||||
if (resp.statusCode() != 200)
|
||||
throw new RuntimeException();
|
||||
return resp.body();
|
||||
});
|
||||
response.join();
|
||||
assertFileContentsEqual(src, dest);
|
||||
System.err.println("streamTest: DONE");
|
||||
}
|
||||
|
||||
static void paramsTest() throws Exception {
|
||||
httpsServer.addHandler(((HttpTestExchange t) -> {
|
||||
SSLSession s = t.getSSLSession();
|
||||
String prot = s.getProtocol();
|
||||
if (prot.equals("TLSv1.2") || prot.equals("TLSv1.3")) {
|
||||
t.sendResponseHeaders(200, HttpTestExchange.RSPBODY_EMPTY);
|
||||
} else {
|
||||
System.err.printf("Protocols =%s\n", prot);
|
||||
t.sendResponseHeaders(500, HttpTestExchange.RSPBODY_EMPTY);
|
||||
}
|
||||
}), "/");
|
||||
URI u = new URI("https://" + httpsServer.serverAuthority() + "/foo");
|
||||
HttpClient client = getClient();
|
||||
HttpRequest req = HttpRequest.newBuilder(u).build();
|
||||
HttpResponse<String> resp = client.send(req, BodyHandlers.ofString());
|
||||
int stat = resp.statusCode();
|
||||
if (stat != 200) {
|
||||
throw new RuntimeException("paramsTest failed "
|
||||
+ Integer.toString(stat));
|
||||
}
|
||||
System.err.println("paramsTest: DONE");
|
||||
}
|
||||
|
||||
static void warmup(boolean secure) throws Exception {
|
||||
URI uri = getURI(secure);
|
||||
System.err.println("Request to " + uri);
|
||||
|
||||
// Do a simple warmup request
|
||||
|
||||
HttpClient client = getClient();
|
||||
HttpRequest req = HttpRequest.newBuilder(uri)
|
||||
.POST(BodyPublishers.ofString(SIMPLE_STRING))
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(req, BodyHandlers.ofString());
|
||||
checkStatus(200, response.statusCode());
|
||||
String responseBody = response.body();
|
||||
HttpHeaders h = response.headers();
|
||||
checkStrings(SIMPLE_STRING, responseBody);
|
||||
checkStrings(h.firstValue("x-hello").get(), "world");
|
||||
checkStrings(h.firstValue("x-bye").get(), "universe");
|
||||
}
|
||||
|
||||
static void simpleTest(boolean secure, boolean ping) throws Exception {
|
||||
URI uri = getURI(secure, ping);
|
||||
System.err.println("Request to " + uri);
|
||||
|
||||
// Do loops asynchronously
|
||||
|
||||
CompletableFuture[] responses = new CompletableFuture[LOOPS];
|
||||
final Path source = createTempFileOfSize(TEMP_FILE_PREFIX, null, FILESIZE);
|
||||
HttpRequest request = HttpRequest.newBuilder(uri)
|
||||
.POST(BodyPublishers.ofFile(source))
|
||||
.build();
|
||||
for (int i = 0; i < LOOPS; i++) {
|
||||
responses[i] = client.sendAsync(request, BodyHandlers.ofFile(createTempFile(TEMP_FILE_PREFIX, null)))
|
||||
//.thenApply(resp -> compareFiles(resp.body(), source));
|
||||
.thenApply(resp -> {
|
||||
Path body = resp.body();
|
||||
System.out.printf("Resp status %d body size %d\n",
|
||||
resp.statusCode(), body.toFile().length());
|
||||
assertFileContentsEqual(body, source);
|
||||
return null;
|
||||
});
|
||||
Thread.sleep(100);
|
||||
}
|
||||
CompletableFuture.allOf(responses).join();
|
||||
System.err.println("simpleTest: DONE");
|
||||
}
|
||||
}
|
||||
208
test/jdk/java/net/httpclient/http2/BurstyRequestsTest.java
Normal file
208
test/jdk/java/net/httpclient/http2/BurstyRequestsTest.java
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* Copyright (c) 2025, 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.
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javax.net.ssl.SSLSession;
|
||||
|
||||
import jdk.httpclient.test.lib.http2.BodyOutputStream;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchangeSupplier;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
|
||||
import jdk.internal.net.http.HttpClientImplAccess;
|
||||
import jdk.internal.net.http.common.HttpHeadersBuilder;
|
||||
import jdk.test.lib.net.URIBuilder;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8326498 8361091
|
||||
* @summary verify that the HttpClient does not leak connections when dealing with
|
||||
* sudden rush of HTTP/2 requests
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib ../access
|
||||
* @build jdk.test.lib.net.SimpleSSLContext
|
||||
* jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* jdk.httpclient.test.lib.http2.Http2Handler
|
||||
* jdk.httpclient.test.lib.http2.Http2TestExchange
|
||||
* jdk.httpclient.test.lib.http2.Http2TestExchangeSupplier
|
||||
* java.net.http/jdk.internal.net.http.HttpClientImplAccess
|
||||
* @run junit ${test.main.class}
|
||||
*/
|
||||
class BurstyRequestsTest {
|
||||
|
||||
private static final String HANDLER_PATH = "/8326498/";
|
||||
|
||||
// we use a h2c server but it doesn't matter if it is h2c or h2
|
||||
private static Http2TestServer http2Server;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() throws Exception {
|
||||
http2Server = new Http2TestServer(false, 0);
|
||||
http2Server.setExchangeSupplier(new ExchangeSupplier());
|
||||
http2Server.addHandler(new Handler(), HANDLER_PATH);
|
||||
http2Server.start();
|
||||
System.err.println("started HTTP/2 server " + http2Server.getAddress());
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
if (http2Server != null) {
|
||||
System.err.println("stopping server " + http2Server.getAddress());
|
||||
http2Server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Issues a burst of HTTP/2 requests to the same server (host/port) and expects all of
|
||||
* them to complete normally.
|
||||
* Once these requests have completed, the test then peeks into an internal field of the
|
||||
* HttpClientImpl to verify that the client is holding on to at most 1 connection.
|
||||
*/
|
||||
@Test
|
||||
void testOpenConnections() throws Exception {
|
||||
final URI reqURI = URIBuilder.newBuilder()
|
||||
.scheme("http")
|
||||
.host(http2Server.getAddress().getAddress())
|
||||
.port(http2Server.getAddress().getPort())
|
||||
.path(HANDLER_PATH)
|
||||
.build();
|
||||
final HttpRequest req = HttpRequest.newBuilder().uri(reqURI).build();
|
||||
|
||||
final int numRequests = 20;
|
||||
// latch for the tasks to wait on, before issuing the requests
|
||||
final CountDownLatch startLatch = new CountDownLatch(numRequests);
|
||||
final List<Future<Void>> futures = new ArrayList<>();
|
||||
|
||||
try (final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
final HttpClient client = HttpClient.newBuilder()
|
||||
.proxy(NO_PROXY)
|
||||
.version(HTTP_2)
|
||||
.build()) {
|
||||
// our test needs to peek into the internal field of jdk.internal.net.http.HttpClientImpl
|
||||
final Set<?> openedConnections = HttpClientImplAccess.getOpenedConnections(client);
|
||||
assertNotNull(openedConnections, "HttpClientImpl#openedConnections field is null or not available");
|
||||
|
||||
for (int i = 0; i < numRequests; i++) {
|
||||
final Future<Void> f = executor.submit(new RequestIssuer(startLatch, client, req));
|
||||
futures.add(f);
|
||||
}
|
||||
// wait for the requests to complete
|
||||
for (final Future<Void> f : futures) {
|
||||
f.get();
|
||||
}
|
||||
System.err.println("all " + numRequests + " requests completed successfully");
|
||||
// the request completion happens asynchronously to the closing of the HTTP/2 Stream
|
||||
// as well as the HTTP/2 connection. we wait for at most 1 connection to be retained
|
||||
// by HttpClientImpl.
|
||||
System.err.println("waiting for at least " + (numRequests - 1) + " connections to be closed");
|
||||
// now verify that the current open TCP connections is not more than 1.
|
||||
// we let the test timeout if we never reach that count.
|
||||
int size = openedConnections.size();
|
||||
System.err.println("currently " + size + " open connections: " + openedConnections);
|
||||
while (size > 1) {
|
||||
// wait
|
||||
Thread.sleep(100);
|
||||
final int prev = size;
|
||||
size = openedConnections.size();
|
||||
if (prev != size) {
|
||||
System.err.println("currently " + size + " open connections: " + openedConnections);
|
||||
}
|
||||
}
|
||||
// we expect at most 1 connection will stay open
|
||||
assertTrue((size == 0 || size == 1),
|
||||
"unexpected number of current open connections: " + size);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RequestIssuer implements Callable<Void> {
|
||||
private final CountDownLatch startLatch;
|
||||
private final HttpClient client;
|
||||
private final HttpRequest request;
|
||||
|
||||
private RequestIssuer(final CountDownLatch startLatch, final HttpClient client,
|
||||
final HttpRequest request) {
|
||||
this.startLatch = startLatch;
|
||||
this.client = client;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void call() throws Exception {
|
||||
this.startLatch.countDown(); // announce our arrival
|
||||
this.startLatch.await(); // wait for other threads to arrive
|
||||
// issue the request
|
||||
final HttpResponse<Void> resp = this.client.send(request, BodyHandlers.discarding());
|
||||
if (resp.statusCode() != 200) {
|
||||
throw new AssertionError("unexpected response status code: " + resp.statusCode());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Handler implements Http2Handler {
|
||||
private static final int NO_RESP_BODY = -1;
|
||||
|
||||
@Override
|
||||
public void handle(final Http2TestExchange exchange) throws IOException {
|
||||
System.err.println("handling request " + exchange.getRequestURI());
|
||||
exchange.sendResponseHeaders(200, NO_RESP_BODY);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ExchangeSupplier implements Http2TestExchangeSupplier {
|
||||
|
||||
@Override
|
||||
public Http2TestExchange get(int streamid, String method, HttpHeaders reqheaders,
|
||||
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
|
||||
SSLSession sslSession, BodyOutputStream os,
|
||||
Http2TestServerConnection conn, boolean pushAllowed) {
|
||||
// don't close the connection when/if the client sends a GOAWAY
|
||||
conn.closeConnOnIncomingGoAway = false;
|
||||
return Http2TestExchangeSupplier.ofDefault().get(streamid, method, reqheaders,
|
||||
rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
/*
|
||||
* Copyright (c) 2024, 2025, 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
|
||||
* @bug 8342075
|
||||
* @summary checks connection flow control
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext
|
||||
* @run junit/othervm -Djdk.internal.httpclient.debug=err
|
||||
* -Djdk.httpclient.connectionWindowSize=65535
|
||||
* -Djdk.httpclient.windowsize=16384
|
||||
* ConnectionFlowControlTest
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer;
|
||||
import jdk.httpclient.test.lib.http2.BodyOutputStream;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
|
||||
import jdk.internal.net.http.common.HttpHeadersBuilder;
|
||||
import jdk.internal.net.http.frame.SettingsFrame;
|
||||
import jdk.test.lib.Utils;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ConnectionFlowControlTest {
|
||||
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
private static HttpTestServer http2TestServer; // HTTP/2 ( h2c )
|
||||
private static HttpTestServer https2TestServer; // HTTP/2 ( h2 )
|
||||
private static String http2URI;
|
||||
private static String https2URI;
|
||||
private final AtomicInteger reqid = new AtomicInteger();
|
||||
|
||||
static Object[][] variants() {
|
||||
return new Object[][] {
|
||||
{ http2URI },
|
||||
{ https2URI },
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("variants")
|
||||
void test(String uri) throws Exception {
|
||||
System.out.printf("%ntesting %s%n", uri);
|
||||
ConcurrentHashMap<String, CompletableFuture<String>> responseSent = new ConcurrentHashMap<>();
|
||||
ConcurrentHashMap<String, HttpResponse<InputStream>> responses = new ConcurrentHashMap<>();
|
||||
FCHttp2TestExchange.setResponseSentCB((s) -> responseSent.get(s).complete(s));
|
||||
int connectionWindowSize = Math.max(Integer.getInteger(
|
||||
"jdk.httpclient.connectionWindowSize", 65535), 65535);
|
||||
int windowSize = Math.max(Integer.getInteger(
|
||||
"jdk.httpclient.windowsize", 65535), 16384);
|
||||
int max = connectionWindowSize / windowSize + 2;
|
||||
System.out.printf("connection window: %s, stream window: %s, will make %s requests%n",
|
||||
connectionWindowSize, windowSize, max);
|
||||
|
||||
try (HttpClient client = HttpClient.newBuilder().sslContext(sslContext).build()) {
|
||||
String label = null;
|
||||
|
||||
Throwable t = null;
|
||||
try {
|
||||
String[] keys = new String[max];
|
||||
for (int i = 0; i < max; i++) {
|
||||
String query = "reqId=" + reqid.incrementAndGet();
|
||||
keys[i] = query;
|
||||
URI uriWithQuery = URI.create(uri + "?" + query);
|
||||
CompletableFuture<String> sent = new CompletableFuture<>();
|
||||
responseSent.put(query, sent);
|
||||
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
|
||||
.POST(BodyPublishers.ofString("Hello there!"))
|
||||
.build();
|
||||
System.out.println("\nSending request:" + uriWithQuery);
|
||||
final HttpClient cc = client;
|
||||
var response = cc.send(request, BodyHandlers.ofInputStream());
|
||||
responses.put(query, response);
|
||||
String ckey = response.connectionLabel().get();
|
||||
if (label == null) label = ckey;
|
||||
try {
|
||||
if (i < max - 1) {
|
||||
// the connection window might be exceeded at i == max - 2, which
|
||||
// means that the last request could go on a new connection.
|
||||
assertEquals(label, ckey, "Unexpected key for " + query);
|
||||
}
|
||||
} catch (AssertionError ass) {
|
||||
// since we won't pull all responses, the client
|
||||
// will not exit unless we ask it to shutdown now.
|
||||
client.shutdownNow();
|
||||
throw ass;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException ie) {
|
||||
// ignore
|
||||
}
|
||||
CompletableFuture<?> allsent = CompletableFuture.allOf(responseSent.values().stream()
|
||||
.toArray(CompletableFuture<?>[]::new));
|
||||
allsent.get();
|
||||
for (int i = 0; i < max; i++) {
|
||||
try {
|
||||
String query = keys[i];
|
||||
var response = responses.get(keys[i]);
|
||||
String ckey = response.connectionLabel().get();
|
||||
if (label == null) label = ckey;
|
||||
if (i < max - 1) {
|
||||
// the connection window might be exceeded at i == max - 2, which
|
||||
// means that the last request could go on a new connection.
|
||||
assertEquals(label, ckey, "Unexpected key for " + query);
|
||||
}
|
||||
int wait = uri.startsWith("https://") ? 500 : 250;
|
||||
try (InputStream is = response.body()) {
|
||||
Thread.sleep(Utils.adjustTimeout(wait));
|
||||
is.readAllBytes();
|
||||
}
|
||||
System.out.printf("%s did not fail: %s%n", query, response.statusCode());
|
||||
} catch (AssertionError t1) {
|
||||
// since we won't pull all responses, the client
|
||||
// will not exit unless we ask it to shutdown now.
|
||||
client.shutdownNow();
|
||||
throw t1;
|
||||
} catch (Throwable t0) {
|
||||
System.out.println("Got EXPECTED: " + t0);
|
||||
if (t0 instanceof ExecutionException) {
|
||||
t0 = t0.getCause();
|
||||
}
|
||||
t = t0;
|
||||
try {
|
||||
assertDetailMessage(t0, i);
|
||||
} catch (AssertionError e) {
|
||||
// since we won't pull all responses, the client
|
||||
// will not exit unless we ask it to shutdown now.
|
||||
client.shutdownNow();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable t0) {
|
||||
System.out.println("Got EXPECTED: " + t0);
|
||||
if (t0 instanceof ExecutionException) {
|
||||
t0 = t0.getCause();
|
||||
}
|
||||
t = t0;
|
||||
}
|
||||
if (t == null) {
|
||||
// we could fail here if we haven't waited long enough
|
||||
fail("Expected exception, got all responses, should sleep time be raised?");
|
||||
} else {
|
||||
assertDetailMessage(t, max);
|
||||
}
|
||||
String query = "reqId=" + reqid.incrementAndGet();
|
||||
URI uriWithQuery = URI.create(uri + "?" + query);
|
||||
CompletableFuture<String> sent = new CompletableFuture<>();
|
||||
responseSent.put(query, sent);
|
||||
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
|
||||
.POST(BodyPublishers.ofString("Hello there!"))
|
||||
.build();
|
||||
System.out.println("\nSending last request:" + uriWithQuery);
|
||||
var response = client.send(request, BodyHandlers.ofString());
|
||||
if (label != null) {
|
||||
String ckey = response.connectionLabel().get();
|
||||
assertNotEquals(label, ckey);
|
||||
System.out.printf("last request %s sent on different connection as expected:" +
|
||||
"\n\tlast: %s\n\tprevious: %s%n", query, ckey, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assertions based on implementation specific detail messages. Keep in
|
||||
// sync with implementation.
|
||||
static void assertDetailMessage(Throwable throwable, int iterationIndex) {
|
||||
try {
|
||||
Throwable cause = throwable;
|
||||
while (cause != null) {
|
||||
if (cause instanceof ProtocolException) {
|
||||
if (cause.getMessage().contains("connection window exceeded")) {
|
||||
System.out.println("Found expected exception: " + cause);
|
||||
return;
|
||||
}
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
throw new AssertionError(
|
||||
"ProtocolException(\"protocol error: connection window exceeded\") not found",
|
||||
throwable);
|
||||
} catch (AssertionError e) {
|
||||
System.out.println("Exception does not match expectation: " + throwable);
|
||||
throwable.printStackTrace(System.out);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws Exception {
|
||||
var http2TestServerLocal = new Http2TestServer("localhost", false, 0);
|
||||
http2TestServerLocal.addHandler(new Http2TestHandler(), "/http2/");
|
||||
http2TestServer = HttpTestServer.of(http2TestServerLocal);
|
||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/x";
|
||||
|
||||
var https2TestServerLocal = new Http2TestServer("localhost", true, sslContext);
|
||||
https2TestServerLocal.addHandler(new Http2TestHandler(), "/https2/");
|
||||
https2TestServer = HttpTestServer.of(https2TestServerLocal);
|
||||
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/x";
|
||||
|
||||
// Override the default exchange supplier with a custom one to enable
|
||||
// particular test scenarios
|
||||
http2TestServerLocal.setExchangeSupplier(FCHttp2TestExchange::new);
|
||||
https2TestServerLocal.setExchangeSupplier(FCHttp2TestExchange::new);
|
||||
|
||||
http2TestServer.start();
|
||||
https2TestServer.start();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void teardown() throws Exception {
|
||||
http2TestServer.stop();
|
||||
https2TestServer.stop();
|
||||
}
|
||||
|
||||
static class Http2TestHandler implements Http2Handler {
|
||||
|
||||
@Override
|
||||
public void handle(Http2TestExchange t) throws IOException {
|
||||
String query = t.getRequestURI().getRawQuery();
|
||||
|
||||
try (InputStream is = t.getRequestBody();
|
||||
OutputStream os = t.getResponseBody()) {
|
||||
|
||||
byte[] bytes = is.readAllBytes();
|
||||
System.out.println("Server " + t.getLocalAddress() + " received:\n"
|
||||
+ t.getRequestURI() + ": " + new String(bytes, StandardCharsets.UTF_8));
|
||||
|
||||
if (bytes.length == 0) bytes = "no request body!".getBytes(StandardCharsets.UTF_8);
|
||||
int window = Math.max(16384, Integer.getInteger("jdk.httpclient.windowsize", 2*16*1024));
|
||||
final int maxChunkSize;
|
||||
if (t instanceof FCHttp2TestExchange fct) {
|
||||
maxChunkSize = Math.min(window, fct.conn.getMaxFrameSize());
|
||||
} else {
|
||||
maxChunkSize = Math.min(window, SettingsFrame.MAX_FRAME_SIZE);
|
||||
}
|
||||
byte[] resp = bytes.length < maxChunkSize
|
||||
? bytes
|
||||
: Arrays.copyOfRange(bytes, 0, maxChunkSize);
|
||||
int max = (window / resp.length);
|
||||
// send in chunks
|
||||
t.sendResponseHeaders(200, 0);
|
||||
int sent = 0;
|
||||
for (int i=0; i<=max; i++) {
|
||||
int len = Math.min(resp.length, window - sent);
|
||||
if (len <= 0) break;
|
||||
if (os instanceof BodyOutputStream bos) {
|
||||
try {
|
||||
// we don't wait for the stream window, but we want
|
||||
// to wait for the connection window
|
||||
bos.waitForStreamWindow(len);
|
||||
} catch (InterruptedException ie) {
|
||||
// ignore and continue...
|
||||
}
|
||||
}
|
||||
((BodyOutputStream) os).writeUncontrolled(resp, 0, len);
|
||||
sent += len;
|
||||
}
|
||||
if (sent != window) fail("should have sent %s, sent %s".formatted(window, sent));
|
||||
}
|
||||
if (t instanceof FCHttp2TestExchange fct) {
|
||||
fct.responseSent(query);
|
||||
} else {
|
||||
fail("Exchange is not %s but %s"
|
||||
.formatted(FCHttp2TestExchange.class.getName(), t.getClass().getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A custom Http2TestExchangeImpl that overrides sendResponseHeaders to
|
||||
// allow headers to be sent with a number of CONTINUATION frames.
|
||||
static class FCHttp2TestExchange extends Http2TestExchangeImpl {
|
||||
static volatile Consumer<String> responseSentCB;
|
||||
static void setResponseSentCB(Consumer<String> responseSentCB) {
|
||||
FCHttp2TestExchange.responseSentCB = responseSentCB;
|
||||
}
|
||||
|
||||
final Http2TestServerConnection conn;
|
||||
FCHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
|
||||
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
|
||||
SSLSession sslSession, BodyOutputStream os,
|
||||
Http2TestServerConnection conn, boolean pushAllowed) {
|
||||
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
|
||||
this.conn = conn;
|
||||
}
|
||||
public void responseSent(String query) {
|
||||
System.out.println("Server: response sent for " + query);
|
||||
responseSentCB.accept(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
181
test/jdk/java/net/httpclient/http2/ConnectionReuseTest.java
Normal file
181
test/jdk/java/net/httpclient/http2/ConnectionReuseTest.java
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright (c) 2023, 2025, 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.
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange;
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler;
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer;
|
||||
import jdk.test.lib.net.IPSupport;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8305906
|
||||
* @summary verify that the HttpClient pools and reuses a connection for HTTP/2 requests
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.test.lib.net.SimpleSSLContext
|
||||
* jdk.test.lib.net.IPSupport
|
||||
* jdk.httpclient.test.lib.common.HttpServerAdapters
|
||||
*
|
||||
* @run junit ConnectionReuseTest
|
||||
* @run junit/othervm -Djava.net.preferIPv6Addresses=true
|
||||
* -Djdk.internal.httpclient.debug=true ConnectionReuseTest
|
||||
*/
|
||||
public class ConnectionReuseTest {
|
||||
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
private static HttpTestServer http2_Server; // h2 server over HTTP
|
||||
private static HttpTestServer https2_Server; // h2 server over HTTPS
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() throws Exception {
|
||||
if (IPSupport.preferIPv6Addresses()) {
|
||||
IPSupport.printPlatformSupport(System.err); // for debug purposes
|
||||
// this test is run with -Djava.net.preferIPv6Addresses=true, so skip (all) tests
|
||||
// if IPv6 isn't supported on this host
|
||||
Assumptions.assumeTrue(IPSupport.hasIPv6(), "Skipping tests - IPv6 is not supported");
|
||||
}
|
||||
|
||||
http2_Server = HttpTestServer.create(HTTP_2);
|
||||
http2_Server.addHandler(new Handler(), "/");
|
||||
http2_Server.start();
|
||||
System.out.println("Started HTTP v2 server at " + http2_Server.serverAuthority());
|
||||
|
||||
https2_Server = HttpTestServer.create(HTTP_2, sslContext);
|
||||
https2_Server.addHandler(new Handler(), "/");
|
||||
https2_Server.start();
|
||||
System.out.println("Started HTTPS v2 server at " + https2_Server.serverAuthority());
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterAll() {
|
||||
if (https2_Server != null) {
|
||||
System.out.println("Stopping server " + https2_Server);
|
||||
https2_Server.stop();
|
||||
}
|
||||
if (http2_Server != null) {
|
||||
System.out.println("Stopping server " + http2_Server);
|
||||
http2_Server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> requestURIs() throws Exception {
|
||||
final List<Arguments> arguments = new ArrayList<>();
|
||||
// h2 over HTTPS
|
||||
arguments.add(Arguments.of(new URI("https://" + https2_Server.serverAuthority() + "/")));
|
||||
// h2 over HTTP
|
||||
arguments.add(Arguments.of(new URI("http://" + http2_Server.serverAuthority() + "/")));
|
||||
if (IPSupport.preferIPv6Addresses()) {
|
||||
if (https2_Server.getAddress().getAddress().isLoopbackAddress()) {
|
||||
// h2 over HTTPS, use the short form of the host, in the request URI
|
||||
arguments.add(Arguments.of(new URI("https://[::1]:" +
|
||||
https2_Server.getAddress().getPort() + "/")));
|
||||
}
|
||||
if (http2_Server.getAddress().getAddress().isLoopbackAddress()) {
|
||||
// h2 over HTTP, use the short form of the host, in the request URI
|
||||
arguments.add(Arguments.of(new URI("http://[::1]:" +
|
||||
http2_Server.getAddress().getPort() + "/")));
|
||||
}
|
||||
}
|
||||
return arguments.stream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a single instance of a HttpClient and issues multiple requests to {@code requestURI}
|
||||
* and expects that each of the request internally uses the same connection
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("requestURIs")
|
||||
public void testConnReuse(final URI requestURI) throws Exception {
|
||||
final HttpClient.Builder builder = HttpClient.newBuilder()
|
||||
.proxy(NO_PROXY).sslContext(sslContext);
|
||||
final HttpRequest req = HttpRequest.newBuilder().uri(requestURI)
|
||||
.GET().version(HTTP_2).build();
|
||||
try (final HttpClient client = builder.build()) {
|
||||
String clientConnAddr = null;
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
System.out.println("Issuing request(" + i + ") " + req);
|
||||
final HttpResponse<String> resp = client.send(req, BodyHandlers.ofString());
|
||||
assertEquals(200, resp.statusCode(), "unexpected response code");
|
||||
final String respBody = resp.body();
|
||||
System.out.println("Server side handler responded to a request from " + respBody);
|
||||
assertNotEquals(Handler.UNKNOWN_CLIENT_ADDR, respBody,
|
||||
"server handler couldn't determine client address in request");
|
||||
if (i == 1) {
|
||||
// for the first request we just keep track of the client connection address
|
||||
// that got used for this request
|
||||
clientConnAddr = respBody;
|
||||
} else {
|
||||
// verify that the client connection used to issue the request is the same
|
||||
// as the previous request's client connection
|
||||
assertEquals(clientConnAddr, respBody, "HttpClient unexpectedly used a" +
|
||||
" different connection for request(" + i + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Handler implements HttpTestHandler {
|
||||
|
||||
private static final String UNKNOWN_CLIENT_ADDR = "unknown";
|
||||
|
||||
@Override
|
||||
public void handle(final HttpTestExchange t) throws IOException {
|
||||
final InetSocketAddress clientAddr = t.getRemoteAddress();
|
||||
System.out.println("Handling request " + t.getRequestURI() + " from " + clientAddr);
|
||||
// we write out the client address into the response body
|
||||
final byte[] responseBody = clientAddr == null
|
||||
? UNKNOWN_CLIENT_ADDR.getBytes(StandardCharsets.UTF_8)
|
||||
: clientAddr.toString().getBytes(StandardCharsets.UTF_8);
|
||||
t.sendResponseHeaders(200, responseBody.length);
|
||||
try (final OutputStream os = t.getResponseBody()) {
|
||||
os.write(responseBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
319
test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java
Normal file
319
test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
/*
|
||||
* Copyright (c) 2017, 2025, 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
|
||||
* @summary Test for CONTINUATION frame handling
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext
|
||||
* @compile ../ReferenceTracker.java
|
||||
* @run junit/othervm ContinuationFrameTest
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
|
||||
import jdk.internal.net.http.common.HttpHeadersBuilder;
|
||||
import jdk.internal.net.http.frame.ContinuationFrame;
|
||||
import jdk.internal.net.http.frame.HeaderFrame;
|
||||
import jdk.internal.net.http.frame.HeadersFrame;
|
||||
import jdk.internal.net.http.frame.Http2Frame;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
import jdk.httpclient.test.lib.http2.BodyOutputStream;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
|
||||
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import static java.lang.System.out;
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class ContinuationFrameTest {
|
||||
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
private static Http2TestServer http2TestServer; // HTTP/2 ( h2c )
|
||||
private static Http2TestServer https2TestServer; // HTTP/2 ( h2 )
|
||||
private static String http2URI;
|
||||
private static String https2URI;
|
||||
private static String noBodyhttp2URI;
|
||||
private static String noBodyhttps2URI;
|
||||
private final static ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
|
||||
|
||||
/**
|
||||
* A function that returns a list of 1) a HEADERS frame ( with an empty
|
||||
* payload ), and 2) a CONTINUATION frame with the actual headers.
|
||||
*/
|
||||
static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> oneContinuation =
|
||||
(Integer streamid, List<ByteBuffer> encodedHeaders) -> {
|
||||
List<ByteBuffer> empty = List.of(ByteBuffer.wrap(new byte[0]));
|
||||
HeadersFrame hf = new HeadersFrame(streamid, 0, empty);
|
||||
ContinuationFrame cf = new ContinuationFrame(streamid,
|
||||
HeaderFrame.END_HEADERS,
|
||||
encodedHeaders);
|
||||
return List.of(hf, cf);
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that returns a list of 1) a HEADERS frame with END_STREAM
|
||||
* ( and with an empty payload ), and 2) two CONTINUATION frames,the first
|
||||
* is empty and the second contains headers and the END_HEADERS flag
|
||||
*/
|
||||
static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> twoContinuation =
|
||||
(Integer streamid, List<ByteBuffer> encodedHeaders) -> {
|
||||
List<ByteBuffer> empty = List.of(ByteBuffer.wrap(new byte[0]));
|
||||
HeadersFrame hf = new HeadersFrame(streamid, HeaderFrame.END_STREAM, empty);
|
||||
ContinuationFrame cf = new ContinuationFrame(streamid, 0,empty);
|
||||
ContinuationFrame cf1 = new ContinuationFrame(streamid,
|
||||
HeaderFrame.END_HEADERS,
|
||||
encodedHeaders);
|
||||
|
||||
return List.of(hf, cf, cf1);
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that returns a list of a HEADERS frame followed by a number of
|
||||
* CONTINUATION frames. Each frame contains just a single byte of payload.
|
||||
*/
|
||||
static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> byteAtATime =
|
||||
(Integer streamid, List<ByteBuffer> encodedHeaders) -> {
|
||||
assert encodedHeaders.get(0).hasRemaining();
|
||||
List<Http2Frame> frames = new ArrayList<>();
|
||||
ByteBuffer hb = ByteBuffer.wrap(new byte[] {encodedHeaders.get(0).get()});
|
||||
HeadersFrame hf = new HeadersFrame(streamid, 0, hb);
|
||||
frames.add(hf);
|
||||
for (ByteBuffer bb : encodedHeaders) {
|
||||
while (bb.hasRemaining()) {
|
||||
List<ByteBuffer> data = List.of(ByteBuffer.wrap(new byte[] {bb.get()}));
|
||||
ContinuationFrame cf = new ContinuationFrame(streamid, 0, data);
|
||||
frames.add(cf);
|
||||
}
|
||||
}
|
||||
frames.get(frames.size() - 1).setFlag(HeaderFrame.END_HEADERS);
|
||||
return frames;
|
||||
};
|
||||
|
||||
static Object[][] variants() {
|
||||
return new Object[][] {
|
||||
{ http2URI, false, oneContinuation },
|
||||
{ https2URI, false, oneContinuation },
|
||||
{ http2URI, true, oneContinuation },
|
||||
{ https2URI, true, oneContinuation },
|
||||
|
||||
{ noBodyhttp2URI, false, twoContinuation },
|
||||
{ noBodyhttp2URI, true, twoContinuation },
|
||||
{ noBodyhttps2URI, false, twoContinuation },
|
||||
{ noBodyhttps2URI, true, twoContinuation },
|
||||
|
||||
{ http2URI, false, byteAtATime },
|
||||
{ https2URI, false, byteAtATime },
|
||||
{ http2URI, true, byteAtATime },
|
||||
{ https2URI, true, byteAtATime },
|
||||
};
|
||||
}
|
||||
|
||||
static final int ITERATION_COUNT = 20;
|
||||
|
||||
static HttpClient sharedClient;
|
||||
HttpClient httpClient(boolean shared) {
|
||||
if (!shared || sharedClient == null) {
|
||||
var client = HttpClient.newBuilder()
|
||||
.proxy(HttpClient.Builder.NO_PROXY)
|
||||
.sslContext(sslContext)
|
||||
.build();
|
||||
if (sharedClient == null) {
|
||||
sharedClient = client;
|
||||
}
|
||||
TRACKER.track(client);
|
||||
return client;
|
||||
}
|
||||
return sharedClient;
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("variants")
|
||||
void test(String uri,
|
||||
boolean sameClient,
|
||||
BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)
|
||||
throws Exception
|
||||
{
|
||||
CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier);
|
||||
|
||||
HttpClient client = null;
|
||||
for (int i=0; i< ITERATION_COUNT; i++) {
|
||||
if (!sameClient || client == null) {
|
||||
client = httpClient(sameClient);
|
||||
}
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
|
||||
.POST(BodyPublishers.ofString("Hello there!"))
|
||||
.build();
|
||||
HttpResponse<String> resp;
|
||||
if (i % 2 == 0) {
|
||||
resp = client.send(request, BodyHandlers.ofString());
|
||||
} else {
|
||||
resp = client.sendAsync(request, BodyHandlers.ofString()).join();
|
||||
}
|
||||
|
||||
if(uri.contains("nobody")) {
|
||||
out.println("Got response: " + resp);
|
||||
assertEquals(204, resp.statusCode(), "Expected 204, got:" + resp.statusCode());
|
||||
assertEquals(HTTP_2, resp.version());
|
||||
continue;
|
||||
}
|
||||
out.println("Got response: " + resp);
|
||||
out.println("Got body: " + resp.body());
|
||||
assertEquals(200, resp.statusCode(), "Expected 200, got:" + resp.statusCode());
|
||||
assertEquals("Hello there!", resp.body());
|
||||
assertEquals(HTTP_2, resp.version());
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws Exception {
|
||||
http2TestServer = new Http2TestServer("localhost", false, 0);
|
||||
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
|
||||
http2TestServer.addHandler(new Http2NoBodyHandler(), "/http2/nobody");
|
||||
int port = http2TestServer.getAddress().getPort();
|
||||
http2URI = "http://localhost:" + port + "/http2/echo";
|
||||
noBodyhttp2URI = "http://localhost:" + port + "/http2/nobody";
|
||||
|
||||
https2TestServer = new Http2TestServer("localhost", true, sslContext);
|
||||
https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
|
||||
https2TestServer.addHandler(new Http2NoBodyHandler(), "/https2/nobody");
|
||||
port = https2TestServer.getAddress().getPort();
|
||||
https2URI = "https://localhost:" + port + "/https2/echo";
|
||||
noBodyhttps2URI = "https://localhost:" + port + "/https2/nobody";
|
||||
|
||||
// Override the default exchange supplier with a custom one to enable
|
||||
// particular test scenarios
|
||||
http2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new);
|
||||
https2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new);
|
||||
|
||||
http2TestServer.start();
|
||||
https2TestServer.start();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void teardown() throws Exception {
|
||||
sharedClient = null;
|
||||
AssertionError fail = TRACKER.check(500);
|
||||
try {
|
||||
http2TestServer.stop();
|
||||
https2TestServer.stop();
|
||||
} finally {
|
||||
if (fail != null) {
|
||||
throw fail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class Http2EchoHandler implements Http2Handler {
|
||||
@Override
|
||||
public void handle(Http2TestExchange t) throws IOException {
|
||||
try (InputStream is = t.getRequestBody();
|
||||
OutputStream os = t.getResponseBody()) {
|
||||
byte[] bytes = is.readAllBytes();
|
||||
t.getResponseHeaders().addHeader("justSome", "Noise");
|
||||
t.getResponseHeaders().addHeader("toAdd", "payload in");
|
||||
t.getResponseHeaders().addHeader("theHeader", "Frames");
|
||||
t.sendResponseHeaders(200, bytes.length);
|
||||
os.write(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class Http2NoBodyHandler implements Http2Handler {
|
||||
@Override
|
||||
public void handle(Http2TestExchange t) throws IOException {
|
||||
try (InputStream is = t.getRequestBody();
|
||||
OutputStream os = t.getResponseBody()) {
|
||||
byte[] bytes = is.readAllBytes();
|
||||
t.sendResponseHeaders(204, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A custom Http2TestExchangeImpl that overrides sendResponseHeaders to
|
||||
// allow headers to be sent with a number of CONTINUATION frames.
|
||||
static class CFTHttp2TestExchange extends Http2TestExchangeImpl {
|
||||
static volatile BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFrameSupplier;
|
||||
|
||||
static void setHeaderFrameSupplier(BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> hfs) {
|
||||
headerFrameSupplier = hfs;
|
||||
}
|
||||
|
||||
CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
|
||||
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
|
||||
SSLSession sslSession, BodyOutputStream os,
|
||||
Http2TestServerConnection conn, boolean pushAllowed) {
|
||||
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession,
|
||||
os, conn, pushAllowed);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
|
||||
this.responseLength = responseLength;
|
||||
if (responseLength != 0 && rCode != 204) {
|
||||
long clen = responseLength > 0 ? responseLength : 0;
|
||||
rspheadersBuilder.setHeader("Content-length", Long.toString(clen));
|
||||
}
|
||||
rspheadersBuilder.setHeader(":status", Integer.toString(rCode));
|
||||
HttpHeaders headers = rspheadersBuilder.build();
|
||||
|
||||
List<ByteBuffer> encodeHeaders = conn.encodeHeaders(headers);
|
||||
List<Http2Frame> headerFrames = headerFrameSupplier.apply(streamid, encodeHeaders);
|
||||
assert headerFrames.size() > 0; // there must always be at least 1
|
||||
|
||||
if(headerFrames.get(0).getFlag(HeaderFrame.END_STREAM))
|
||||
os.markClosed();
|
||||
|
||||
for (Http2Frame f : headerFrames) {
|
||||
conn.addToOutputQ(f);
|
||||
}
|
||||
|
||||
os.goodToGo();
|
||||
System.err.println("Sent response headers " + rCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
129
test/jdk/java/net/httpclient/http2/ErrorTest.java
Normal file
129
test/jdk/java/net/httpclient/http2/ErrorTest.java
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright (c) 2015, 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
|
||||
* @bug 8157105
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* jdk.test.lib.Asserts
|
||||
* jdk.test.lib.net.SimpleSSLContext
|
||||
* @modules java.base/sun.net.www.http
|
||||
* java.net.http/jdk.internal.net.http.common
|
||||
* java.net.http/jdk.internal.net.http.frame
|
||||
* java.net.http/jdk.internal.net.http.hpack
|
||||
* java.base/jdk.internal.net.quic
|
||||
* java.net.http/jdk.internal.net.http.quic
|
||||
* java.net.http/jdk.internal.net.http.quic.packets
|
||||
* java.net.http/jdk.internal.net.http.quic.frames
|
||||
* java.net.http/jdk.internal.net.http.quic.streams
|
||||
* java.net.http/jdk.internal.net.http.http3.streams
|
||||
* java.net.http/jdk.internal.net.http.http3.frames
|
||||
* java.net.http/jdk.internal.net.http.http3
|
||||
* java.net.http/jdk.internal.net.http.qpack
|
||||
* java.net.http/jdk.internal.net.http.qpack.readers
|
||||
* java.net.http/jdk.internal.net.http.qpack.writers
|
||||
* java.security.jgss
|
||||
* @modules java.base/jdk.internal.util
|
||||
* @run junit/othervm/timeout=60 -Djavax.net.debug=ssl -Djdk.httpclient.HttpClient.log=all ErrorTest
|
||||
* @summary check exception thrown when bad TLS parameters selected
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* When selecting an unacceptable cipher suite the TLS handshake will fail.
|
||||
* But, the exception that was thrown was not being returned up to application
|
||||
* causing hang problems
|
||||
*/
|
||||
public class ErrorTest implements HttpServerAdapters {
|
||||
|
||||
static final String[] CIPHER_SUITES = new String[]{ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" };
|
||||
|
||||
static final String SIMPLE_STRING = "Hello world Goodbye world";
|
||||
|
||||
//@Test(timeOut=5000)
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
ExecutorService exec = Executors.newCachedThreadPool();
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.executor(exec)
|
||||
.sslContext(sslContext)
|
||||
.sslParameters(new SSLParameters(CIPHER_SUITES))
|
||||
.version(HTTP_2)
|
||||
.build();
|
||||
|
||||
HttpTestServer httpsServer = null;
|
||||
try {
|
||||
SSLContext serverContext = SimpleSSLContext.findSSLContext();
|
||||
SSLParameters p = serverContext.getSupportedSSLParameters();
|
||||
p.setApplicationProtocols(new String[]{"h2"});
|
||||
Http2TestServer httpsServerImpl = new Http2TestServer(true,
|
||||
0,
|
||||
exec,
|
||||
serverContext);
|
||||
httpsServer = HttpTestServer.of(httpsServerImpl);
|
||||
httpsServer.addHandler(new HttpTestFileEchoHandler(), "/");
|
||||
int httpsPort = httpsServer.getAddress().getPort();
|
||||
String httpsURIString = "https://localhost:" + httpsPort + "/bar/";
|
||||
|
||||
httpsServer.start();
|
||||
URI uri = URI.create(httpsURIString);
|
||||
System.err.println("Request to " + uri);
|
||||
|
||||
HttpRequest req = HttpRequest.newBuilder(uri)
|
||||
.POST(BodyPublishers.ofString(SIMPLE_STRING))
|
||||
.build();
|
||||
HttpResponse response;
|
||||
try {
|
||||
response = client.send(req, BodyHandlers.discarding());
|
||||
throw new RuntimeException("Unexpected response: " + response);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Caught Expected IOException: " + e);
|
||||
}
|
||||
System.err.println("DONE");
|
||||
} finally {
|
||||
if (httpsServer != null ) { httpsServer.stop(); }
|
||||
exec.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
263
test/jdk/java/net/httpclient/http2/FixedThreadPoolTest.java
Normal file
263
test/jdk/java/net/httpclient/http2/FixedThreadPoolTest.java
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
/*
|
||||
* Copyright (c) 2015, 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
|
||||
* @bug 8087112 8177935
|
||||
* @library /test/jdk/java/net/httpclient/lib
|
||||
* /test/lib
|
||||
* @build jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* jdk.test.lib.Asserts
|
||||
* jdk.test.lib.Utils
|
||||
* jdk.test.lib.net.SimpleSSLContext
|
||||
* @run junit/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors FixedThreadPoolTest
|
||||
*/
|
||||
|
||||
import java.net.*;
|
||||
import java.net.http.*;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import javax.net.ssl.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
import static jdk.test.lib.Asserts.assertFileContentsEqual;
|
||||
import static jdk.test.lib.Utils.createTempFile;
|
||||
import static jdk.test.lib.Utils.createTempFileOfSize;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class FixedThreadPoolTest implements HttpServerAdapters {
|
||||
|
||||
private static final String TEMP_FILE_PREFIX =
|
||||
HttpClient.class.getPackageName() + '-' + FixedThreadPoolTest.class.getSimpleName() + '-';
|
||||
|
||||
static int httpPort, httpsPort;
|
||||
static HttpTestServer httpServer, httpsServer;
|
||||
static HttpClient client = null;
|
||||
static ExecutorService exec;
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
|
||||
static String httpURIString, httpsURIString;
|
||||
|
||||
static void initialize() throws Exception {
|
||||
try {
|
||||
client = getClient();
|
||||
httpServer = HttpTestServer.of(
|
||||
new Http2TestServer(false, 0, exec, sslContext));
|
||||
httpServer.addHandler(new HttpTestFileEchoHandler(), "/");
|
||||
httpPort = httpServer.getAddress().getPort();
|
||||
|
||||
httpsServer = HttpTestServer.of(
|
||||
new Http2TestServer(true, 0, exec, sslContext));
|
||||
httpsServer.addHandler(new HttpTestFileEchoHandler(), "/");
|
||||
|
||||
httpsPort = httpsServer.getAddress().getPort();
|
||||
httpURIString = "http://" + httpServer.serverAuthority() + "/foo/";
|
||||
httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/";
|
||||
|
||||
httpServer.start();
|
||||
httpsServer.start();
|
||||
} catch (Throwable e) {
|
||||
System.err.println("Throwing now");
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
try {
|
||||
initialize();
|
||||
simpleTest(false);
|
||||
simpleTest(true);
|
||||
streamTest(false);
|
||||
streamTest(true);
|
||||
paramsTest();
|
||||
Thread.sleep(1000 * 4);
|
||||
} catch (Exception | Error tt) {
|
||||
tt.printStackTrace();
|
||||
throw tt;
|
||||
} finally {
|
||||
httpServer.stop();
|
||||
httpsServer.stop();
|
||||
exec.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
static HttpClient getClient() {
|
||||
if (client == null) {
|
||||
exec = Executors.newCachedThreadPool();
|
||||
// Executor e1 = Executors.newFixedThreadPool(1);
|
||||
// Executor e = (Runnable r) -> e1.execute(() -> {
|
||||
// System.out.println("[" + Thread.currentThread().getName()
|
||||
// + "] Executing: "
|
||||
// + r.getClass().getName());
|
||||
// r.run();
|
||||
// });
|
||||
client = HttpClient.newBuilder()
|
||||
.executor(Executors.newFixedThreadPool(2))
|
||||
.sslContext(sslContext)
|
||||
.version(HTTP_2)
|
||||
.build();
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
static URI getURI(boolean secure) {
|
||||
if (secure)
|
||||
return URI.create(httpsURIString);
|
||||
else
|
||||
return URI.create(httpURIString);
|
||||
}
|
||||
|
||||
static void checkStatus(int expected, int found) throws Exception {
|
||||
if (expected != found) {
|
||||
System.err.printf ("Test failed: wrong status code %d/%d\n",
|
||||
expected, found);
|
||||
throw new RuntimeException("Test failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void checkStrings(String expected, String found) throws Exception {
|
||||
if (!expected.equals(found)) {
|
||||
System.err.printf ("Test failed: wrong string %s/%s\n",
|
||||
expected, found);
|
||||
throw new RuntimeException("Test failed");
|
||||
}
|
||||
}
|
||||
|
||||
static final String SIMPLE_STRING = "Hello world Goodbye world";
|
||||
|
||||
static final int LOOPS = 32;
|
||||
static final int FILESIZE = 64 * 1024 + 200;
|
||||
|
||||
static void streamTest(boolean secure) throws Exception {
|
||||
URI uri = getURI(secure);
|
||||
System.err.printf("streamTest %b to %s\n" , secure, uri);
|
||||
|
||||
HttpClient client = getClient();
|
||||
Path src = createTempFileOfSize(TEMP_FILE_PREFIX, null, FILESIZE * 4);
|
||||
HttpRequest req = HttpRequest.newBuilder(uri)
|
||||
.POST(BodyPublishers.ofFile(src))
|
||||
.build();
|
||||
|
||||
Path dest = Paths.get("streamtest.txt");
|
||||
dest.toFile().delete();
|
||||
CompletableFuture<Path> response = client.sendAsync(req, BodyHandlers.ofFile(dest))
|
||||
.thenApply(resp -> {
|
||||
if (resp.statusCode() != 200)
|
||||
throw new RuntimeException();
|
||||
return resp.body();
|
||||
});
|
||||
response.join();
|
||||
assertFileContentsEqual(src, dest);
|
||||
System.err.println("DONE");
|
||||
}
|
||||
|
||||
// expect highest supported version we know about
|
||||
static String expectedTLSVersion(SSLContext ctx) {
|
||||
SSLParameters params = ctx.getSupportedSSLParameters();
|
||||
String[] protocols = params.getProtocols();
|
||||
for (String prot : protocols) {
|
||||
if (prot.equals("TLSv1.3"))
|
||||
return "TLSv1.3";
|
||||
}
|
||||
return "TLSv1.2";
|
||||
}
|
||||
|
||||
static void paramsTest() throws Exception {
|
||||
System.err.println("paramsTest");
|
||||
Http2TestServer server = new Http2TestServer(true, 0, exec, sslContext);
|
||||
server.addHandler(((Http2TestExchange t) -> {
|
||||
SSLSession s = t.getSSLSession();
|
||||
String prot = s.getProtocol();
|
||||
if (prot.equals(expectedTLSVersion(sslContext))) {
|
||||
t.sendResponseHeaders(200, -1);
|
||||
} else {
|
||||
System.err.printf("Protocols =%s\n", prot);
|
||||
t.sendResponseHeaders(500, -1);
|
||||
}
|
||||
}), "/");
|
||||
server.start();
|
||||
int port = server.getAddress().getPort();
|
||||
URI u = new URI("https://localhost:"+port+"/foo");
|
||||
HttpClient client = getClient();
|
||||
HttpRequest req = HttpRequest.newBuilder(u).build();
|
||||
HttpResponse<String> resp = client.sendAsync(req, BodyHandlers.ofString()).get();
|
||||
int stat = resp.statusCode();
|
||||
if (stat != 200) {
|
||||
throw new RuntimeException("paramsTest failed "
|
||||
+ Integer.toString(stat));
|
||||
}
|
||||
}
|
||||
|
||||
static void simpleTest(boolean secure) throws Exception {
|
||||
URI uri = getURI(secure);
|
||||
System.err.println("Request to " + uri);
|
||||
|
||||
// Do a simple warmup request
|
||||
|
||||
HttpClient client = getClient();
|
||||
HttpRequest req = HttpRequest.newBuilder(uri)
|
||||
.POST(BodyPublishers.ofString(SIMPLE_STRING))
|
||||
.build();
|
||||
HttpResponse<String> response = client.sendAsync(req, BodyHandlers.ofString()).get();
|
||||
HttpHeaders h = response.headers();
|
||||
|
||||
checkStatus(200, response.statusCode());
|
||||
|
||||
String responseBody = response.body();
|
||||
checkStrings(SIMPLE_STRING, responseBody);
|
||||
|
||||
checkStrings(h.firstValue("x-hello").get(), "world");
|
||||
checkStrings(h.firstValue("x-bye").get(), "universe");
|
||||
|
||||
// Do loops asynchronously
|
||||
|
||||
CompletableFuture[] responses = new CompletableFuture[LOOPS];
|
||||
final Path source = createTempFileOfSize(TEMP_FILE_PREFIX, null, FILESIZE);
|
||||
HttpRequest request = HttpRequest.newBuilder(uri)
|
||||
.POST(BodyPublishers.ofFile(source))
|
||||
.build();
|
||||
for (int i = 0; i < LOOPS; i++) {
|
||||
responses[i] = client.sendAsync(request, BodyHandlers.ofFile(createTempFile(TEMP_FILE_PREFIX, null)))
|
||||
//.thenApply(resp -> compareFiles(resp.body(), source));
|
||||
.thenApply(resp -> {
|
||||
Path body = resp.body();
|
||||
System.out.printf("Resp status %d body size %d\n",
|
||||
resp.statusCode(), body.toFile().length());
|
||||
assertFileContentsEqual(body, source);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
CompletableFuture.allOf(responses).join();
|
||||
System.err.println("DONE");
|
||||
}
|
||||
}
|
||||
338
test/jdk/java/net/httpclient/http2/H2GoAwayTest.java
Normal file
338
test/jdk/java/net/httpclient/http2/H2GoAwayTest.java
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
/*
|
||||
* Copyright (c) 2024, 2025, 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.
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange;
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler;
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
import jdk.test.lib.net.URIBuilder;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8335181
|
||||
* @summary verify that the HttpClient correctly handles incoming GOAWAY frames and
|
||||
* retries any unprocessed requests on a new connection
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.httpclient.test.lib.common.HttpServerAdapters
|
||||
* jdk.test.lib.net.SimpleSSLContext
|
||||
* @run junit H2GoAwayTest
|
||||
*/
|
||||
public class H2GoAwayTest {
|
||||
private static final String REQ_PATH = "/test";
|
||||
private static HttpTestServer server;
|
||||
private static String REQ_URI_BASE;
|
||||
private static final SSLContext sslCtx = SimpleSSLContext.findSSLContext();
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() throws Exception {
|
||||
server = HttpTestServer.create(HTTP_2, sslCtx);
|
||||
server.addHandler(new Handler(), REQ_PATH);
|
||||
server.start();
|
||||
System.out.println("Server started at " + server.getAddress());
|
||||
REQ_URI_BASE = URIBuilder.newBuilder().scheme("https")
|
||||
.loopback()
|
||||
.port(server.getAddress().getPort())
|
||||
.path(REQ_PATH)
|
||||
.build().toString();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
if (server != null) {
|
||||
System.out.println("Stopping server at " + server.getAddress());
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that when several requests are sent using send() and the server
|
||||
* connection is configured to send a GOAWAY after processing only a few requests, then
|
||||
* the remaining requests are retried on a different connection
|
||||
*/
|
||||
@Test
|
||||
public void testSequential() throws Exception {
|
||||
final LimitedPerConnRequestApprover reqApprover = new LimitedPerConnRequestApprover();
|
||||
server.setRequestApprover(reqApprover::allowNewRequest);
|
||||
try (final HttpClient client = HttpClient.newBuilder().version(HTTP_2)
|
||||
.sslContext(sslCtx).build()) {
|
||||
final String[] reqMethods = {"HEAD", "GET", "POST"};
|
||||
for (final String reqMethod : reqMethods) {
|
||||
final int numReqs = LimitedPerConnRequestApprover.MAX_REQS_PER_CONN + 3;
|
||||
final Set<String> connectionKeys = new LinkedHashSet<>();
|
||||
for (int i = 1; i <= numReqs; i++) {
|
||||
final URI reqURI = new URI(REQ_URI_BASE + "?seq&" + reqMethod + "=" + i);
|
||||
final HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(reqURI)
|
||||
.method(reqMethod, HttpRequest.BodyPublishers.noBody())
|
||||
.build();
|
||||
System.out.println("initiating request " + req);
|
||||
final HttpResponse<String> resp = client.send(req, BodyHandlers.ofString());
|
||||
final String respBody = resp.body();
|
||||
System.out.println("received response: " + respBody);
|
||||
assertEquals(200, resp.statusCode(),
|
||||
"unexpected status code for request " + resp.request());
|
||||
// response body is the logical key of the connection on which the
|
||||
// request was handled
|
||||
connectionKeys.add(respBody);
|
||||
}
|
||||
System.out.println("connections involved in handling the requests: "
|
||||
+ connectionKeys);
|
||||
// all requests have finished, we now just do a basic check that
|
||||
// more than one connection was involved in processing these requests
|
||||
assertEquals(2, connectionKeys.size(),
|
||||
"unexpected number of connections " + connectionKeys);
|
||||
}
|
||||
} finally {
|
||||
server.setRequestApprover(null); // reset
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that when a server responds with a GOAWAY and then never processes the new retried
|
||||
* requests on a new connection too, then the application code receives the request failure.
|
||||
* This tests the send() API of the HttpClient.
|
||||
*/
|
||||
@Test
|
||||
public void testUnprocessedRaisesException() throws Exception {
|
||||
try (final HttpClient client = HttpClient.newBuilder().version(HTTP_2)
|
||||
.sslContext(sslCtx).build()) {
|
||||
final Random random = new Random();
|
||||
final String[] reqMethods = {"HEAD", "GET", "POST"};
|
||||
for (final String reqMethod : reqMethods) {
|
||||
final int maxAllowedReqs = 2;
|
||||
final int numReqs = maxAllowedReqs + 3; // 3 more requests than max allowed
|
||||
// configure the approver
|
||||
final LimitedRequestApprover reqApprover = new LimitedRequestApprover(maxAllowedReqs);
|
||||
server.setRequestApprover(reqApprover::allowNewRequest);
|
||||
try {
|
||||
int numSuccess = 0;
|
||||
int numFailed = 0;
|
||||
for (int i = 1; i <= numReqs; i++) {
|
||||
final String reqQueryPart = "?sync&" + reqMethod + "=" + i;
|
||||
final URI reqURI = new URI(REQ_URI_BASE + reqQueryPart);
|
||||
final HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(reqURI)
|
||||
.method(reqMethod, HttpRequest.BodyPublishers.noBody())
|
||||
.build();
|
||||
System.out.println("initiating request " + req);
|
||||
if (i <= maxAllowedReqs) {
|
||||
// expected to successfully complete
|
||||
numSuccess++;
|
||||
final HttpResponse<String> resp = client.send(req, BodyHandlers.ofString());
|
||||
final String respBody = resp.body();
|
||||
System.out.println("received response: " + respBody);
|
||||
assertEquals(200, resp.statusCode(),
|
||||
"unexpected status code for request " + resp.request());
|
||||
} else {
|
||||
// expected to fail as unprocessed
|
||||
try {
|
||||
final HttpResponse<String> resp = client.send(req, BodyHandlers.ofString());
|
||||
fail("Request was expected to fail as unprocessed,"
|
||||
+ " but got response: " + resp.body() + ", status code: "
|
||||
+ resp.statusCode());
|
||||
} catch (IOException ioe) {
|
||||
// verify it failed for the right reason
|
||||
if (ioe.getMessage() == null
|
||||
|| !ioe.getMessage().contains("request not processed by peer")) {
|
||||
// propagate the original failure
|
||||
throw ioe;
|
||||
}
|
||||
numFailed++; // failed due to right reason
|
||||
System.out.println("received expected failure: " + ioe
|
||||
+ ", for request " + reqURI);
|
||||
}
|
||||
}
|
||||
}
|
||||
// verify the correct number of requests succeeded/failed
|
||||
assertEquals(maxAllowedReqs, numSuccess, "unexpected number of requests succeeded");
|
||||
assertEquals((numReqs - maxAllowedReqs), numFailed, "unexpected number of requests failed");
|
||||
} finally {
|
||||
server.setRequestApprover(null); // reset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that when a server responds with a GOAWAY and then never processes the new retried
|
||||
* requests on a new connection too, then the application code receives the request failure.
|
||||
* This tests the sendAsync() API of the HttpClient.
|
||||
*/
|
||||
@Test
|
||||
public void testUnprocessedRaisesExceptionAsync() throws Throwable {
|
||||
try (final HttpClient client = HttpClient.newBuilder().version(HTTP_2)
|
||||
.sslContext(sslCtx).build()) {
|
||||
final Random random = new Random();
|
||||
final String[] reqMethods = {"HEAD", "GET", "POST"};
|
||||
for (final String reqMethod : reqMethods) {
|
||||
final int maxAllowedReqs = 2;
|
||||
final int numReqs = maxAllowedReqs + 3; // 3 more requests than max allowed
|
||||
// configure the approver
|
||||
final LimitedRequestApprover reqApprover = new LimitedRequestApprover(maxAllowedReqs);
|
||||
server.setRequestApprover(reqApprover::allowNewRequest);
|
||||
try {
|
||||
final List<Future<HttpResponse<String>>> futures = new ArrayList<>();
|
||||
for (int i = 1; i <= numReqs; i++) {
|
||||
final URI reqURI = new URI(REQ_URI_BASE + "?async&" + reqMethod + "=" + i);
|
||||
final HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(reqURI)
|
||||
.method(reqMethod, HttpRequest.BodyPublishers.noBody())
|
||||
.build();
|
||||
System.out.println("initiating request " + req);
|
||||
final Future<HttpResponse<String>> f = client.sendAsync(req, BodyHandlers.ofString());
|
||||
futures.add(f);
|
||||
}
|
||||
// wait for responses
|
||||
int numFailed = 0;
|
||||
int numSuccess = 0;
|
||||
for (int i = 1; i <= numReqs; i++) {
|
||||
final String reqQueryPart = "?async&" + reqMethod + "=" + i;
|
||||
try {
|
||||
System.out.println("waiting response of request "
|
||||
+ REQ_URI_BASE + reqQueryPart);
|
||||
final HttpResponse<String> resp = futures.get(i - 1).get();
|
||||
numSuccess++;
|
||||
final String respBody = resp.body();
|
||||
System.out.println("request: " + resp.request()
|
||||
+ ", received response: " + respBody);
|
||||
assertEquals(200, resp.statusCode(),
|
||||
"unexpected status code for request " + resp.request());
|
||||
} catch (ExecutionException ee) {
|
||||
final Throwable cause = ee.getCause();
|
||||
if (!(cause instanceof IOException ioe)) {
|
||||
System.err.println("unexpected exception: " + cause
|
||||
+ ", for request " + REQ_URI_BASE + reqQueryPart);
|
||||
throw cause;
|
||||
}
|
||||
// verify it failed for the right reason
|
||||
if (ioe.getMessage() == null
|
||||
|| !ioe.getMessage().contains("request not processed by peer")) {
|
||||
System.err.println("unexpected exception message: " + ioe.getMessage()
|
||||
+ ", for request " + REQ_URI_BASE + reqQueryPart);
|
||||
// propagate the original failure
|
||||
throw ioe;
|
||||
}
|
||||
numFailed++; // failed due to the right reason
|
||||
System.out.println("received expected failure: " + ioe
|
||||
+ ", for request " + REQ_URI_BASE + reqQueryPart);
|
||||
}
|
||||
}
|
||||
// verify the correct number of requests succeeded/failed
|
||||
assertEquals(maxAllowedReqs, numSuccess, "unexpected number of requests succeeded");
|
||||
assertEquals((numReqs - maxAllowedReqs), numFailed, "unexpected number of requests failed");
|
||||
} finally {
|
||||
server.setRequestApprover(null); // reset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// only allows fixed number of requests, irrespective of which server connection handles
|
||||
// it. requests that are rejected will either be sent a GOAWAY on the connection
|
||||
// or a RST_FRAME with a REFUSED_STREAM on the stream
|
||||
private static final class LimitedRequestApprover {
|
||||
private final int maxAllowedReqs;
|
||||
private final AtomicInteger numApproved = new AtomicInteger();
|
||||
|
||||
private LimitedRequestApprover(final int maxAllowedReqs) {
|
||||
this.maxAllowedReqs = maxAllowedReqs;
|
||||
}
|
||||
|
||||
public boolean allowNewRequest(final String serverConnKey) {
|
||||
final int approved = numApproved.incrementAndGet();
|
||||
return approved <= maxAllowedReqs;
|
||||
}
|
||||
}
|
||||
|
||||
// allows a certain number of requests per server connection.
|
||||
// requests that are rejected will either be sent a GOAWAY on the connection
|
||||
// or a RST_FRAME with a REFUSED_STREAM on the stream
|
||||
private static final class LimitedPerConnRequestApprover {
|
||||
private static final int MAX_REQS_PER_CONN = 6;
|
||||
private final Map<String, AtomicInteger> numApproved =
|
||||
new ConcurrentHashMap<>();
|
||||
private final Map<String, AtomicInteger> numDisapproved =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
public boolean allowNewRequest(final String serverConnKey) {
|
||||
final AtomicInteger approved = numApproved.computeIfAbsent(serverConnKey,
|
||||
(k) -> new AtomicInteger());
|
||||
int curr = approved.get();
|
||||
while (curr < MAX_REQS_PER_CONN) {
|
||||
if (approved.compareAndSet(curr, curr + 1)) {
|
||||
return true; // new request allowed
|
||||
}
|
||||
curr = approved.get();
|
||||
}
|
||||
final AtomicInteger disapproved = numDisapproved.computeIfAbsent(serverConnKey,
|
||||
(k) -> new AtomicInteger());
|
||||
final int numUnprocessed = disapproved.incrementAndGet();
|
||||
System.out.println(approved.get() + " processed, "
|
||||
+ numUnprocessed + " unprocessed requests on connection " + serverConnKey);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Handler implements HttpTestHandler {
|
||||
|
||||
@Override
|
||||
public void handle(final HttpTestExchange exchange) throws IOException {
|
||||
final String connectionKey = exchange.getConnectionKey();
|
||||
System.out.println("responding to request: " + exchange.getRequestURI()
|
||||
+ " on connection " + connectionKey);
|
||||
final byte[] response = connectionKey.getBytes(UTF_8);
|
||||
exchange.sendResponseHeaders(200, response.length);
|
||||
try (final OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
224
test/jdk/java/net/httpclient/http2/H2SelectorVTTest.java
Normal file
224
test/jdk/java/net/httpclient/http2/H2SelectorVTTest.java
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
* Copyright (c) 2025, 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.
|
||||
*/
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
|
||||
/*
|
||||
* @test id=default
|
||||
* @bug 8372159
|
||||
* @summary Verifies whether `SelectorManager` uses virtual threads
|
||||
* as expected when no explicit configuration is provided
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.test.lib.net.SimpleSSLContext
|
||||
* jdk.httpclient.test.lib.common.HttpServerAdapters
|
||||
* @run junit/othervm
|
||||
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors
|
||||
* H2SelectorVTTest
|
||||
*/
|
||||
/*
|
||||
* @test id=never
|
||||
* @bug 8372159
|
||||
* @summary Verifies that `SelectorManager` does *not* use virtual threads
|
||||
when explicitly configured to "never" use them
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.test.lib.net.SimpleSSLContext
|
||||
* jdk.httpclient.test.lib.common.HttpServerAdapters
|
||||
* @run junit/othervm
|
||||
* -Djdk.internal.httpclient.tcp.selector.useVirtualThreads=never
|
||||
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors
|
||||
* H2SelectorVTTest
|
||||
*/
|
||||
/*
|
||||
* @test id=always
|
||||
* @bug 8372159
|
||||
* @summary Verifies that `SelectorManager` does *always* use virtual threads
|
||||
when explicitly configured to "always" use them
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.test.lib.net.SimpleSSLContext
|
||||
* jdk.httpclient.test.lib.common.HttpServerAdapters
|
||||
* @run junit/othervm
|
||||
* -Djdk.internal.httpclient.tcp.selector.useVirtualThreads=always
|
||||
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors
|
||||
* H2SelectorVTTest
|
||||
*/
|
||||
/*
|
||||
* @test id=explicit-default
|
||||
* @bug 8372159
|
||||
* @summary Verifies whether `SelectorManager` uses virtual threads
|
||||
* as expected when `default` is explicitly configured
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.test.lib.net.SimpleSSLContext
|
||||
* jdk.httpclient.test.lib.common.HttpServerAdapters
|
||||
* @run junit/othervm
|
||||
* -Djdk.internal.httpclient.tcp.selector.useVirtualThreads=default
|
||||
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors
|
||||
* H2SelectorVTTest
|
||||
*/
|
||||
/*
|
||||
* @test id=garbage
|
||||
* @bug 8372159
|
||||
* @summary Verifies whether `SelectorManager` uses virtual threads when
|
||||
it is configured using an invalid value
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.test.lib.net.SimpleSSLContext
|
||||
* jdk.httpclient.test.lib.common.HttpServerAdapters
|
||||
* @run junit/othervm
|
||||
* -Djdk.internal.httpclient.tcp.selector.useVirtualThreads=garbage
|
||||
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors
|
||||
* H2SelectorVTTest
|
||||
*/
|
||||
// -Djava.security.debug=all
|
||||
class H2SelectorVTTest implements HttpServerAdapters {
|
||||
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
private static HttpTestServer h2Server;
|
||||
private static String requestURI;
|
||||
|
||||
enum UseVTForSelector { ALWAYS, NEVER, DEFAULT }
|
||||
private static final String PROP_NAME = "jdk.internal.httpclient.tcp.selector.useVirtualThreads";
|
||||
private static final UseVTForSelector USE_VT_FOR_SELECTOR;
|
||||
static {
|
||||
String useVtForSelector =
|
||||
System.getProperty(PROP_NAME, "default");
|
||||
USE_VT_FOR_SELECTOR = Stream.of(UseVTForSelector.values())
|
||||
.filter((v) -> v.name().equalsIgnoreCase(useVtForSelector))
|
||||
.findFirst().orElse(UseVTForSelector.DEFAULT);
|
||||
}
|
||||
|
||||
private static boolean isTCPSelectorThreadVirtual() {
|
||||
return switch (USE_VT_FOR_SELECTOR) {
|
||||
case ALWAYS -> true;
|
||||
case NEVER -> false;
|
||||
default -> true;
|
||||
};
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
static void beforeClass() throws Exception {
|
||||
// create a h2 server
|
||||
h2Server = HttpTestServer.create(HTTP_2, sslContext);
|
||||
h2Server.addHandler((exchange) -> exchange.sendResponseHeaders(200, 0), "/hello");
|
||||
h2Server.start();
|
||||
System.out.println("Server started at " + h2Server.getAddress());
|
||||
requestURI = "https://" + h2Server.serverAuthority() + "/hello";
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterClass() throws Exception {
|
||||
if (h2Server != null) {
|
||||
System.out.println("Stopping server " + h2Server.getAddress());
|
||||
h2Server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Issues various HTTP/2 requests and verifies the responses are received
|
||||
*/
|
||||
@Test
|
||||
void testBasicRequests() throws Exception {
|
||||
try (final HttpClient client = HttpClient.newBuilder()
|
||||
.proxy(NO_PROXY)
|
||||
.sslContext(sslContext).build()) {
|
||||
final URI reqURI = new URI(requestURI);
|
||||
final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI);
|
||||
|
||||
// GET
|
||||
final HttpRequest req1 = reqBuilder.copy().GET().build();
|
||||
System.out.println("\nIssuing request: " + req1);
|
||||
final HttpResponse<?> resp1 = client.send(req1, BodyHandlers.ofString());
|
||||
assertEquals(200, resp1.statusCode(), "unexpected response code for GET request");
|
||||
assertSelectorThread(client);
|
||||
|
||||
// POST
|
||||
final HttpRequest req2 = reqBuilder.copy().POST(BodyPublishers.ofString("foo")).build();
|
||||
System.out.println("\nIssuing request: " + req2);
|
||||
final HttpResponse<?> resp2 = client.send(req2, BodyHandlers.ofString());
|
||||
assertEquals(200, resp2.statusCode(), "unexpected response code for POST request");
|
||||
assertSelectorThread(client);
|
||||
|
||||
// HEAD
|
||||
final HttpRequest req3 = reqBuilder.copy().HEAD().build();
|
||||
System.out.println("\nIssuing request: " + req3);
|
||||
final HttpResponse<?> resp3 = client.send(req3, BodyHandlers.ofString());
|
||||
assertEquals(200, resp3.statusCode(), "unexpected response code for HEAD request");
|
||||
assertSelectorThread(client);
|
||||
}
|
||||
}
|
||||
|
||||
// This method attempts to determine whether the selector thread
|
||||
// is a platform thread or a virtual thread, and throws if expectations
|
||||
// ar not met.
|
||||
// Since we don't have access to the selector thread, the method
|
||||
// uses a roundabout way to figure this out: it enumerates all
|
||||
// platform threads, and if it finds a thread whose name matches
|
||||
// the expected name of the selector thread it concludes that the
|
||||
// selector thread is a platform thread. Otherwise, it assumes
|
||||
// that the thread is virtual.
|
||||
private static void assertSelectorThread(HttpClient client) {
|
||||
String cname = client.toString();
|
||||
String clientId = cname.substring(cname.indexOf('(') + 1, cname.length() -1);
|
||||
String name = "HttpClient-" + clientId + "-SelectorManager";
|
||||
Set<String> threads = new HashSet<>(Thread.getAllStackTraces().keySet().stream()
|
||||
.map(Thread::getName)
|
||||
.toList());
|
||||
boolean found = threads.contains(name);
|
||||
String status = found == isTCPSelectorThreadVirtual() ? "ERROR" : "SUCCESS";
|
||||
String propval = System.getProperty(PROP_NAME);
|
||||
if (propval == null) {
|
||||
System.out.printf("%s not defined, virtual=%s, thread found=%s%n",
|
||||
PROP_NAME, isTCPSelectorThreadVirtual(), found);
|
||||
} else {
|
||||
System.out.printf("%s=%s, virtual=%s, thread found=%s%n",
|
||||
PROP_NAME, propval, isTCPSelectorThreadVirtual(), found);
|
||||
}
|
||||
final String msg;
|
||||
if (found) {
|
||||
msg = "%s found in %s".formatted(name, threads);
|
||||
System.out.printf("%s: %s%n", status, msg);
|
||||
} else {
|
||||
msg = "%s not found in %s".formatted(name, threads);
|
||||
System.out.printf("%s: %s%n", status, msg);
|
||||
}
|
||||
assertEquals(!isTCPSelectorThreadVirtual(), found, msg);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2017, 2025, 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
|
||||
* @bug 8153353
|
||||
* @modules java.net.http/jdk.internal.net.http.hpack
|
||||
* @key randomness
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
|
||||
* @run junit/othervm/timeout=240 java.net.http/jdk.internal.net.http.hpack.BinaryPrimitivesTest
|
||||
*/
|
||||
public class HpackBinaryTestDriver { }
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2025, 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
|
||||
* @bug 8153353
|
||||
* @modules java.net.http/jdk.internal.net.http.hpack
|
||||
* @key randomness
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
|
||||
* @run junit/othervm java.net.http/jdk.internal.net.http.hpack.CircularBufferTest
|
||||
*/
|
||||
public class HpackCircularBufferDriver { }
|
||||
34
test/jdk/java/net/httpclient/http2/HpackDecoderDriver.java
Normal file
34
test/jdk/java/net/httpclient/http2/HpackDecoderDriver.java
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2025, 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
|
||||
* @bug 8153353
|
||||
* @modules java.net.http/jdk.internal.net.http.hpack
|
||||
* @key randomness
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
|
||||
* @run junit/othervm java.net.http/jdk.internal.net.http.hpack.DecoderTest
|
||||
*/
|
||||
public class HpackDecoderDriver { }
|
||||
34
test/jdk/java/net/httpclient/http2/HpackEncoderDriver.java
Normal file
34
test/jdk/java/net/httpclient/http2/HpackEncoderDriver.java
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2025, 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
|
||||
* @bug 8153353
|
||||
* @modules java.net.http/jdk.internal.net.http.hpack
|
||||
* @key randomness
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
|
||||
* @run junit/othervm java.net.http/jdk.internal.net.http.hpack.EncoderTest
|
||||
*/
|
||||
public class HpackEncoderDriver { }
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2025, 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
|
||||
* @bug 8153353
|
||||
* @modules java.net.http/jdk.internal.net.http.hpack
|
||||
* jdk.localedata
|
||||
* @key randomness
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
|
||||
* @run junit/othervm java.net.http/jdk.internal.net.http.hpack.HeaderTableTest
|
||||
*/
|
||||
public class HpackHeaderTableDriver { }
|
||||
34
test/jdk/java/net/httpclient/http2/HpackHuffmanDriver.java
Normal file
34
test/jdk/java/net/httpclient/http2/HpackHuffmanDriver.java
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2025, 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
|
||||
* @bug 8153353
|
||||
* @modules java.net.http/jdk.internal.net.http.hpack
|
||||
* @key randomness
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
|
||||
* @run junit/othervm/timeout=300 java.net.http/jdk.internal.net.http.hpack.HuffmanTest
|
||||
*/
|
||||
public class HpackHuffmanDriver { }
|
||||
34
test/jdk/java/net/httpclient/http2/HpackTestHelper.java
Normal file
34
test/jdk/java/net/httpclient/http2/HpackTestHelper.java
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2025, 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
|
||||
* @bug 8153353
|
||||
* @modules java.net.http/jdk.internal.net.http.hpack
|
||||
* @key randomness
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java
|
||||
* @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java
|
||||
* @run junit/othervm java.net.http/jdk.internal.net.http.hpack.TestHelper
|
||||
*/
|
||||
public class HpackTestHelperDriver { }
|
||||
179
test/jdk/java/net/httpclient/http2/IdlePooledConnectionTest.java
Normal file
179
test/jdk/java/net/httpclient/http2/IdlePooledConnectionTest.java
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Copyright (c) 2023, 2025, 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.
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters;
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange;
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer;
|
||||
import jdk.internal.net.http.common.Utils;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8312433
|
||||
* @summary verify that the HttpClient's HTTP2 idle connection management doesn't close a connection
|
||||
* when that connection has been handed out from the pool to a caller
|
||||
* @library /test/jdk/java/net/httpclient/lib
|
||||
* /test/lib
|
||||
* @build jdk.httpclient.test.lib.common.HttpServerAdapters
|
||||
* jdk.test.lib.Asserts
|
||||
* @run junit/othervm -Djdk.internal.httpclient.debug=true
|
||||
* -Djdk.httpclient.keepalive.timeout.h2=3
|
||||
* IdlePooledConnectionTest
|
||||
*/
|
||||
public class IdlePooledConnectionTest {
|
||||
|
||||
private static final String ALL_OK_PATH = "/allOK";
|
||||
private static HttpTestServer h2Server;
|
||||
private static URI allOKUri;
|
||||
private static final String H2_KEEPALIVE_TIMEOUT_PROP = "jdk.httpclient.keepalive.timeout.h2";
|
||||
private static final String KEEPALIVE_TIMEOUT_PROP = "jdk.httpclient.keepalive.timeout";
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() throws Exception {
|
||||
h2Server = HttpTestServer.create(HTTP_2);
|
||||
h2Server.addHandler(new AllOKHandler(), ALL_OK_PATH);
|
||||
h2Server.start();
|
||||
System.err.println("Started H2 server at " + h2Server.serverAuthority());
|
||||
allOKUri = new URI("http://" + h2Server.serverAuthority() + ALL_OK_PATH);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() throws Exception {
|
||||
if (h2Server != null) {
|
||||
System.err.println("Stopping h2 server: " + h2Server.serverAuthority());
|
||||
h2Server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
// just returns a 200 HTTP response for all requests
|
||||
private static final class AllOKHandler implements HttpServerAdapters.HttpTestHandler {
|
||||
|
||||
@Override
|
||||
public void handle(final HttpTestExchange exchange) throws IOException {
|
||||
System.err.println("Responding with 200 response code for request "
|
||||
+ exchange.getRequestURI());
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Issues a HTTP2 request against a server and expects it to succeed.
|
||||
* The connection that was used is internally pooled by the HttpClient implementation.
|
||||
* Then waits for the H2 idle connection timeout, before again firing several concurrent HTTP2
|
||||
* requests against the same server. It is expected that all these requests complete
|
||||
* successfully without running into a race condition where the H2 idle connection management
|
||||
* closes the (pooled) connection during the time connection has been handed out to a caller
|
||||
* and a new stream hasn't yet been created.
|
||||
*/
|
||||
@Test
|
||||
public void testPooledConnection() throws Exception {
|
||||
final Duration h2TimeoutDuration = getEffectiveH2IdleTimeoutDuration();
|
||||
assertNotNull(h2TimeoutDuration, "H2 idle connection timeout cannot be null");
|
||||
// the wait time, which represents the time to wait before firing off additional requests,
|
||||
// is intentionally a few milliseconds smaller than the h2 idle connection timeout,
|
||||
// to allow for the requests to reach the place where connection checkout from the pool
|
||||
// happens and thus allow the code to race with the idle connection timer task
|
||||
// closing the connection.
|
||||
final long waitTimeMillis = TimeUnit.of(ChronoUnit.MILLIS).convert(h2TimeoutDuration) - 5;
|
||||
try (final HttpClient client = HttpClient.newBuilder().proxy(NO_PROXY).build()) {
|
||||
final HttpRequest request = HttpRequest.newBuilder(allOKUri)
|
||||
.GET().version(HTTP_2).build();
|
||||
// keep ready the additional concurrent requests that we will fire later.
|
||||
// we do this now so that when it's time to fire off these additional requests,
|
||||
// this main thread does as little work as possible to increase the chances of a
|
||||
// race condition in idle connection management closing a pooled connection
|
||||
// and new requests being fired
|
||||
final Callable<HttpResponse<Void>> task = () -> client.send(request,
|
||||
BodyHandlers.discarding());
|
||||
final List<Callable<HttpResponse<Void>>> tasks = new ArrayList<>();
|
||||
final int numAdditionalReqs = 20;
|
||||
for (int i = 0; i < numAdditionalReqs; i++) {
|
||||
tasks.add(task);
|
||||
}
|
||||
// issue the first request
|
||||
System.err.println("issuing first request: " + request);
|
||||
final HttpResponse<Void> firstResp = client.send(request, BodyHandlers.discarding());
|
||||
assertEquals(200, firstResp.statusCode(), "unexpected response code for request "
|
||||
+ request);
|
||||
System.err.println("waiting for " + waitTimeMillis + " milli seconds" +
|
||||
" before issuing additional requests");
|
||||
Thread.sleep(waitTimeMillis);
|
||||
// issue additional concurrent requests
|
||||
final List<Future<HttpResponse<Void>>> responses;
|
||||
try (final ExecutorService executor = Executors.newFixedThreadPool(numAdditionalReqs)) {
|
||||
responses = executor.invokeAll(tasks);
|
||||
}
|
||||
System.err.println("All " + responses.size() + " requests completed, now" +
|
||||
" verifying each response");
|
||||
// verify all requests succeeded
|
||||
for (final Future<HttpResponse<Void>> future : responses) {
|
||||
final HttpResponse<Void> rsp = future.get();
|
||||
assertEquals(200, rsp.statusCode(), "unexpected response code for request "
|
||||
+ request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns the effective idle timeout duration of a HTTP2 connection
|
||||
private static Duration getEffectiveH2IdleTimeoutDuration() {
|
||||
final long keepAliveTimeoutInSecs = getNetProp(KEEPALIVE_TIMEOUT_PROP, 30);
|
||||
final long h2TimeoutInSecs = getNetProp(H2_KEEPALIVE_TIMEOUT_PROP, keepAliveTimeoutInSecs);
|
||||
return Duration.of(h2TimeoutInSecs, ChronoUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static long getNetProp(final String prop, final long def) {
|
||||
final String s = Utils.getNetProperty(prop);
|
||||
if (s == null) {
|
||||
return def;
|
||||
}
|
||||
try {
|
||||
final long timeoutVal = Long.parseLong(s);
|
||||
return timeoutVal >= 0 ? timeoutVal : def;
|
||||
} catch (NumberFormatException ignored) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
186
test/jdk/java/net/httpclient/http2/ImplicitPushCancel.java
Normal file
186
test/jdk/java/net/httpclient/http2/ImplicitPushCancel.java
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* Copyright (c) 2018, 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.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* @run junit/othervm
|
||||
* -Djdk.internal.httpclient.debug=true
|
||||
* -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace
|
||||
* ImplicitPushCancel
|
||||
*/
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
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.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.net.http.HttpResponse.PushPromiseHandler;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class ImplicitPushCancel {
|
||||
|
||||
static final Map<String,String> PUSH_PROMISES = Map.of(
|
||||
"/x/y/z/1", "the first push promise body",
|
||||
"/x/y/z/2", "the second push promise body",
|
||||
"/x/y/z/3", "the third push promise body",
|
||||
"/x/y/z/4", "the fourth push promise body",
|
||||
"/x/y/z/5", "the fifth push promise body",
|
||||
"/x/y/z/6", "the sixth push promise body",
|
||||
"/x/y/z/7", "the seventh push promise body",
|
||||
"/x/y/z/8", "the eight push promise body",
|
||||
"/x/y/z/9", "the ninth push promise body"
|
||||
);
|
||||
static final String MAIN_RESPONSE_BODY = "the main response body";
|
||||
|
||||
private static Http2TestServer server;
|
||||
private static URI uri;
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws Exception {
|
||||
server = new Http2TestServer(false, 0);
|
||||
Http2Handler handler = new ServerPushHandler(MAIN_RESPONSE_BODY,
|
||||
PUSH_PROMISES);
|
||||
server.addHandler(handler, "/");
|
||||
server.start();
|
||||
int port = server.getAddress().getPort();
|
||||
System.err.println("Server listening on port " + port);
|
||||
uri = new URI("http://localhost:" + port + "/foo/a/b/c");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void teardown() {
|
||||
server.stop();
|
||||
}
|
||||
|
||||
static final <T> HttpResponse<T> assert200ResponseCode(HttpResponse<T> response) {
|
||||
assertEquals(200, response.statusCode());
|
||||
return response;
|
||||
}
|
||||
|
||||
/*
|
||||
* With a handler not capable of accepting push promises, then all push
|
||||
* promises should be rejected / cancelled, without interfering with the
|
||||
* main response.
|
||||
*/
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
client.sendAsync(HttpRequest.newBuilder(uri).build(), BodyHandlers.ofString())
|
||||
.thenApply(ImplicitPushCancel::assert200ResponseCode)
|
||||
.thenApply(HttpResponse::body)
|
||||
.thenAccept(body -> body.equals(MAIN_RESPONSE_BODY))
|
||||
.join();
|
||||
|
||||
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>> promises
|
||||
= new ConcurrentHashMap<>();
|
||||
PushPromiseHandler<String> pph = PushPromiseHandler
|
||||
.of((r) -> BodyHandlers.ofString(), promises);
|
||||
HttpResponse<String> main = client.sendAsync(
|
||||
HttpRequest.newBuilder(uri).build(),
|
||||
BodyHandlers.ofString(),
|
||||
pph)
|
||||
.join();
|
||||
|
||||
promises.entrySet().stream().forEach(e -> System.out.println(e.getKey() + ":" + e.getValue().join().body()));
|
||||
|
||||
promises.putIfAbsent(main.request(), CompletableFuture.completedFuture(main));
|
||||
promises.entrySet().stream().forEach(entry -> {
|
||||
HttpRequest request = entry.getKey();
|
||||
HttpResponse<String> response = entry.getValue().join();
|
||||
assertEquals(200, response.statusCode());
|
||||
if (PUSH_PROMISES.containsKey(request.uri().getPath())) {
|
||||
assertEquals(PUSH_PROMISES.get(request.uri().getPath()), response.body());
|
||||
} else {
|
||||
assertEquals(MAIN_RESPONSE_BODY, response.body());
|
||||
}
|
||||
|
||||
} );
|
||||
}
|
||||
|
||||
// --- server push handler ---
|
||||
static class ServerPushHandler implements Http2Handler {
|
||||
|
||||
private final String mainResponseBody;
|
||||
private final Map<String,String> promises;
|
||||
|
||||
public ServerPushHandler(String mainResponseBody,
|
||||
Map<String,String> promises)
|
||||
throws Exception
|
||||
{
|
||||
Objects.requireNonNull(promises);
|
||||
this.mainResponseBody = mainResponseBody;
|
||||
this.promises = promises;
|
||||
}
|
||||
|
||||
public void handle(Http2TestExchange exchange) throws IOException {
|
||||
System.err.println("Server: handle " + exchange);
|
||||
try (InputStream is = exchange.getRequestBody()) {
|
||||
is.readAllBytes();
|
||||
}
|
||||
|
||||
if (exchange.serverPushAllowed()) {
|
||||
pushPromises(exchange);
|
||||
}
|
||||
|
||||
// response data for the main response
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
byte[] bytes = mainResponseBody.getBytes(UTF_8);
|
||||
exchange.sendResponseHeaders(200, bytes.length);
|
||||
os.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private void pushPromises(Http2TestExchange exchange) throws IOException {
|
||||
URI requestURI = exchange.getRequestURI();
|
||||
for (Map.Entry<String,String> promise : promises.entrySet()) {
|
||||
URI uri = requestURI.resolve(promise.getKey());
|
||||
InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8));
|
||||
HttpHeaders headers = HttpHeaders.of(Collections.emptyMap(), (x, y) -> true);
|
||||
exchange.serverPush(uri, headers, is);
|
||||
}
|
||||
System.err.println("Server: All pushes sent");
|
||||
}
|
||||
}
|
||||
}
|
||||
220
test/jdk/java/net/httpclient/http2/NoBodyTest.java
Normal file
220
test/jdk/java/net/httpclient/http2/NoBodyTest.java
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* Copyright (c) 2015, 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
|
||||
* @bug 8087112
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* @run junit/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors
|
||||
* -Djdk.internal.httpclient.debug=true
|
||||
* NoBodyTest
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.*;
|
||||
import javax.net.ssl.*;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.util.concurrent.*;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class NoBodyTest {
|
||||
static int httpPort, httpsPort;
|
||||
static Http2TestServer httpServer, httpsServer;
|
||||
static HttpClient client = null;
|
||||
static ExecutorService clientExec;
|
||||
static ExecutorService serverExec;
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
static String TEST_STRING = "The quick brown fox jumps over the lazy dog ";
|
||||
|
||||
static String httpURIString, httpsURIString;
|
||||
|
||||
static void initialize() throws Exception {
|
||||
try {
|
||||
client = getClient();
|
||||
httpServer = new Http2TestServer(false, 0, serverExec, sslContext);
|
||||
httpServer.addHandler(new Handler(), "/");
|
||||
httpPort = httpServer.getAddress().getPort();
|
||||
|
||||
httpsServer = new Http2TestServer(true, 0, serverExec, sslContext);
|
||||
httpsServer.addHandler(new Handler(), "/");
|
||||
|
||||
httpsPort = httpsServer.getAddress().getPort();
|
||||
httpURIString = "http://localhost:" + httpPort + "/foo/";
|
||||
httpsURIString = "https://localhost:" + httpsPort + "/bar/";
|
||||
|
||||
httpServer.start();
|
||||
httpsServer.start();
|
||||
} catch (Throwable e) {
|
||||
System.err.println("Throwing now");
|
||||
e.printStackTrace(System.err);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runtest() throws Exception {
|
||||
try {
|
||||
initialize();
|
||||
warmup(false);
|
||||
warmup(true);
|
||||
test(false);
|
||||
test(true);
|
||||
} catch (Throwable tt) {
|
||||
System.err.println("tt caught");
|
||||
tt.printStackTrace(System.err);
|
||||
throw tt;
|
||||
} finally {
|
||||
httpServer.stop();
|
||||
httpsServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
static HttpClient getClient() {
|
||||
if (client == null) {
|
||||
serverExec = Executors.newCachedThreadPool();
|
||||
clientExec = Executors.newCachedThreadPool();
|
||||
client = HttpClient.newBuilder()
|
||||
.executor(clientExec)
|
||||
.sslContext(sslContext)
|
||||
.version(HTTP_2)
|
||||
.build();
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
static URI getURI(boolean secure) {
|
||||
if (secure)
|
||||
return URI.create(httpsURIString);
|
||||
else
|
||||
return URI.create(httpURIString);
|
||||
}
|
||||
|
||||
static void checkStatus(int expected, int found) throws Exception {
|
||||
if (expected != found) {
|
||||
System.err.printf ("Test failed: wrong status code %d/%d\n",
|
||||
expected, found);
|
||||
throw new RuntimeException("Test failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void checkStrings(String expected, String found) throws Exception {
|
||||
if (!expected.equals(found)) {
|
||||
System.err.printf ("Test failed: wrong string %s/%s\n",
|
||||
expected, found);
|
||||
throw new RuntimeException("Test failed");
|
||||
}
|
||||
}
|
||||
|
||||
static final int LOOPS = 13;
|
||||
|
||||
static void warmup(boolean secure) throws Exception {
|
||||
URI uri = getURI(secure);
|
||||
String type = secure ? "https" : "http";
|
||||
System.err.println("Request to " + uri);
|
||||
|
||||
// Do a simple warmup request
|
||||
|
||||
HttpClient client = getClient();
|
||||
HttpRequest req = HttpRequest.newBuilder(uri)
|
||||
.POST(BodyPublishers.ofString("Random text"))
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(req, BodyHandlers.ofString());
|
||||
checkStatus(200, response.statusCode());
|
||||
String responseBody = response.body();
|
||||
HttpHeaders h = response.headers();
|
||||
checkStrings(TEST_STRING + type, responseBody);
|
||||
}
|
||||
|
||||
static void test(boolean secure) throws Exception {
|
||||
URI uri = getURI(secure);
|
||||
String type = secure ? "https" : "http";
|
||||
System.err.println("Request to " + uri);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder(uri)
|
||||
.POST(BodyPublishers.ofString(TEST_STRING))
|
||||
.build();
|
||||
for (int i = 0; i < LOOPS; i++) {
|
||||
System.out.println("Loop " + i);
|
||||
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
|
||||
int expectedResponse = (i % 2) == 0 ? 204 : 200;
|
||||
if (response.statusCode() != expectedResponse)
|
||||
throw new RuntimeException("wrong response code " + Integer.toString(response.statusCode()));
|
||||
if (expectedResponse == 200 && !response.body().equals(TEST_STRING + type)) {
|
||||
System.err.printf("response received/expected %s/%s\n", response.body(), TEST_STRING + type);
|
||||
throw new RuntimeException("wrong response body");
|
||||
}
|
||||
}
|
||||
System.err.println("test: DONE");
|
||||
}
|
||||
|
||||
static class Handler implements Http2Handler {
|
||||
|
||||
public Handler() {}
|
||||
|
||||
volatile int invocation = 0;
|
||||
|
||||
@Override
|
||||
public void handle(Http2TestExchange t)
|
||||
throws IOException {
|
||||
try {
|
||||
URI uri = t.getRequestURI();
|
||||
System.err.printf("Handler received request to %s from %s\n",
|
||||
uri, t.getRemoteAddress());
|
||||
String type = uri.getScheme().toLowerCase();
|
||||
InputStream is = t.getRequestBody();
|
||||
while (is.read() != -1);
|
||||
is.close();
|
||||
|
||||
// every second response is 204.
|
||||
|
||||
if ((invocation++ % 2) == 1) {
|
||||
System.err.println("Server sending 204");
|
||||
t.sendResponseHeaders(204, -1);
|
||||
} else {
|
||||
String body = TEST_STRING + type;
|
||||
t.sendResponseHeaders(200, body.length());
|
||||
OutputStream os = t.getResponseBody();
|
||||
os.write(body.getBytes());
|
||||
os.close();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace(System.err);
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
171
test/jdk/java/net/httpclient/http2/PostPutTest.java
Normal file
171
test/jdk/java/net/httpclient/http2/PostPutTest.java
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* Copyright (c) 2023, 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
|
||||
* @bug 8293786
|
||||
* @summary Checks to see if the HttpClient can process a request to cancel a transmission from a remote if the server
|
||||
* does not process any data. The client should read all data from the server and close the connection.
|
||||
* @library /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* @run junit/othervm/timeout=50 -Djdk.httpclient.HttpClient.log=all
|
||||
* PostPutTest
|
||||
*/
|
||||
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
import static java.net.http.HttpRequest.BodyPublishers.ofByteArray;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
public class PostPutTest {
|
||||
|
||||
private static Http2TestServer http2TestServer;
|
||||
private static URI warmupURI, testHandlerBasicURI, testHandlerCloseBosURI, testHandleNegativeContentLengthURI;
|
||||
static PrintStream testLog = System.err;
|
||||
|
||||
// As per jdk.internal.net.http.WindowController.DEFAULT_INITIAL_WINDOW_SIZE
|
||||
private static final int DEFAULT_INITIAL_WINDOW_SIZE = (64 * 1024) - 1;
|
||||
// Add on a small amount of arbitrary bytes to see if client hangs when receiving RST_STREAM
|
||||
private static byte[] data = new byte[DEFAULT_INITIAL_WINDOW_SIZE + 10];
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws Exception {
|
||||
http2TestServer = new Http2TestServer(false, 0);
|
||||
http2TestServer.addHandler(new WarmupHandler(), "/Warmup");
|
||||
http2TestServer.addHandler(new TestHandlerBasic(), "/TestHandlerBasic");
|
||||
http2TestServer.addHandler(new TestHandlerCloseBos(), "/TestHandlerCloseBos");
|
||||
http2TestServer.addHandler(new TestHandleNegativeContentLength(), "/TestHandleNegativeContentLength");
|
||||
http2TestServer.start();
|
||||
testLog.println("PostPutTest.setup(): Starting server");
|
||||
warmupURI = new URI("http://" + http2TestServer.serverAuthority() + "/Warmup");
|
||||
testHandlerBasicURI = new URI("http://" + http2TestServer.serverAuthority() + "/TestHandlerBasic");
|
||||
testHandlerCloseBosURI = new URI("http://" + http2TestServer.serverAuthority() + "/TestHandlerCloseBos");
|
||||
testHandleNegativeContentLengthURI = new URI("http://" + http2TestServer.serverAuthority() + "/TestHandleNegativeContentLength");
|
||||
testLog.println("PostPutTest.setup(): warmupURI: " + warmupURI);
|
||||
testLog.println("PostPutTest.setup(): testHandlerBasicURI: " + testHandlerBasicURI);
|
||||
testLog.println("PostPutTest.setup(): testHandlerCloseBosURI: " + testHandlerCloseBosURI);
|
||||
testLog.println("PostPutTest.setup(): testHandleNegativeContentLengthURI: " + testHandleNegativeContentLengthURI);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void teardown() {
|
||||
testLog.println("PostPutTest.teardown(): Stopping server");
|
||||
http2TestServer.stop();
|
||||
data = null;
|
||||
}
|
||||
|
||||
public static Object[][] variants() {
|
||||
HttpRequest over64kPost, over64kPut, over64kPostCloseBos, over64kPutCloseBos, over64kPostNegativeContentLength, over64kPutNegativeContentLength;
|
||||
over64kPost = HttpRequest.newBuilder().version(HTTP_2).POST(ofByteArray(data)).uri(testHandlerBasicURI).build();
|
||||
over64kPut = HttpRequest.newBuilder().version(HTTP_2).PUT(ofByteArray(data)).uri(testHandlerBasicURI).build();
|
||||
|
||||
over64kPostCloseBos = HttpRequest.newBuilder().version(HTTP_2).POST(ofByteArray(data)).uri(testHandlerCloseBosURI).build();
|
||||
over64kPutCloseBos = HttpRequest.newBuilder().version(HTTP_2).PUT(ofByteArray(data)).uri(testHandlerCloseBosURI).build();
|
||||
|
||||
over64kPostNegativeContentLength = HttpRequest.newBuilder().version(HTTP_2).POST(ofByteArray(data)).uri(testHandleNegativeContentLengthURI).build();
|
||||
over64kPutNegativeContentLength = HttpRequest.newBuilder().version(HTTP_2).PUT(ofByteArray(data)).uri(testHandleNegativeContentLengthURI).build();
|
||||
|
||||
return new Object[][] {
|
||||
{ over64kPost, "POST data over 64k bytes" },
|
||||
{ over64kPut, "PUT data over 64k bytes" },
|
||||
{ over64kPostCloseBos, "POST data over 64k bytes with close bos" },
|
||||
{ over64kPutCloseBos, "PUT data over 64k bytes with close bos" },
|
||||
{ over64kPostNegativeContentLength, "POST data over 64k bytes with negative content length" },
|
||||
{ over64kPutNegativeContentLength, "PUT data over 64k bytes with negative content length" }
|
||||
};
|
||||
}
|
||||
|
||||
public HttpRequest getWarmupReq() {
|
||||
return HttpRequest.newBuilder()
|
||||
.GET()
|
||||
.uri(warmupURI)
|
||||
.build();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("variants")
|
||||
public void testOver64kPUT(HttpRequest req, String testMessage) {
|
||||
testLog.println("PostPutTest: Performing test: " + testMessage);
|
||||
HttpClient hc = HttpClient.newBuilder().version(HTTP_2).build();
|
||||
hc.sendAsync(getWarmupReq(), HttpResponse.BodyHandlers.ofString()).join();
|
||||
hc.sendAsync(req, HttpResponse.BodyHandlers.ofString()).join();
|
||||
/*
|
||||
If this test fails in timeout, it is likely due to one of two reasons:
|
||||
- The responseSubscriber is null, so no incoming frames are being processed by the client
|
||||
(See Stream::schedule)
|
||||
- The test server is for some reason not sending a RST_STREAM with the NO_ERROR flag set after
|
||||
sending an empty DATA frame with the END_STREAM flag set.
|
||||
*/
|
||||
}
|
||||
|
||||
private static class TestHandlerBasic implements Http2Handler {
|
||||
|
||||
@Override
|
||||
public void handle(Http2TestExchange exchange) throws IOException {
|
||||
// The input stream is not read in this bug as this will trigger window updates for the server. This bug
|
||||
// concerns the case where no updates are sent and the server instead tells the client to abort the transmission.
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestHandlerCloseBos implements Http2Handler {
|
||||
|
||||
@Override
|
||||
public void handle(Http2TestExchange exchange) throws IOException {
|
||||
// This case does actually cause the test to hang due to the body input stream being closed before it can send
|
||||
// the RST_STREAM frame.
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
exchange.getResponseBody().close();
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestHandleNegativeContentLength implements Http2Handler {
|
||||
|
||||
@Override
|
||||
public void handle(Http2TestExchange exchange) throws IOException {
|
||||
exchange.sendResponseHeaders(200, -1);
|
||||
}
|
||||
}
|
||||
|
||||
private static class WarmupHandler implements Http2Handler {
|
||||
|
||||
@Override
|
||||
public void handle(Http2TestExchange exchange) throws IOException {
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
323
test/jdk/java/net/httpclient/http2/ProxyTest2.java
Normal file
323
test/jdk/java/net/httpclient/http2/ProxyTest2.java
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
/*
|
||||
* Copyright (c) 2017, 2025, 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.
|
||||
*/
|
||||
|
||||
import com.sun.net.httpserver.HttpsConfigurator;
|
||||
import com.sun.net.httpserver.HttpsParameters;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.Writer;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @bug 8181422
|
||||
* @summary Verifies that you can access an HTTP/2 server over HTTPS by
|
||||
* tunnelling through an HTTP/1.1 proxy.
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* @run main/othervm ProxyTest2
|
||||
* @author danielfuchs
|
||||
*/
|
||||
public class ProxyTest2 {
|
||||
|
||||
static {
|
||||
SSLContext.setDefault(SimpleSSLContext.findSSLContext());
|
||||
}
|
||||
|
||||
static final String RESPONSE = "<html><body><p>Hello World!</body></html>";
|
||||
static final String PATH = "/foo/";
|
||||
|
||||
static Http2TestServer createHttpsServer(ExecutorService exec) throws Exception {
|
||||
Http2TestServer server = new Http2TestServer(true, 0, exec, SSLContext.getDefault());
|
||||
server.addHandler(new Http2Handler() {
|
||||
@Override
|
||||
public void handle(Http2TestExchange he) throws IOException {
|
||||
he.getResponseHeaders().addHeader("encoding", "UTF-8");
|
||||
he.sendResponseHeaders(200, RESPONSE.length());
|
||||
he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8));
|
||||
he.close();
|
||||
}
|
||||
}, PATH);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
throws Exception
|
||||
{
|
||||
ExecutorService exec = Executors.newCachedThreadPool();
|
||||
Http2TestServer server = createHttpsServer(exec);
|
||||
server.start();
|
||||
try {
|
||||
// Http2TestServer over HTTPS does not support HTTP/1.1
|
||||
// => only test with a HTTP/2 client
|
||||
test(server, HttpClient.Version.HTTP_2);
|
||||
} finally {
|
||||
server.stop();
|
||||
exec.shutdown();
|
||||
System.out.println("Server stopped");
|
||||
}
|
||||
}
|
||||
|
||||
public static void test(Http2TestServer server, HttpClient.Version version)
|
||||
throws Exception
|
||||
{
|
||||
System.out.println("Server is: " + server.getAddress().toString());
|
||||
URI uri = new URI("https://localhost:" + server.getAddress().getPort() + PATH + "x");
|
||||
TunnelingProxy proxy = new TunnelingProxy(server);
|
||||
proxy.start();
|
||||
try {
|
||||
System.out.println("Proxy started");
|
||||
Proxy p = new Proxy(Proxy.Type.HTTP,
|
||||
InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort()));
|
||||
System.out.println("Setting up request with HttpClient for version: "
|
||||
+ version.name() + "URI=" + uri);
|
||||
ProxySelector ps = ProxySelector.of(
|
||||
InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort()));
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.version(version)
|
||||
.proxy(ps)
|
||||
.build();
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(uri)
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
System.out.println("Sending request with HttpClient");
|
||||
HttpResponse<String> response
|
||||
= client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
System.out.println("Got response");
|
||||
String resp = response.body();
|
||||
System.out.println("Received: " + resp);
|
||||
if (!RESPONSE.equals(resp)) {
|
||||
throw new AssertionError("Unexpected response");
|
||||
}
|
||||
} finally {
|
||||
System.out.println("Stopping proxy");
|
||||
proxy.stop();
|
||||
System.out.println("Proxy stopped");
|
||||
}
|
||||
}
|
||||
|
||||
static class TunnelingProxy {
|
||||
final Thread accept;
|
||||
final ServerSocket ss;
|
||||
final boolean DEBUG = false;
|
||||
final Http2TestServer serverImpl;
|
||||
final CopyOnWriteArrayList<CompletableFuture<Void>> connectionCFs
|
||||
= new CopyOnWriteArrayList<>();
|
||||
private volatile boolean stopped;
|
||||
TunnelingProxy(Http2TestServer serverImpl) throws IOException {
|
||||
this.serverImpl = serverImpl;
|
||||
ss = new ServerSocket();
|
||||
accept = new Thread(this::accept);
|
||||
accept.setDaemon(true);
|
||||
}
|
||||
|
||||
void start() throws IOException {
|
||||
ss.setReuseAddress(false);
|
||||
ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
|
||||
accept.start();
|
||||
}
|
||||
|
||||
// Pipe the input stream to the output stream.
|
||||
private synchronized Thread pipe(InputStream is, OutputStream os,
|
||||
char tag, CompletableFuture<Void> end) {
|
||||
return new Thread("TunnelPipe("+tag+")") {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
try {
|
||||
int len;
|
||||
byte[] buf = new byte[16 * 1024];
|
||||
while ((len = is.read(buf)) != -1) {
|
||||
os.write(buf, 0, len);
|
||||
os.flush();
|
||||
// if DEBUG prints a + or a - for each transferred
|
||||
// character.
|
||||
if (DEBUG) System.out.print(String.valueOf(tag).repeat(len));
|
||||
}
|
||||
is.close();
|
||||
} finally {
|
||||
os.close();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
if (DEBUG) ex.printStackTrace(System.out);
|
||||
} finally {
|
||||
end.complete(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public InetSocketAddress getAddress() {
|
||||
return new InetSocketAddress( InetAddress.getLoopbackAddress(), ss.getLocalPort());
|
||||
}
|
||||
|
||||
// This is a bit shaky. It doesn't handle continuation
|
||||
// lines, but our client shouldn't send any.
|
||||
// Read a line from the input stream, swallowing the final
|
||||
// \r\n sequence. Stops at the first \n, doesn't complain
|
||||
// if it wasn't preceded by '\r'.
|
||||
//
|
||||
String readLine(InputStream r) throws IOException {
|
||||
StringBuilder b = new StringBuilder();
|
||||
int c;
|
||||
while ((c = r.read()) != -1) {
|
||||
if (c == '\n') break;
|
||||
b.appendCodePoint(c);
|
||||
}
|
||||
if (b.codePointAt(b.length() -1) == '\r') {
|
||||
b.delete(b.length() -1, b.length());
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
public void accept() {
|
||||
Socket clientConnection = null;
|
||||
try {
|
||||
while (!stopped) {
|
||||
System.out.println("Tunnel: Waiting for client");
|
||||
Socket toClose;
|
||||
try {
|
||||
toClose = clientConnection = ss.accept();
|
||||
} catch (IOException io) {
|
||||
if (DEBUG) io.printStackTrace(System.out);
|
||||
break;
|
||||
}
|
||||
System.out.println("Tunnel: Client accepted");
|
||||
Socket targetConnection = null;
|
||||
InputStream ccis = clientConnection.getInputStream();
|
||||
OutputStream ccos = clientConnection.getOutputStream();
|
||||
Writer w = new OutputStreamWriter(ccos, "UTF-8");
|
||||
PrintWriter pw = new PrintWriter(w);
|
||||
System.out.println("Tunnel: Reading request line");
|
||||
String requestLine = readLine(ccis);
|
||||
System.out.println("Tunnel: Request status line: " + requestLine);
|
||||
if (requestLine.startsWith("CONNECT ")) {
|
||||
// We should probably check that the next word following
|
||||
// CONNECT is the host:port of our HTTPS serverImpl.
|
||||
// Some improvement for a followup!
|
||||
|
||||
// Read all headers until we find the empty line that
|
||||
// signals the end of all headers.
|
||||
while(!requestLine.equals("")) {
|
||||
System.out.println("Tunnel: Reading header: "
|
||||
+ (requestLine = readLine(ccis)));
|
||||
}
|
||||
|
||||
// Open target connection
|
||||
targetConnection = new Socket(
|
||||
InetAddress.getLoopbackAddress(),
|
||||
serverImpl.getAddress().getPort());
|
||||
|
||||
// Then send the 200 OK response to the client
|
||||
System.out.println("Tunnel: Sending "
|
||||
+ "HTTP/1.1 200 OK\r\n\r\n");
|
||||
pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
|
||||
pw.flush();
|
||||
} else {
|
||||
// This should not happen. If it does then just print an
|
||||
// error - both on out and err, and close the accepted
|
||||
// socket
|
||||
System.out.println("WARNING: Tunnel: Unexpected status line: "
|
||||
+ requestLine + " received by "
|
||||
+ ss.getLocalSocketAddress()
|
||||
+ " from "
|
||||
+ toClose.getRemoteSocketAddress()
|
||||
+ " - closing accepted socket");
|
||||
// Print on err
|
||||
System.err.println("WARNING: Tunnel: Unexpected status line: "
|
||||
+ requestLine + " received by "
|
||||
+ ss.getLocalSocketAddress()
|
||||
+ " from "
|
||||
+ toClose.getRemoteSocketAddress());
|
||||
// close accepted socket.
|
||||
toClose.close();
|
||||
System.err.println("Tunnel: accepted socket closed.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pipe the input stream of the client connection to the
|
||||
// output stream of the target connection and conversely.
|
||||
// Now the client and target will just talk to each other.
|
||||
System.out.println("Tunnel: Starting tunnel pipes");
|
||||
CompletableFuture<Void> end, end1, end2;
|
||||
Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+',
|
||||
end1 = new CompletableFuture<>());
|
||||
Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-',
|
||||
end2 = new CompletableFuture<>());
|
||||
end = CompletableFuture.allOf(end1, end2);
|
||||
end.whenComplete(
|
||||
(r,t) -> {
|
||||
try { toClose.close(); } catch (IOException x) { }
|
||||
finally {connectionCFs.remove(end);}
|
||||
});
|
||||
connectionCFs.add(end);
|
||||
t1.start();
|
||||
t2.start();
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
try {
|
||||
ss.close();
|
||||
} catch (IOException ex1) {
|
||||
ex.addSuppressed(ex1);
|
||||
}
|
||||
ex.printStackTrace(System.err);
|
||||
} finally {
|
||||
System.out.println("Tunnel: exiting (stopped=" + stopped + ")");
|
||||
connectionCFs.forEach(cf -> cf.complete(null));
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() throws IOException {
|
||||
stopped = true;
|
||||
ss.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
367
test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java
Normal file
367
test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
/*
|
||||
* Copyright (c) 2022, 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
|
||||
* @bug 8263031
|
||||
* @summary Tests that the HttpClient can correctly receive a Push Promise
|
||||
* Frame with the END_HEADERS flag unset followed by one or more
|
||||
* Continuation Frames.
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* jdk.httpclient.test.lib.http2.BodyOutputStream
|
||||
* jdk.httpclient.test.lib.http2.OutgoingPushPromise
|
||||
* @run junit/othervm PushPromiseContinuation
|
||||
*/
|
||||
|
||||
import javax.net.ssl.SSLSession;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
import jdk.httpclient.test.lib.http2.BodyOutputStream;
|
||||
import jdk.httpclient.test.lib.http2.OutgoingPushPromise;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
|
||||
|
||||
import jdk.internal.net.http.common.HttpHeadersBuilder;
|
||||
import jdk.internal.net.http.frame.ContinuationFrame;
|
||||
import jdk.internal.net.http.frame.HeaderFrame;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
||||
public class PushPromiseContinuation {
|
||||
|
||||
static volatile HttpHeaders testHeaders;
|
||||
static volatile HttpHeadersBuilder testHeadersBuilder;
|
||||
static volatile int continuationCount;
|
||||
static final String mainPromiseBody = "Main Promise Body";
|
||||
static final String mainResponseBody = "Main Response Body";
|
||||
private static Http2TestServer server;
|
||||
private static URI uri;
|
||||
|
||||
// Set up simple client-side push promise handler
|
||||
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>> pushPromiseMap = new ConcurrentHashMap<>();
|
||||
HttpResponse.PushPromiseHandler<String> pph = (initial, pushRequest, acceptor) -> {
|
||||
HttpResponse.BodyHandler<String> s = HttpResponse.BodyHandlers.ofString(UTF_8);
|
||||
pushPromiseMap.put(pushRequest, acceptor.apply(s));
|
||||
};
|
||||
|
||||
@BeforeEach
|
||||
public void beforeMethod() {
|
||||
pushPromiseMap = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws Exception {
|
||||
server = new Http2TestServer(false, 0);
|
||||
server.addHandler(new ServerPushHandler(), "/");
|
||||
|
||||
// Need to have a custom exchange supplier to manage the server's push
|
||||
// promise with continuation flow
|
||||
server.setExchangeSupplier(Http2PushPromiseContinuationExchangeImpl::new);
|
||||
|
||||
System.err.println("PushPromiseContinuation: Server listening on port " + server.getAddress().getPort());
|
||||
server.start();
|
||||
int port = server.getAddress().getPort();
|
||||
uri = new URI("http://localhost:" + port + "/");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void teardown() {
|
||||
server.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that when the client receives PushPromise Frame with the END_HEADERS
|
||||
* flag set to 0x0 and subsequently receives a continuation frame, no exception
|
||||
* is thrown and all headers from the PushPromise and Continuation Frames sent
|
||||
* by the server arrive at the client.
|
||||
*/
|
||||
@Test
|
||||
public void testOneContinuation() {
|
||||
continuationCount = 1;
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
// Carry out request
|
||||
HttpRequest hreq = HttpRequest.newBuilder(uri).version(HttpClient.Version.HTTP_2).GET().build();
|
||||
CompletableFuture<HttpResponse<String>> cf =
|
||||
client.sendAsync(hreq, HttpResponse.BodyHandlers.ofString(UTF_8), pph);
|
||||
HttpResponse<String> resp = cf.join();
|
||||
|
||||
// Verify results
|
||||
verify(resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as above, but tests for the case where two Continuation Frames are sent
|
||||
* with the END_HEADERS flag set only on the last frame.
|
||||
*/
|
||||
@Test
|
||||
public void testTwoContinuations() {
|
||||
continuationCount = 2;
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
// Carry out request
|
||||
HttpRequest hreq = HttpRequest.newBuilder(uri).version(HttpClient.Version.HTTP_2).GET().build();
|
||||
CompletableFuture<HttpResponse<String>> cf =
|
||||
client.sendAsync(hreq, HttpResponse.BodyHandlers.ofString(UTF_8), pph);
|
||||
HttpResponse<String> resp = cf.join();
|
||||
|
||||
// Verify results
|
||||
verify(resp);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThreeContinuations() {
|
||||
continuationCount = 3;
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
// Carry out request
|
||||
HttpRequest hreq = HttpRequest.newBuilder(uri).version(HttpClient.Version.HTTP_2).GET().build();
|
||||
CompletableFuture<HttpResponse<String>> cf =
|
||||
client.sendAsync(hreq, HttpResponse.BodyHandlers.ofString(UTF_8), pph);
|
||||
HttpResponse<String> resp = cf.join();
|
||||
|
||||
// Verify results
|
||||
verify(resp);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendHeadersOnPushPromiseStream() throws Exception {
|
||||
// This test server sends a push promise that should be followed by a continuation but
|
||||
// incorrectly sends on Response Headers while the client awaits the continuation.
|
||||
Http2TestServer faultyServer = new Http2TestServer(false, 0);
|
||||
faultyServer.addHandler(new ServerPushHandler(), "/");
|
||||
faultyServer.setExchangeSupplier(Http2PushPromiseHeadersExchangeImpl::new);
|
||||
System.err.println("PushPromiseContinuation: FaultyServer listening on port " + faultyServer.getAddress().getPort());
|
||||
faultyServer.start();
|
||||
|
||||
int faultyPort = faultyServer.getAddress().getPort();
|
||||
URI faultyUri = new URI("http://localhost:" + faultyPort + "/");
|
||||
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
// Server is making a request to an incorrect URI
|
||||
HttpRequest hreq = HttpRequest.newBuilder(faultyUri).version(HttpClient.Version.HTTP_2).GET().build();
|
||||
CompletableFuture<HttpResponse<String>> cf =
|
||||
client.sendAsync(hreq, HttpResponse.BodyHandlers.ofString(UTF_8), pph);
|
||||
|
||||
CompletionException t = assertThrows(CompletionException.class, () -> cf.join());
|
||||
assertEquals(ProtocolException.class, t.getCause().getClass(),
|
||||
"Expected a ProtocolException but got " + t.getCause());
|
||||
System.err.println("Client received the following expected exception: " + t.getCause());
|
||||
faultyServer.stop();
|
||||
}
|
||||
|
||||
private void verify(HttpResponse<String> resp) {
|
||||
assertEquals(200, resp.statusCode());
|
||||
assertEquals(mainResponseBody, resp.body());
|
||||
if (pushPromiseMap.size() > 1) {
|
||||
System.err.println(pushPromiseMap.entrySet());
|
||||
fail("Results map size is greater than 1");
|
||||
} else {
|
||||
// This will only iterate once
|
||||
for (HttpRequest r : pushPromiseMap.keySet()) {
|
||||
HttpResponse<String> serverPushResp = pushPromiseMap.get(r).join();
|
||||
// Received headers should be the same as the combined PushPromise
|
||||
// frame headers combined with the Continuation frame headers
|
||||
assertEquals(r.headers(), testHeaders);
|
||||
// Check status code and push promise body are as expected
|
||||
assertEquals(200, serverPushResp.statusCode());
|
||||
assertEquals(mainPromiseBody, serverPushResp.body());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class Http2PushPromiseHeadersExchangeImpl extends Http2TestExchangeImpl {
|
||||
|
||||
Http2PushPromiseHeadersExchangeImpl(int streamid, String method, HttpHeaders reqheaders,
|
||||
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
|
||||
SSLSession sslSession, BodyOutputStream os,
|
||||
Http2TestServerConnection conn, boolean pushAllowed) {
|
||||
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) {
|
||||
HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder();
|
||||
headersBuilder.setHeader(":method", "GET");
|
||||
headersBuilder.setHeader(":scheme", uri.getScheme());
|
||||
headersBuilder.setHeader(":authority", uri.getAuthority());
|
||||
headersBuilder.setHeader(":path", uri.getPath());
|
||||
for (Map.Entry<String,List<String>> entry : reqHeaders.map().entrySet()) {
|
||||
for (String value : entry.getValue())
|
||||
headersBuilder.addHeader(entry.getKey(), value);
|
||||
}
|
||||
HttpHeaders combinedHeaders = headersBuilder.build();
|
||||
OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, rspHeaders, content);
|
||||
// Indicates to the client that a continuation should be expected
|
||||
pp.setFlag(0x0);
|
||||
try {
|
||||
conn.addToOutputQ(pp);
|
||||
// writeLoop will spin up thread to read the InputStream
|
||||
} catch (IOException ex) {
|
||||
System.err.println("TestServer: pushPromise exception: " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class Http2PushPromiseContinuationExchangeImpl extends Http2TestExchangeImpl {
|
||||
|
||||
HttpHeadersBuilder pushPromiseHeadersBuilder;
|
||||
List<ContinuationFrame> cfs;
|
||||
|
||||
Http2PushPromiseContinuationExchangeImpl(int streamid, String method, HttpHeaders reqheaders,
|
||||
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
|
||||
SSLSession sslSession, BodyOutputStream os,
|
||||
Http2TestServerConnection conn, boolean pushAllowed) {
|
||||
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
|
||||
}
|
||||
|
||||
private void setPushHeaders(String name, String value) {
|
||||
pushPromiseHeadersBuilder.setHeader(name, value);
|
||||
testHeadersBuilder.setHeader(name, value);
|
||||
}
|
||||
|
||||
private void assembleContinuations() {
|
||||
for (int i = 0; i < continuationCount; i++) {
|
||||
HttpHeadersBuilder builder = new HttpHeadersBuilder();
|
||||
for (int j = 0; j < 10; j++) {
|
||||
String name = "x-cont-" + i + "-" + j;
|
||||
builder.setHeader(name, "data_" + j);
|
||||
testHeadersBuilder.setHeader(name, "data_" + j);
|
||||
}
|
||||
|
||||
ContinuationFrame cf = new ContinuationFrame(streamid, 0x0, conn.encodeHeaders(builder.build()));
|
||||
// If this is the last Continuation Frame, set the END_HEADERS flag.
|
||||
if (i >= continuationCount - 1) {
|
||||
cf.setFlag(HeaderFrame.END_HEADERS);
|
||||
}
|
||||
cfs.add(cf);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) {
|
||||
pushPromiseHeadersBuilder = new HttpHeadersBuilder();
|
||||
testHeadersBuilder = new HttpHeadersBuilder();
|
||||
cfs = new ArrayList<>();
|
||||
|
||||
setPushHeaders(":method", "GET");
|
||||
setPushHeaders(":scheme", uri.getScheme());
|
||||
setPushHeaders(":authority", uri.getAuthority());
|
||||
setPushHeaders(":path", uri.getPath());
|
||||
for (Map.Entry<String,List<String>> entry : reqHeaders.map().entrySet()) {
|
||||
for (String value : entry.getValue()) {
|
||||
setPushHeaders(entry.getKey(), value);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
setPushHeaders("x-push-header-" + i, "data_" + i);
|
||||
}
|
||||
|
||||
// Create the Continuation Frame/s, done before Push Promise Frame for test purposes
|
||||
// as testHeaders contains all headers used in all frames
|
||||
assembleContinuations();
|
||||
|
||||
HttpHeaders pushPromiseHeaders = pushPromiseHeadersBuilder.build();
|
||||
testHeaders = testHeadersBuilder.build();
|
||||
// Create the Push Promise Frame
|
||||
OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, pushPromiseHeaders, rspHeaders, content, cfs);
|
||||
|
||||
// Indicates to the client that a continuation should be expected
|
||||
pp.setFlag(0x0);
|
||||
|
||||
try {
|
||||
// Schedule push promise and continuation for sending
|
||||
conn.addToOutputQ(pp);
|
||||
System.err.println("Server: Scheduled a Push Promise to Send");
|
||||
} catch (IOException ex) {
|
||||
System.err.println("Server: pushPromise exception: " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class ServerPushHandler implements Http2Handler {
|
||||
|
||||
public void handle(Http2TestExchange exchange) throws IOException {
|
||||
System.err.println("Server: handle " + exchange);
|
||||
try (InputStream is = exchange.getRequestBody()) {
|
||||
is.readAllBytes();
|
||||
}
|
||||
|
||||
if (exchange.serverPushAllowed()) {
|
||||
pushPromise(exchange);
|
||||
}
|
||||
|
||||
// response data for the main response
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
byte[] bytes = mainResponseBody.getBytes(UTF_8);
|
||||
exchange.sendResponseHeaders(200, bytes.length);
|
||||
os.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
|
||||
|
||||
|
||||
private void pushPromise(Http2TestExchange exchange) throws IOException {
|
||||
URI requestURI = exchange.getRequestURI();
|
||||
URI uri = requestURI.resolve("/promise");
|
||||
InputStream is = new ByteArrayInputStream(mainPromiseBody.getBytes(UTF_8));
|
||||
Map<String, List<String>> map = new HashMap<>();
|
||||
map.put("x-promise", List.of("promise-header"));
|
||||
HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
|
||||
exchange.serverPush(uri, headers, is);
|
||||
System.err.println("Server: Push Promise complete");
|
||||
}
|
||||
}
|
||||
}
|
||||
252
test/jdk/java/net/httpclient/http2/RedirectTest.java
Normal file
252
test/jdk/java/net/httpclient/http2/RedirectTest.java
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
* Copyright (c) 2015, 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
|
||||
* @bug 8156514
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.httpclient.test.lib.http2.Http2TestExchange
|
||||
* jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* jdk.httpclient.test.lib.http2.Http2RedirectHandler
|
||||
* jdk.test.lib.Asserts
|
||||
* jdk.test.lib.net.SimpleSSLContext
|
||||
* @run junit/othervm
|
||||
* -Djdk.httpclient.HttpClient.log=frames,ssl,requests,responses,errors
|
||||
* -Djdk.internal.httpclient.debug=true
|
||||
* RedirectTest
|
||||
*/
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2RedirectHandler;
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class RedirectTest implements HttpServerAdapters {
|
||||
static int httpPort;
|
||||
static HttpTestServer httpServer;
|
||||
static HttpClient client;
|
||||
|
||||
static String httpURIString, altURIString1, altURIString2;
|
||||
static URI httpURI, altURI1, altURI2;
|
||||
|
||||
static Supplier<String> sup(String... args) {
|
||||
Iterator<String> i = Arrays.asList(args).iterator();
|
||||
// need to know when to stop calling it.
|
||||
return () -> i.next();
|
||||
}
|
||||
|
||||
static class Redirector extends Http2RedirectHandler {
|
||||
private InetSocketAddress remoteAddr;
|
||||
private boolean error = false;
|
||||
|
||||
Redirector(Supplier<String> supplier) {
|
||||
super(supplier);
|
||||
}
|
||||
|
||||
protected synchronized void examineExchange(Http2TestExchange ex) {
|
||||
InetSocketAddress addr = ex.getRemoteAddress();
|
||||
if (remoteAddr == null) {
|
||||
remoteAddr = addr;
|
||||
return;
|
||||
}
|
||||
// check that the client addr/port stays the same, proving
|
||||
// that the connection didn't get dropped.
|
||||
if (!remoteAddr.equals(addr)) {
|
||||
System.err.printf("Error %s/%s\n", remoteAddr.toString(),
|
||||
addr.toString());
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int redirectCode() {
|
||||
return 308; // we need to use a code that preserves the body
|
||||
}
|
||||
|
||||
public synchronized boolean error() {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
static void initialize() throws Exception {
|
||||
try {
|
||||
client = getClient();
|
||||
Http2TestServer http2ServerImpl =
|
||||
new Http2TestServer(false, 0, null, null);
|
||||
httpServer = HttpTestServer.of(http2ServerImpl);
|
||||
httpPort = httpServer.getAddress().getPort();
|
||||
|
||||
// urls are accessed in sequence below. The first two are on
|
||||
// different servers. Third on same server as second. So, the
|
||||
// client should use the same http connection.
|
||||
httpURIString = "http://" + httpServer.serverAuthority() + "/foo/";
|
||||
httpURI = URI.create(httpURIString);
|
||||
altURIString1 = "http://" + httpServer.serverAuthority() + "/redir";
|
||||
altURI1 = URI.create(altURIString1);
|
||||
altURIString2 = "http://" + httpServer.serverAuthority() + "/redir_again";
|
||||
altURI2 = URI.create(altURIString2);
|
||||
|
||||
// TODO: remove dependency on Http2RedirectHandler
|
||||
Redirector r = new Redirector(sup(altURIString1, altURIString2));
|
||||
http2ServerImpl.addHandler(r, "/foo");
|
||||
http2ServerImpl.addHandler(r, "/redir");
|
||||
httpServer.addHandler(new HttpTestFileEchoHandler(), "/redir_again");
|
||||
|
||||
httpServer.start();
|
||||
} catch (Throwable e) {
|
||||
System.err.println("Throwing now");
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
try {
|
||||
initialize();
|
||||
simpleTest();
|
||||
} finally {
|
||||
httpServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
static HttpClient getClient() {
|
||||
if (client == null) {
|
||||
client = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.ALWAYS)
|
||||
.version(HTTP_2)
|
||||
.build();
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
static URI getURI() {
|
||||
return URI.create(httpURIString);
|
||||
}
|
||||
|
||||
static void checkStatus(int expected, int found) throws Exception {
|
||||
if (expected != found) {
|
||||
System.err.printf ("Test failed: wrong status code %d/%d\n",
|
||||
expected, found);
|
||||
throw new RuntimeException("Test failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void checkURIs(URI expected, URI found) throws Exception {
|
||||
System.out.printf ("Expected: %s, Found: %s\n", expected.toString(), found.toString());
|
||||
if (!expected.equals(found)) {
|
||||
System.err.printf ("Test failed: wrong URI %s/%s\n",
|
||||
expected.toString(), found.toString());
|
||||
throw new RuntimeException("Test failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void checkStrings(String expected, String found) throws Exception {
|
||||
if (!expected.equals(found)) {
|
||||
System.err.printf ("Test failed: wrong string %s/%s\n",
|
||||
expected, found);
|
||||
throw new RuntimeException("Test failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void check(boolean cond, Object... msg) {
|
||||
if (cond)
|
||||
return;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Object o : msg)
|
||||
sb.append(o);
|
||||
throw new RuntimeException(sb.toString());
|
||||
}
|
||||
|
||||
static final String SIMPLE_STRING = "Hello world Goodbye world";
|
||||
|
||||
static void simpleTest() throws Exception {
|
||||
URI uri = getURI();
|
||||
System.err.println("Request to " + uri);
|
||||
|
||||
HttpClient client = getClient();
|
||||
HttpRequest req = HttpRequest.newBuilder(uri)
|
||||
.POST(BodyPublishers.ofString(SIMPLE_STRING))
|
||||
.build();
|
||||
CompletableFuture<HttpResponse<String>> cf = client.sendAsync(req, BodyHandlers.ofString());
|
||||
HttpResponse<String> response = cf.join();
|
||||
|
||||
checkStatus(200, response.statusCode());
|
||||
String responseBody = response.body();
|
||||
checkStrings(SIMPLE_STRING, responseBody);
|
||||
checkURIs(response.uri(), altURI2);
|
||||
|
||||
// check two previous responses
|
||||
HttpResponse<String> prev = response.previousResponse()
|
||||
.orElseThrow(() -> new RuntimeException("no previous response"));
|
||||
checkURIs(prev.uri(), altURI1);
|
||||
|
||||
prev = prev.previousResponse()
|
||||
.orElseThrow(() -> new RuntimeException("no previous response"));
|
||||
checkURIs(prev.uri(), httpURI);
|
||||
|
||||
checkPreviousRedirectResponses(req, response);
|
||||
|
||||
System.err.println("DONE");
|
||||
}
|
||||
|
||||
static void checkPreviousRedirectResponses(HttpRequest initialRequest,
|
||||
HttpResponse<?> finalResponse) {
|
||||
// there must be at least one previous response
|
||||
finalResponse.previousResponse()
|
||||
.orElseThrow(() -> new RuntimeException("no previous response"));
|
||||
|
||||
HttpResponse<?> response = finalResponse;
|
||||
do {
|
||||
URI uri = response.uri();
|
||||
response = response.previousResponse().get();
|
||||
check(300 <= response.statusCode() && response.statusCode() <= 309,
|
||||
"Expected 300 <= code <= 309, got:" + response.statusCode());
|
||||
check(response.body() == null, "Unexpected body: " + response.body());
|
||||
String locationHeader = response.headers().firstValue("Location")
|
||||
.orElseThrow(() -> new RuntimeException("no previous Location"));
|
||||
check(uri.toString().endsWith(locationHeader),
|
||||
"URI: " + uri + ", Location: " + locationHeader);
|
||||
} while (response.previousResponse().isPresent());
|
||||
|
||||
// initial
|
||||
check(initialRequest.equals(response.request()),
|
||||
"Expected initial request [%s] to equal last prev req [%s]",
|
||||
initialRequest, response.request());
|
||||
}
|
||||
}
|
||||
312
test/jdk/java/net/httpclient/http2/ServerPush.java
Normal file
312
test/jdk/java/net/httpclient/http2/ServerPush.java
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
/*
|
||||
* Copyright (c) 2015, 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
|
||||
* @bug 8087112 8159814
|
||||
* @library /test/jdk/java/net/httpclient/lib
|
||||
* /test/lib
|
||||
* @build jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* jdk.httpclient.test.lib.http2.PushHandler
|
||||
* jdk.test.lib.Utils
|
||||
* @run junit/othervm
|
||||
* -Djdk.httpclient.HttpClient.log=errors,requests,responses
|
||||
* ServerPush
|
||||
*/
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.*;
|
||||
import java.net.http.*;
|
||||
import java.net.http.HttpResponse.BodyHandler;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.net.http.HttpResponse.BodySubscribers;
|
||||
import java.net.http.HttpResponse.PushPromiseHandler;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Consumer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.PushHandler;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static jdk.test.lib.Utils.createTempFileOfSize;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ServerPush {
|
||||
|
||||
private static final String TEMP_FILE_PREFIX =
|
||||
HttpClient.class.getPackageName() + '-' + ServerPush.class.getSimpleName() + '-';
|
||||
|
||||
static final int LOOPS = 13;
|
||||
static final int FILE_SIZE = 512 * 1024 + 343;
|
||||
|
||||
static Path tempFile;
|
||||
|
||||
private static Http2TestServer server;
|
||||
private static URI uri;
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws Exception {
|
||||
tempFile = createTempFileOfSize(TEMP_FILE_PREFIX, null, FILE_SIZE);
|
||||
server = new Http2TestServer(false, 0);
|
||||
server.addHandler(new PushHandler(tempFile, LOOPS), "/");
|
||||
System.out.println("Using temp file:" + tempFile);
|
||||
|
||||
System.err.println("Server listening on port " + server.getAddress().getPort());
|
||||
server.start();
|
||||
int port = server.getAddress().getPort();
|
||||
uri = new URI("http://localhost:" + port + "/foo/a/b/c");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void teardown() {
|
||||
server.stop();
|
||||
}
|
||||
|
||||
// Test 1 - custom written push promise handler, everything as a String
|
||||
@Test
|
||||
public void testTypeString() throws Exception {
|
||||
String tempFileAsString = new String(Files.readAllBytes(tempFile), UTF_8);
|
||||
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>>
|
||||
resultMap = new ConcurrentHashMap<>();
|
||||
|
||||
PushPromiseHandler<String> pph = (initial, pushRequest, acceptor) -> {
|
||||
BodyHandler<String> s = BodyHandlers.ofString(UTF_8);
|
||||
CompletableFuture<HttpResponse<String>> cf = acceptor.apply(s);
|
||||
resultMap.put(pushRequest, cf);
|
||||
};
|
||||
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
|
||||
CompletableFuture<HttpResponse<String>> cf =
|
||||
client.sendAsync(request, BodyHandlers.ofString(UTF_8), pph);
|
||||
cf.join();
|
||||
resultMap.put(request, cf);
|
||||
System.err.println("results.size: " + resultMap.size());
|
||||
for (HttpRequest r : resultMap.keySet()) {
|
||||
HttpResponse<String> response = resultMap.get(r).join();
|
||||
assertEquals(200, response.statusCode());
|
||||
assertEquals(tempFileAsString, response.body());
|
||||
}
|
||||
assertEquals(LOOPS + 1, resultMap.size());
|
||||
}
|
||||
|
||||
// Test 2 - of(...) populating the given Map, everything as a String
|
||||
@Test
|
||||
public void testTypeStringOfMap() throws Exception {
|
||||
String tempFileAsString = new String(Files.readAllBytes(tempFile), UTF_8);
|
||||
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>>
|
||||
resultMap = new ConcurrentHashMap<>();
|
||||
|
||||
PushPromiseHandler<String> pph =
|
||||
PushPromiseHandler.of(pushPromise -> BodyHandlers.ofString(UTF_8),
|
||||
resultMap);
|
||||
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
|
||||
CompletableFuture<HttpResponse<String>> cf =
|
||||
client.sendAsync(request, BodyHandlers.ofString(UTF_8), pph);
|
||||
cf.join();
|
||||
resultMap.put(request, cf);
|
||||
System.err.println("results.size: " + resultMap.size());
|
||||
for (HttpRequest r : resultMap.keySet()) {
|
||||
HttpResponse<String> response = resultMap.get(r).join();
|
||||
assertEquals(200, response.statusCode());
|
||||
assertEquals(tempFileAsString, response.body());
|
||||
}
|
||||
assertEquals(LOOPS + 1, resultMap.size());
|
||||
}
|
||||
|
||||
// --- Path ---
|
||||
|
||||
static final Path dir = Paths.get(".", "serverPush");
|
||||
static BodyHandler<Path> requestToPath(HttpRequest req) {
|
||||
URI u = req.uri();
|
||||
Path path = Paths.get(dir.toString(), u.getPath());
|
||||
try {
|
||||
Files.createDirectories(path.getParent());
|
||||
} catch (IOException ee) {
|
||||
throw new UncheckedIOException(ee);
|
||||
}
|
||||
return BodyHandlers.ofFile(path);
|
||||
}
|
||||
|
||||
// Test 3 - custom written push promise handler, everything as a Path
|
||||
@Test
|
||||
public void testTypePath() throws Exception {
|
||||
String tempFileAsString = new String(Files.readAllBytes(tempFile), UTF_8);
|
||||
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<Path>>> resultsMap
|
||||
= new ConcurrentHashMap<>();
|
||||
|
||||
PushPromiseHandler<Path> pushPromiseHandler = (initial, pushRequest, acceptor) -> {
|
||||
BodyHandler<Path> pp = requestToPath(pushRequest);
|
||||
CompletableFuture<HttpResponse<Path>> cf = acceptor.apply(pp);
|
||||
resultsMap.put(pushRequest, cf);
|
||||
};
|
||||
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
|
||||
CompletableFuture<HttpResponse<Path>> cf =
|
||||
client.sendAsync(request, requestToPath(request), pushPromiseHandler);
|
||||
cf.join();
|
||||
resultsMap.put(request, cf);
|
||||
|
||||
for (HttpRequest r : resultsMap.keySet()) {
|
||||
HttpResponse<Path> response = resultsMap.get(r).join();
|
||||
assertEquals(200, response.statusCode());
|
||||
String fileAsString = new String(Files.readAllBytes(response.body()), UTF_8);
|
||||
assertEquals(tempFileAsString, fileAsString);
|
||||
}
|
||||
assertEquals(LOOPS + 1, resultsMap.size());
|
||||
}
|
||||
|
||||
// Test 4 - of(...) populating the given Map, everything as a Path
|
||||
@Test
|
||||
public void testTypePathOfMap() throws Exception {
|
||||
String tempFileAsString = new String(Files.readAllBytes(tempFile), UTF_8);
|
||||
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<Path>>> resultsMap
|
||||
= new ConcurrentHashMap<>();
|
||||
|
||||
PushPromiseHandler<Path> pushPromiseHandler =
|
||||
PushPromiseHandler.of(pushRequest -> requestToPath(pushRequest),
|
||||
resultsMap);
|
||||
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
|
||||
CompletableFuture<HttpResponse<Path>> cf =
|
||||
client.sendAsync(request, requestToPath(request), pushPromiseHandler);
|
||||
cf.join();
|
||||
resultsMap.put(request, cf);
|
||||
|
||||
for (HttpRequest r : resultsMap.keySet()) {
|
||||
HttpResponse<Path> response = resultsMap.get(r).join();
|
||||
assertEquals(200, response.statusCode());
|
||||
String fileAsString = new String(Files.readAllBytes(response.body()), UTF_8);
|
||||
assertEquals(tempFileAsString, fileAsString);
|
||||
}
|
||||
assertEquals(LOOPS + 1, resultsMap.size());
|
||||
}
|
||||
|
||||
// --- Consumer<byte[]> ---
|
||||
|
||||
static class ByteArrayConsumer implements Consumer<Optional<byte[]>> {
|
||||
volatile List<byte[]> listByteArrays = new ArrayList<>();
|
||||
volatile byte[] accumulatedBytes;
|
||||
|
||||
public byte[] getAccumulatedBytes() { return accumulatedBytes; }
|
||||
|
||||
@Override
|
||||
public void accept(Optional<byte[]> optionalBytes) {
|
||||
assert accumulatedBytes == null;
|
||||
if (!optionalBytes.isPresent()) {
|
||||
int size = listByteArrays.stream().mapToInt(ba -> ba.length).sum();
|
||||
ByteBuffer bb = ByteBuffer.allocate(size);
|
||||
listByteArrays.stream().forEach(ba -> bb.put(ba));
|
||||
accumulatedBytes = bb.array();
|
||||
} else {
|
||||
listByteArrays.add(optionalBytes.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test 5 - custom written handler, everything as a consumer of optional byte[]
|
||||
@Test
|
||||
public void testTypeByteArrayConsumer() throws Exception {
|
||||
String tempFileAsString = new String(Files.readAllBytes(tempFile), UTF_8);
|
||||
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<Void>>> resultsMap
|
||||
= new ConcurrentHashMap<>();
|
||||
Map<HttpRequest,ByteArrayConsumer> byteArrayConsumerMap
|
||||
= new ConcurrentHashMap<>();
|
||||
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
|
||||
ByteArrayConsumer bac = new ByteArrayConsumer();
|
||||
byteArrayConsumerMap.put(request, bac);
|
||||
|
||||
PushPromiseHandler<Void> pushPromiseHandler = (initial, pushRequest, acceptor) -> {
|
||||
CompletableFuture<HttpResponse<Void>> cf = acceptor.apply(
|
||||
(info) -> {
|
||||
ByteArrayConsumer bc = new ByteArrayConsumer();
|
||||
byteArrayConsumerMap.put(pushRequest, bc);
|
||||
return BodySubscribers.ofByteArrayConsumer(bc); } );
|
||||
resultsMap.put(pushRequest, cf);
|
||||
};
|
||||
|
||||
CompletableFuture<HttpResponse<Void>> cf =
|
||||
client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(bac), pushPromiseHandler);
|
||||
cf.join();
|
||||
resultsMap.put(request, cf);
|
||||
|
||||
for (HttpRequest r : resultsMap.keySet()) {
|
||||
HttpResponse<Void> response = resultsMap.get(r).join();
|
||||
assertEquals(200, response.statusCode());
|
||||
byte[] ba = byteArrayConsumerMap.get(r).getAccumulatedBytes();
|
||||
String result = new String(ba, UTF_8);
|
||||
assertEquals(tempFileAsString, result);
|
||||
}
|
||||
assertEquals(LOOPS + 1, resultsMap.size());
|
||||
}
|
||||
|
||||
// Test 6 - of(...) populating the given Map, everything as a consumer of optional byte[]
|
||||
@Test
|
||||
public void testTypeByteArrayConsumerOfMap() throws Exception {
|
||||
String tempFileAsString = new String(Files.readAllBytes(tempFile), UTF_8);
|
||||
ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<Void>>> resultsMap
|
||||
= new ConcurrentHashMap<>();
|
||||
Map<HttpRequest,ByteArrayConsumer> byteArrayConsumerMap
|
||||
= new ConcurrentHashMap<>();
|
||||
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
|
||||
ByteArrayConsumer bac = new ByteArrayConsumer();
|
||||
byteArrayConsumerMap.put(request, bac);
|
||||
|
||||
PushPromiseHandler<Void> pushPromiseHandler =
|
||||
PushPromiseHandler.of(
|
||||
pushRequest -> {
|
||||
ByteArrayConsumer bc = new ByteArrayConsumer();
|
||||
byteArrayConsumerMap.put(pushRequest, bc);
|
||||
return BodyHandlers.ofByteArrayConsumer(bc);
|
||||
},
|
||||
resultsMap);
|
||||
|
||||
CompletableFuture<HttpResponse<Void>> cf =
|
||||
client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(bac), pushPromiseHandler);
|
||||
cf.join();
|
||||
resultsMap.put(request, cf);
|
||||
|
||||
for (HttpRequest r : resultsMap.keySet()) {
|
||||
HttpResponse<Void> response = resultsMap.get(r).join();
|
||||
assertEquals(200, response.statusCode());
|
||||
byte[] ba = byteArrayConsumerMap.get(r).getAccumulatedBytes();
|
||||
String result = new String(ba, UTF_8);
|
||||
assertEquals(tempFileAsString, result);
|
||||
}
|
||||
assertEquals(LOOPS + 1, resultsMap.size());
|
||||
}
|
||||
}
|
||||
261
test/jdk/java/net/httpclient/http2/ServerPushWithDiffTypes.java
Normal file
261
test/jdk/java/net/httpclient/http2/ServerPushWithDiffTypes.java
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
* Copyright (c) 2018, 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.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* @run junit/othervm
|
||||
* -Djdk.internal.httpclient.debug=true
|
||||
* -Djdk.httpclient.HttpClient.log=errors,requests,responses
|
||||
* ServerPushWithDiffTypes
|
||||
*/
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.*;
|
||||
import java.net.http.*;
|
||||
import java.net.http.HttpResponse.BodyHandler;
|
||||
import java.net.http.HttpResponse.PushPromiseHandler;
|
||||
import java.net.http.HttpResponse.BodySubscriber;
|
||||
import java.net.http.HttpResponse.BodySubscribers;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.BiPredicate;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class ServerPushWithDiffTypes {
|
||||
|
||||
static Map<String,String> PUSH_PROMISES = Map.of(
|
||||
"/x/y/z/1", "the first push promise body",
|
||||
"/x/y/z/2", "the second push promise body",
|
||||
"/x/y/z/3", "the third push promise body",
|
||||
"/x/y/z/4", "the fourth push promise body",
|
||||
"/x/y/z/5", "the fifth push promise body",
|
||||
"/x/y/z/6", "the sixth push promise body",
|
||||
"/x/y/z/7", "the seventh push promise body",
|
||||
"/x/y/z/8", "the eighth push promise body",
|
||||
"/x/y/z/9", "the ninth push promise body"
|
||||
);
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
Http2TestServer server = null;
|
||||
try {
|
||||
server = new Http2TestServer(false, 0);
|
||||
Http2Handler handler =
|
||||
new ServerPushHandler("the main response body",
|
||||
PUSH_PROMISES);
|
||||
server.addHandler(handler, "/");
|
||||
server.start();
|
||||
int port = server.getAddress().getPort();
|
||||
System.err.println("Server listening on port " + port);
|
||||
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
// use multi-level path
|
||||
URI uri = new URI("http://localhost:" + port + "/foo/a/b/c");
|
||||
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
|
||||
|
||||
ConcurrentMap<HttpRequest,CompletableFuture<HttpResponse<BodyAndType<?>>>>
|
||||
results = new ConcurrentHashMap<>();
|
||||
PushPromiseHandler<BodyAndType<?>> bh = PushPromiseHandler.of(
|
||||
(pushRequest) -> new BodyAndTypeHandler(pushRequest), results);
|
||||
|
||||
CompletableFuture<HttpResponse<BodyAndType<?>>> cf =
|
||||
client.sendAsync(request, new BodyAndTypeHandler(request), bh);
|
||||
results.put(request, cf);
|
||||
cf.join();
|
||||
|
||||
assertEquals(PUSH_PROMISES.size() + 1, results.size());
|
||||
|
||||
for (HttpRequest r : results.keySet()) {
|
||||
URI u = r.uri();
|
||||
BodyAndType<?> body = results.get(r).get().body();
|
||||
String result;
|
||||
// convert all body types to String for easier comparison
|
||||
if (body.type() == String.class) {
|
||||
result = (String)body.getBody();
|
||||
} else if (body.type() == byte[].class) {
|
||||
byte[] bytes = (byte[])body.getBody();
|
||||
result = new String(bytes, UTF_8);
|
||||
} else if (Path.class.isAssignableFrom(body.type())) {
|
||||
Path path = (Path)body.getBody();
|
||||
result = new String(Files.readAllBytes(path), UTF_8);
|
||||
} else {
|
||||
throw new AssertionError("Unknown:" + body.type());
|
||||
}
|
||||
|
||||
System.err.printf("%s -> %s\n", u.toString(), result.toString());
|
||||
String expected = PUSH_PROMISES.get(r.uri().getPath());
|
||||
if (expected == null)
|
||||
expected = "the main response body";
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
interface BodyAndType<T> {
|
||||
Class<T> type();
|
||||
T getBody();
|
||||
}
|
||||
|
||||
static final Path WORK_DIR = Paths.get(".");
|
||||
|
||||
static class BodyAndTypeHandler implements BodyHandler<BodyAndType<?>> {
|
||||
int count;
|
||||
final HttpRequest request;
|
||||
|
||||
BodyAndTypeHandler(HttpRequest request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponse.BodySubscriber<BodyAndType<?>> apply(HttpResponse.ResponseInfo info) {
|
||||
int whichType = count++ % 3; // real world may base this on the request metadata
|
||||
switch (whichType) {
|
||||
case 0: // String
|
||||
return new BodyAndTypeSubscriber(BodySubscribers.ofString(UTF_8));
|
||||
case 1: // byte[]
|
||||
return new BodyAndTypeSubscriber(BodySubscribers.ofByteArray());
|
||||
case 2: // Path
|
||||
URI u = request.uri();
|
||||
Path path = Paths.get(WORK_DIR.toString(), u.getPath());
|
||||
try {
|
||||
Files.createDirectories(path.getParent());
|
||||
} catch (IOException ee) {
|
||||
throw new UncheckedIOException(ee);
|
||||
}
|
||||
return new BodyAndTypeSubscriber(BodySubscribers.ofFile(path));
|
||||
default:
|
||||
throw new AssertionError("Unexpected " + whichType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class BodyAndTypeSubscriber<T>
|
||||
implements HttpResponse.BodySubscriber<BodyAndType<T>>
|
||||
{
|
||||
private static class BodyAndTypeImpl<T> implements BodyAndType<T> {
|
||||
private final Class<T> type;
|
||||
private final T body;
|
||||
public BodyAndTypeImpl(Class<T> type, T body) { this.type = type; this.body = body; }
|
||||
@Override public Class<T> type() { return type; }
|
||||
@Override public T getBody() { return body; }
|
||||
}
|
||||
|
||||
private final BodySubscriber<?> bodySubscriber;
|
||||
private final CompletableFuture<BodyAndType<T>> cf;
|
||||
|
||||
BodyAndTypeSubscriber(BodySubscriber bodySubscriber) {
|
||||
this.bodySubscriber = bodySubscriber;
|
||||
cf = new CompletableFuture<>();
|
||||
bodySubscriber.getBody().whenComplete(
|
||||
(r,t) -> cf.complete(new BodyAndTypeImpl(r.getClass(), r)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Flow.Subscription subscription) {
|
||||
bodySubscriber.onSubscribe(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<ByteBuffer> item) {
|
||||
bodySubscriber.onNext(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
bodySubscriber.onError(throwable);
|
||||
cf.completeExceptionally(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
bodySubscriber.onComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<BodyAndType<T>> getBody() {
|
||||
return cf;
|
||||
}
|
||||
}
|
||||
|
||||
// --- server push handler ---
|
||||
static class ServerPushHandler implements Http2Handler {
|
||||
|
||||
private final String mainResponseBody;
|
||||
private final Map<String,String> promises;
|
||||
|
||||
public ServerPushHandler(String mainResponseBody,
|
||||
Map<String,String> promises)
|
||||
throws Exception
|
||||
{
|
||||
Objects.requireNonNull(promises);
|
||||
this.mainResponseBody = mainResponseBody;
|
||||
this.promises = promises;
|
||||
}
|
||||
|
||||
public void handle(Http2TestExchange exchange) throws IOException {
|
||||
System.err.println("Server: handle " + exchange);
|
||||
try (InputStream is = exchange.getRequestBody()) {
|
||||
is.readAllBytes();
|
||||
}
|
||||
|
||||
if (exchange.serverPushAllowed()) {
|
||||
pushPromises(exchange);
|
||||
}
|
||||
|
||||
// response data for the main response
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
byte[] bytes = mainResponseBody.getBytes(UTF_8);
|
||||
exchange.sendResponseHeaders(200, bytes.length);
|
||||
os.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
|
||||
|
||||
private void pushPromises(Http2TestExchange exchange) throws IOException {
|
||||
URI requestURI = exchange.getRequestURI();
|
||||
for (Map.Entry<String,String> promise : promises.entrySet()) {
|
||||
URI uri = requestURI.resolve(promise.getKey());
|
||||
InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8));
|
||||
Map<String,List<String>> map = Map.of("X-Promise", List.of(promise.getKey()));
|
||||
HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
|
||||
// TODO: add some check on headers, maybe
|
||||
exchange.serverPush(uri, headers, is);
|
||||
}
|
||||
System.err.println("Server: All pushes sent");
|
||||
}
|
||||
}
|
||||
}
|
||||
221
test/jdk/java/net/httpclient/http2/SimpleGet.java
Normal file
221
test/jdk/java/net/httpclient/http2/SimpleGet.java
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* Copyright (c) 2015, 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
|
||||
* @bug 8087112
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil
|
||||
* jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* @run junit/othervm -XX:+CrashOnOutOfMemoryError SimpleGet
|
||||
* @run junit/othervm -XX:+CrashOnOutOfMemoryError
|
||||
* -Dsimpleget.repeat=1 -Dsimpleget.chunks=1 -Dsimpleget.requests=1000
|
||||
* SimpleGet
|
||||
* @run junit/othervm -Dsimpleget.requests=150
|
||||
* -Dsimpleget.chunks=16384
|
||||
* -Djdk.httpclient.redirects.retrylimit=5
|
||||
* -Djdk.httpclient.HttpClient.log=errors
|
||||
* -XX:+CrashOnOutOfMemoryError
|
||||
* -XX:+HeapDumpOnOutOfMemoryError
|
||||
* SimpleGet
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Builder;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentSkipListSet;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Predicate;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class SimpleGet implements HttpServerAdapters {
|
||||
static HttpTestServer httpsServer;
|
||||
static HttpClient client = null;
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
static String httpsURIString;
|
||||
static ExecutorService serverExec = Executors.newVirtualThreadPerTaskExecutor();
|
||||
|
||||
static void initialize() throws Exception {
|
||||
try {
|
||||
client = getClient();
|
||||
|
||||
httpsServer = HttpTestServer.create(HTTP_2, sslContext, serverExec);
|
||||
httpsServer.addHandler(new TestHandler(), "/");
|
||||
httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/";
|
||||
|
||||
httpsServer.start();
|
||||
warmup();
|
||||
} catch (Throwable e) {
|
||||
System.err.println("Throwing now");
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static void warmup() throws Exception {
|
||||
// warmup server
|
||||
try (var client2 = createClient(sslContext)) {
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString))
|
||||
.version(HTTP_2)
|
||||
.HEAD().build();
|
||||
client2.send(request, BodyHandlers.discarding());
|
||||
}
|
||||
|
||||
// warmup client
|
||||
var httpsServer2 = HttpTestServer.create(HTTP_2, sslContext,
|
||||
Executors.newVirtualThreadPerTaskExecutor());
|
||||
httpsServer2.addHandler(new TestHandler(), "/");
|
||||
var httpsURIString2 = "https://" + httpsServer2.serverAuthority() + "/bar/";
|
||||
httpsServer2.start();
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString2))
|
||||
.version(HTTP_2)
|
||||
.HEAD().build();
|
||||
client.send(request, BodyHandlers.discarding());
|
||||
} finally {
|
||||
httpsServer2.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new SimpleGet().test();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
try {
|
||||
long prestart = System.nanoTime();
|
||||
initialize();
|
||||
long done = System.nanoTime();
|
||||
System.out.println("Stat: Initialization and warmup took " + TimeUnit.NANOSECONDS.toMillis(done - prestart) + " millis");
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString))
|
||||
.version(HTTP_2)
|
||||
.GET().build();
|
||||
long start = System.nanoTime();
|
||||
var resp = client.send(request, BodyHandlers.ofByteArrayConsumer(b -> {}));
|
||||
assertEquals(200, resp.statusCode());
|
||||
long elapsed = System.nanoTime() - start;
|
||||
System.out.println("Stat: First request took: " + elapsed + " nanos (" + TimeUnit.NANOSECONDS.toMillis(elapsed) + " ms)");
|
||||
final int max = property("simpleget.requests", 50);
|
||||
;
|
||||
List<CompletableFuture<HttpResponse<Void>>> list = new ArrayList<>(max);
|
||||
Set<String> connections = new ConcurrentSkipListSet<>();
|
||||
long start2 = System.nanoTime();
|
||||
for (int i = 0; i < max; i++) {
|
||||
var cf = client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(b -> {}))
|
||||
.whenComplete((r, t) -> Optional.ofNullable(r)
|
||||
.flatMap(HttpResponse::connectionLabel)
|
||||
.ifPresent(connections::add));
|
||||
list.add(cf);
|
||||
//cf.get(); // uncomment to test with serial instead of concurrent requests
|
||||
}
|
||||
try {
|
||||
CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join();
|
||||
} finally {
|
||||
long elapsed2 = System.nanoTime() - start2;
|
||||
long completed = list.stream().filter(CompletableFuture::isDone)
|
||||
.filter(Predicate.not(CompletableFuture::isCompletedExceptionally)).count();
|
||||
connections.forEach(System.out::println);
|
||||
if (completed > 0) {
|
||||
System.out.println("Stat: Next " + completed + " requests took: " + elapsed2 + " nanos ("
|
||||
+ TimeUnit.NANOSECONDS.toMillis(elapsed2) + "ms for " + completed + " requests): "
|
||||
+ elapsed2 / completed + " nanos per request ("
|
||||
+ TimeUnit.NANOSECONDS.toMillis(elapsed2) / completed + " ms) on "
|
||||
+ connections.size() + " connections");
|
||||
}
|
||||
}
|
||||
list.forEach((cf) -> assertEquals(200, cf.join().statusCode()));
|
||||
} catch (Throwable tt) {
|
||||
System.err.println("tt caught");
|
||||
tt.printStackTrace();
|
||||
throw tt;
|
||||
} finally {
|
||||
httpsServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
static HttpClient createClient(SSLContext sslContext) {
|
||||
return HttpClient.newBuilder()
|
||||
.sslContext(sslContext)
|
||||
.version(HTTP_2)
|
||||
.proxy(Builder.NO_PROXY)
|
||||
.executor(Executors.newVirtualThreadPerTaskExecutor())
|
||||
.build();
|
||||
}
|
||||
|
||||
static HttpClient getClient() {
|
||||
if (client == null) {
|
||||
client = createClient(sslContext);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
static int property(String name, int defaultValue) {
|
||||
return Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue)));
|
||||
}
|
||||
|
||||
// 32 * 32 * 1024 * 10 chars = 10Mb responses
|
||||
// 50 requests => 500Mb
|
||||
// 100 requests => 1Gb
|
||||
// 1000 requests => 10Gb
|
||||
private final static int REPEAT = property("simpleget.repeat", 32);
|
||||
private final static String RESPONSE = "abcdefghij".repeat(property("simpleget.chunks", 1024*32));
|
||||
private final static byte[] RESPONSE_BYTES = RESPONSE.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static class TestHandler implements HttpTestHandler {
|
||||
@Override
|
||||
public void handle(HttpTestExchange t) throws IOException {
|
||||
try (var in = t.getRequestBody()) {
|
||||
byte[] input = in.readAllBytes();
|
||||
t.sendResponseHeaders(200, RESPONSE_BYTES.length * REPEAT);
|
||||
try (var out = t.getResponseBody()) {
|
||||
if (t.getRequestMethod().equals("HEAD")) return;
|
||||
for (int i=0; i<REPEAT; i++) {
|
||||
out.write(RESPONSE_BYTES);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
394
test/jdk/java/net/httpclient/http2/StreamFlowControlTest.java
Normal file
394
test/jdk/java/net/httpclient/http2/StreamFlowControlTest.java
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
/*
|
||||
* 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
|
||||
* @bug 8342075 8343855
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext
|
||||
* @run junit/othervm -Djdk.internal.httpclient.debug=true
|
||||
* -Djdk.httpclient.connectionWindowSize=65535
|
||||
* -Djdk.httpclient.windowsize=16384
|
||||
* StreamFlowControlTest
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpHeadOrGetHandler;
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer;
|
||||
import jdk.httpclient.test.lib.http2.BodyOutputStream;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
|
||||
import jdk.internal.net.http.common.HttpHeadersBuilder;
|
||||
import jdk.internal.net.http.frame.SettingsFrame;
|
||||
import jdk.test.lib.Utils;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.NANOSECONDS;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public class StreamFlowControlTest {
|
||||
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
private static HttpTestServer http2TestServer; // HTTP/2 ( h2c )
|
||||
private static HttpTestServer https2TestServer; // HTTP/2 ( h2 )
|
||||
private static String http2URI;
|
||||
private static String https2URI;
|
||||
private static final AtomicInteger reqid = new AtomicInteger();
|
||||
final static int WINDOW =
|
||||
Integer.getInteger("jdk.httpclient.windowsize", 2 * 16 * 1024);
|
||||
|
||||
|
||||
|
||||
public static Object[][] variants() {
|
||||
return new Object[][] {
|
||||
{ http2URI, false },
|
||||
{ https2URI, false },
|
||||
{ http2URI, true },
|
||||
{ https2URI, true },
|
||||
};
|
||||
}
|
||||
|
||||
static void sleep(long wait) throws InterruptedException {
|
||||
if (wait <= 0) return;
|
||||
long remaining = Utils.adjustTimeout(wait);
|
||||
long start = System.nanoTime();
|
||||
while (remaining > 0) {
|
||||
Thread.sleep(remaining);
|
||||
long end = System.nanoTime();
|
||||
remaining = remaining - NANOSECONDS.toMillis(end - start);
|
||||
}
|
||||
System.out.printf("Waited %s ms%n",
|
||||
NANOSECONDS.toMillis(System.nanoTime() - start));
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("variants")
|
||||
void test(String uri,
|
||||
boolean sameClient)
|
||||
throws Exception
|
||||
{
|
||||
System.out.printf("%ntesting test(%s, %s)%n", uri, sameClient);
|
||||
ConcurrentHashMap<String, CompletableFuture<String>> responseSent = new ConcurrentHashMap<>();
|
||||
FCHttp2TestExchange.setResponseSentCB((s) -> responseSent.get(s).complete(s));
|
||||
|
||||
HttpClient client = null;
|
||||
try {
|
||||
int max = sameClient ? 10 : 3;
|
||||
String label = null;
|
||||
for (int i = 0; i < max; i++) {
|
||||
if (!sameClient || client == null)
|
||||
client = HttpClient.newBuilder().sslContext(sslContext).build();
|
||||
|
||||
String query = "reqId=" + reqid.incrementAndGet();
|
||||
URI uriWithQuery = URI.create(uri + "?" + query);
|
||||
CompletableFuture<String> sent = new CompletableFuture<>();
|
||||
responseSent.put(query, sent);
|
||||
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
|
||||
.GET()
|
||||
.build();
|
||||
System.out.println("\nSending request:" + uriWithQuery);
|
||||
final HttpClient cc = client;
|
||||
try {
|
||||
HttpResponse<InputStream> response = cc.send(request, BodyHandlers.ofInputStream());
|
||||
if (sameClient) {
|
||||
String key = response.headers().firstValue("X-Connection-Key").get();
|
||||
if (label == null) label = key;
|
||||
assertEquals(label, key, "Unexpected key for " + query);
|
||||
}
|
||||
sent.join();
|
||||
// we have to pull to get the exception, but slow enough
|
||||
// so that DataFrames are buffered up to the point that
|
||||
// the window is exceeded...
|
||||
long wait = uri.startsWith("https://") ? 800 : 500;
|
||||
try (InputStream is = response.body()) {
|
||||
byte[] discard = new byte[WINDOW/4];
|
||||
for (int j=0; j<2; j++) {
|
||||
sleep(wait);
|
||||
if (is.read(discard) < 0) break;
|
||||
}
|
||||
is.readAllBytes();
|
||||
}
|
||||
// we could fail here if we haven't waited long enough
|
||||
fail("Expected exception, got :" + response + ", should sleep time be raised?");
|
||||
} catch (IOException ioe) {
|
||||
System.out.println("Got EXPECTED: " + ioe);
|
||||
assertDetailMessage(ioe, i);
|
||||
} finally {
|
||||
if (!sameClient && client != null) {
|
||||
client.close();
|
||||
client = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (sameClient && client != null) client.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("variants")
|
||||
void testAsync(String uri,
|
||||
boolean sameClient)
|
||||
{
|
||||
System.out.printf("%ntesting testAsync(%s, %s)%n", uri, sameClient);
|
||||
ConcurrentHashMap<String, CompletableFuture<String>> responseSent = new ConcurrentHashMap<>();
|
||||
FCHttp2TestExchange.setResponseSentCB((s) -> responseSent.get(s).complete(s));
|
||||
|
||||
HttpClient client = null;
|
||||
try {
|
||||
int max = sameClient ? 5 : 3;
|
||||
String label = null;
|
||||
for (int i = 0; i < max; i++) {
|
||||
if (!sameClient || client == null)
|
||||
client = HttpClient.newBuilder().sslContext(sslContext).build();
|
||||
|
||||
String query = "reqId=" + reqid.incrementAndGet();
|
||||
URI uriWithQuery = URI.create(uri + "?" + query);
|
||||
CompletableFuture<String> sent = new CompletableFuture<>();
|
||||
responseSent.put(query, sent);
|
||||
HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
|
||||
.GET()
|
||||
.build();
|
||||
System.out.println("\nSending request:" + uriWithQuery);
|
||||
final HttpClient cc = client;
|
||||
|
||||
Throwable t = null;
|
||||
try {
|
||||
HttpResponse<InputStream> response = cc.sendAsync(request, BodyHandlers.ofInputStream()).get();
|
||||
if (sameClient) {
|
||||
String key = response.headers().firstValue("X-Connection-Key").get();
|
||||
if (label == null) label = key;
|
||||
assertEquals(label, key, "Unexpected key for " + query);
|
||||
}
|
||||
sent.join();
|
||||
long wait = uri.startsWith("https://") ? 800 : 350;
|
||||
try (InputStream is = response.body()) {
|
||||
byte[] discard = new byte[WINDOW/4];
|
||||
for (int j=0; j<2; j++) {
|
||||
sleep(wait);
|
||||
if (is.read(discard) < 0) break;
|
||||
}
|
||||
is.readAllBytes();
|
||||
}
|
||||
// we could fail here if we haven't waited long enough
|
||||
fail("Expected exception, got :" + response + ", should sleep time be raised?");
|
||||
} catch (Throwable t0) {
|
||||
System.out.println("Got EXPECTED: " + t0);
|
||||
if (t0 instanceof ExecutionException) {
|
||||
t0 = t0.getCause();
|
||||
}
|
||||
t = t0;
|
||||
} finally {
|
||||
if (!sameClient && client != null) {
|
||||
client.close();
|
||||
client = null;
|
||||
}
|
||||
}
|
||||
assertDetailMessage(t, i);
|
||||
}
|
||||
} finally {
|
||||
if (sameClient && client != null) client.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Assertions based on implementation specific detail messages. Keep in
|
||||
// sync with implementation.
|
||||
static void assertDetailMessage(Throwable throwable, int iterationIndex) {
|
||||
try {
|
||||
Throwable cause = throwable;
|
||||
while (cause != null) {
|
||||
if (cause instanceof ProtocolException) {
|
||||
if (cause.getMessage().matches("stream [0-9]+ flow control window exceeded")) {
|
||||
System.out.println("Found expected exception: " + cause);
|
||||
return;
|
||||
}
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
throw new AssertionError(
|
||||
"ProtocolException(\"stream X flow control window exceeded\") not found",
|
||||
throwable);
|
||||
} catch (AssertionError e) {
|
||||
System.out.println("Exception does not match expectation: " + throwable);
|
||||
throwable.printStackTrace(System.out);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws Exception {
|
||||
var http2TestServerImpl = new Http2TestServer("localhost", false, 0);
|
||||
http2TestServerImpl.addHandler(new Http2TestHandler(), "/http2/");
|
||||
http2TestServer = HttpTestServer.of(http2TestServerImpl);
|
||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/x";
|
||||
|
||||
var https2TestServerImpl = new Http2TestServer("localhost", true, sslContext);
|
||||
https2TestServerImpl.addHandler(new Http2TestHandler(), "/https2/");
|
||||
https2TestServer = HttpTestServer.of(https2TestServerImpl);
|
||||
https2TestServer.addHandler(new HttpHeadOrGetHandler(), "/https2/head/");
|
||||
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/x";
|
||||
String h2Head = "https://" + https2TestServer.serverAuthority() + "/https2/head/z";
|
||||
|
||||
// Override the default exchange supplier with a custom one to enable
|
||||
// particular test scenarios
|
||||
http2TestServerImpl.setExchangeSupplier(FCHttp2TestExchange::new);
|
||||
https2TestServerImpl.setExchangeSupplier(FCHttp2TestExchange::new);
|
||||
|
||||
http2TestServer.start();
|
||||
https2TestServer.start();
|
||||
|
||||
// warmup to eliminate delay due to SSL class loading and initialization.
|
||||
try (var client = HttpClient.newBuilder().sslContext(sslContext).build()) {
|
||||
var request = HttpRequest.newBuilder(URI.create(h2Head)).HEAD().build();
|
||||
var resp = client.send(request, BodyHandlers.discarding());
|
||||
assertEquals(200, resp.statusCode());
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void teardown() throws Exception {
|
||||
http2TestServer.stop();
|
||||
https2TestServer.stop();
|
||||
}
|
||||
|
||||
static class Http2TestHandler implements Http2Handler {
|
||||
|
||||
@Override
|
||||
public void handle(Http2TestExchange t) throws IOException {
|
||||
String query = t.getRequestURI().getRawQuery();
|
||||
|
||||
try (InputStream is = t.getRequestBody();
|
||||
OutputStream os = t.getResponseBody()) {
|
||||
|
||||
byte[] bytes = is.readAllBytes();
|
||||
if (bytes.length != 0) {
|
||||
System.out.println("Server " + t.getLocalAddress() + " received:\n"
|
||||
+ t.getRequestURI() + ": " + new String(bytes, StandardCharsets.UTF_8));
|
||||
} else {
|
||||
System.out.println("No request body for " + t.getRequestMethod());
|
||||
}
|
||||
|
||||
t.getResponseHeaders().setHeader("X-Connection-Key", t.getConnectionKey());
|
||||
|
||||
if (bytes.length == 0) {
|
||||
bytes = "no request body!"
|
||||
.repeat(100).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
final int maxChunkSize;
|
||||
if (t instanceof FCHttp2TestExchange fct) {
|
||||
maxChunkSize = Math.min(WINDOW, fct.conn.getMaxFrameSize());
|
||||
} else {
|
||||
maxChunkSize = Math.min(WINDOW, SettingsFrame.MAX_FRAME_SIZE);
|
||||
}
|
||||
byte[] resp = bytes.length <= maxChunkSize
|
||||
? bytes
|
||||
: Arrays.copyOfRange(bytes, 0, maxChunkSize);
|
||||
int max = (WINDOW / resp.length) + 2;
|
||||
// send in chunks
|
||||
t.sendResponseHeaders(200, 0);
|
||||
for (int i = 0; i <= max; i++) {
|
||||
if (t instanceof FCHttp2TestExchange fct) {
|
||||
try {
|
||||
// we don't wait for the stream window, but we want
|
||||
// to wait for the connection window
|
||||
fct.conn.obtainConnectionWindow(resp.length);
|
||||
} catch (InterruptedException ie) {
|
||||
var ioe = new InterruptedIOException(ie.toString());
|
||||
ioe.initCause(ie);
|
||||
throw ioe;
|
||||
}
|
||||
}
|
||||
try {
|
||||
((BodyOutputStream) os).writeUncontrolled(resp, 0, resp.length);
|
||||
} catch (IOException x) {
|
||||
if (t instanceof FCHttp2TestExchange fct) {
|
||||
fct.conn.updateConnectionWindow(resp.length);
|
||||
}
|
||||
throw x;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (t instanceof FCHttp2TestExchange fct) {
|
||||
fct.responseSent(query);
|
||||
} else {
|
||||
fail("Exchange is not %s but %s"
|
||||
.formatted(FCHttp2TestExchange.class.getName(), t.getClass().getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A custom Http2TestExchangeImpl that overrides sendResponseHeaders to
|
||||
// allow headers to be sent with a number of CONTINUATION frames.
|
||||
static class FCHttp2TestExchange extends Http2TestExchangeImpl {
|
||||
static volatile Consumer<String> responseSentCB;
|
||||
static void setResponseSentCB(Consumer<String> responseSentCB) {
|
||||
FCHttp2TestExchange.responseSentCB = responseSentCB;
|
||||
}
|
||||
|
||||
final Http2TestServerConnection conn;
|
||||
FCHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
|
||||
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
|
||||
SSLSession sslSession, BodyOutputStream os,
|
||||
Http2TestServerConnection conn, boolean pushAllowed) {
|
||||
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
|
||||
this.conn = conn;
|
||||
}
|
||||
public void responseSent(String query) {
|
||||
System.out.println("Server: response sent for " + query);
|
||||
responseSentCB.accept(query);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
277
test/jdk/java/net/httpclient/http2/TLSConnection.java
Normal file
277
test/jdk/java/net/httpclient/http2/TLSConnection.java
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2025, 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.
|
||||
*/
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.Security;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8150769 8157107 8371887
|
||||
* @library /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* @summary Checks that SSL parameters can be set for HTTP/2 connection
|
||||
* @run main/othervm
|
||||
* -Djdk.internal.httpclient.debug=true
|
||||
* -Djdk.httpclient.HttpClient.log=all
|
||||
* TLSConnection
|
||||
*/
|
||||
public class TLSConnection {
|
||||
|
||||
private static final String KEYSTORE = System.getProperty("test.src")
|
||||
+ File.separator + "keystore.p12";
|
||||
private static final String PASSWORD = "password";
|
||||
|
||||
private static final SSLParameters USE_DEFAULT_SSL_PARAMETERS = new SSLParameters();
|
||||
|
||||
// expect highest supported version we know about
|
||||
static String expectedTLSVersion(SSLContext ctx) throws Exception {
|
||||
if (ctx == null)
|
||||
ctx = SSLContext.getDefault();
|
||||
SSLParameters params = ctx.getSupportedSSLParameters();
|
||||
String[] protocols = params.getProtocols();
|
||||
for (String prot : protocols) {
|
||||
if (prot.equals("TLSv1.3"))
|
||||
return "TLSv1.3";
|
||||
}
|
||||
return "TLSv1.2";
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// re-enable 3DES
|
||||
Security.setProperty("jdk.tls.disabledAlgorithms", "");
|
||||
|
||||
// enable all logging
|
||||
System.setProperty("jdk.httpclient.HttpClient.log", "all,frames:all");
|
||||
|
||||
// initialize JSSE
|
||||
System.setProperty("javax.net.ssl.keyStore", KEYSTORE);
|
||||
System.setProperty("javax.net.ssl.keyStorePassword", PASSWORD);
|
||||
System.setProperty("javax.net.ssl.trustStore", KEYSTORE);
|
||||
System.setProperty("javax.net.ssl.trustStorePassword", PASSWORD);
|
||||
|
||||
Handler handler = new Handler();
|
||||
|
||||
try (Http2TestServer server = new Http2TestServer("localhost", true, 0)) {
|
||||
server.addHandler(handler, "/");
|
||||
server.start();
|
||||
|
||||
int port = server.getAddress().getPort();
|
||||
String uriString = "https://localhost:" + Integer.toString(port);
|
||||
|
||||
// run test cases
|
||||
boolean success = true;
|
||||
|
||||
SSLParameters parameters = null;
|
||||
success &= expectFailure(
|
||||
"---\nTest #1: SSL parameters is null, expect NPE",
|
||||
() -> connect(uriString, parameters),
|
||||
NullPointerException.class);
|
||||
|
||||
success &= expectSuccess(
|
||||
"---\nTest #2: default SSL parameters, "
|
||||
+ "expect successful connection",
|
||||
() -> connect(uriString, USE_DEFAULT_SSL_PARAMETERS));
|
||||
success &= checkProtocol(handler.getSSLSession(), expectedTLSVersion(null));
|
||||
|
||||
// set SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA cipher suite
|
||||
// which has less priority in default cipher suite list
|
||||
success &= expectSuccess(
|
||||
"---\nTest #3: SSL parameters with "
|
||||
+ "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA cipher suite, "
|
||||
+ "expect successful connection",
|
||||
() -> connect(uriString, new SSLParameters(
|
||||
new String[] { "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA" },
|
||||
new String[] { "TLSv1.2" })));
|
||||
success &= checkProtocol(handler.getSSLSession(), "TLSv1.2");
|
||||
success &= checkCipherSuite(handler.getSSLSession(),
|
||||
"SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA");
|
||||
|
||||
// set TLS_RSA_WITH_AES_128_CBC_SHA cipher suite
|
||||
// which has less priority in default cipher suite list
|
||||
// also set TLSv1.2 protocol
|
||||
success &= expectSuccess(
|
||||
"---\nTest #4: SSL parameters with "
|
||||
+ "TLS_RSA_WITH_AES_128_CBC_SHA cipher suite,"
|
||||
+ " expect successful connection",
|
||||
() -> connect(uriString, new SSLParameters(
|
||||
new String[] { "TLS_RSA_WITH_AES_128_CBC_SHA" },
|
||||
new String[] { "TLSv1.2" })));
|
||||
success &= checkProtocol(handler.getSSLSession(), "TLSv1.2");
|
||||
success &= checkCipherSuite(handler.getSSLSession(),
|
||||
"TLS_RSA_WITH_AES_128_CBC_SHA");
|
||||
|
||||
success &= expectSuccess(
|
||||
"---\nTest #5: empty SSL parameters, "
|
||||
+ "expect successful connection",
|
||||
() -> connect(uriString, new SSLParameters()));
|
||||
success &= checkProtocol(handler.getSSLSession(), expectedTLSVersion(null));
|
||||
|
||||
if (success) {
|
||||
System.out.println("Test passed");
|
||||
} else {
|
||||
throw new RuntimeException("At least one test case failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static interface Test {
|
||||
|
||||
public void run() throws Exception;
|
||||
}
|
||||
|
||||
private static class Handler implements Http2Handler {
|
||||
|
||||
private static final byte[] BODY = "Test response".getBytes();
|
||||
|
||||
private volatile SSLSession sslSession;
|
||||
|
||||
@Override
|
||||
public void handle(Http2TestExchange t) throws IOException {
|
||||
System.out.println("Handler: received request to "
|
||||
+ t.getRequestURI());
|
||||
|
||||
try (InputStream is = t.getRequestBody()) {
|
||||
byte[] body = is.readAllBytes();
|
||||
System.out.println("Handler: read " + body.length
|
||||
+ " bytes of body: ");
|
||||
System.out.println(new String(body));
|
||||
}
|
||||
|
||||
sslSession = t.getSSLSession();
|
||||
|
||||
try (OutputStream os = t.getResponseBody()) {
|
||||
t.sendResponseHeaders(200, BODY.length);
|
||||
os.write(BODY);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SSLSession getSSLSession() {
|
||||
return sslSession;
|
||||
}
|
||||
}
|
||||
|
||||
private static void connect(String uriString, SSLParameters sslParameters)
|
||||
throws URISyntaxException, IOException, InterruptedException
|
||||
{
|
||||
HttpClient.Builder builder = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_2);
|
||||
if (sslParameters != USE_DEFAULT_SSL_PARAMETERS)
|
||||
builder.sslParameters(sslParameters);
|
||||
HttpClient client = builder.build();
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder(new URI(uriString))
|
||||
.POST(BodyPublishers.ofString("body"))
|
||||
.build();
|
||||
String body = client.send(request, BodyHandlers.ofString()).body();
|
||||
|
||||
System.out.println("Response: " + body);
|
||||
}
|
||||
|
||||
private static boolean checkProtocol(SSLSession session, String protocol) {
|
||||
if (session == null) {
|
||||
System.out.println("Check protocol: no session provided");
|
||||
return false;
|
||||
}
|
||||
|
||||
System.out.println("Check protocol: negotiated protocol: "
|
||||
+ session.getProtocol());
|
||||
System.out.println("Check protocol: expected protocol: "
|
||||
+ protocol);
|
||||
if (!protocol.equals(session.getProtocol())) {
|
||||
System.out.println("Check protocol: unexpected negotiated protocol");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean checkCipherSuite(SSLSession session, String ciphersuite) {
|
||||
if (session == null) {
|
||||
System.out.println("Check protocol: no session provided");
|
||||
return false;
|
||||
}
|
||||
|
||||
System.out.println("Check protocol: negotiated ciphersuite: "
|
||||
+ session.getCipherSuite());
|
||||
System.out.println("Check protocol: expected ciphersuite: "
|
||||
+ ciphersuite);
|
||||
if (!ciphersuite.equals(session.getCipherSuite())) {
|
||||
System.out.println("Check protocol: unexpected negotiated ciphersuite");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean expectSuccess(String message, Test test) {
|
||||
System.out.println(message);
|
||||
try {
|
||||
test.run();
|
||||
System.out.println("Passed");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed: unexpected exception:");
|
||||
e.printStackTrace(System.out);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean expectFailure(String message, Test test,
|
||||
Class<? extends Throwable> expectedException) {
|
||||
|
||||
System.out.println(message);
|
||||
try {
|
||||
test.run();
|
||||
System.out.println("Failed: unexpected successful connection");
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
System.out.println("Got an exception:");
|
||||
e.printStackTrace(System.out);
|
||||
if (expectedException != null
|
||||
&& !expectedException.isAssignableFrom(e.getClass())) {
|
||||
System.out.printf("Failed: expected %s, but got %s%n",
|
||||
expectedException.getName(),
|
||||
e.getClass().getName());
|
||||
return false;
|
||||
}
|
||||
System.out.println("Passed: expected exception");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
156
test/jdk/java/net/httpclient/http2/Timeout.java
Normal file
156
test/jdk/java/net/httpclient/http2/Timeout.java
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2018, 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.
|
||||
*/
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.net.http.HttpTimeoutException;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import javax.net.ssl.SSLServerSocket;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLServerSocketFactory;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8156710
|
||||
* @summary Check if HttpTimeoutException is thrown if a server doesn't reply
|
||||
* @run main/othervm Timeout
|
||||
*/
|
||||
public class Timeout {
|
||||
|
||||
private static final int RANDOM_PORT = 0;
|
||||
private static final int TIMEOUT = 3 * 1000; // in millis
|
||||
private static final String KEYSTORE = System.getProperty("test.src")
|
||||
+ File.separator + "keystore.p12";
|
||||
private static final String PASSWORD = "password";
|
||||
|
||||
// indicates if server is ready to accept connections
|
||||
private static volatile boolean ready = false;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
test(false);
|
||||
test(true);
|
||||
}
|
||||
|
||||
public static void test(boolean async) throws Exception {
|
||||
System.setProperty("javax.net.ssl.keyStore", KEYSTORE);
|
||||
System.setProperty("javax.net.ssl.keyStorePassword", PASSWORD);
|
||||
System.setProperty("javax.net.ssl.trustStore", KEYSTORE);
|
||||
System.setProperty("javax.net.ssl.trustStorePassword", PASSWORD);
|
||||
|
||||
SSLServerSocketFactory factory =
|
||||
(SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
|
||||
|
||||
try (SSLServerSocket ssocket =
|
||||
(SSLServerSocket) factory.createServerSocket()) {
|
||||
ssocket.setReuseAddress(false);
|
||||
ssocket.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), RANDOM_PORT));
|
||||
|
||||
// start server
|
||||
Thread server = new Thread(() -> {
|
||||
while (true) {
|
||||
System.out.println("server: ready");
|
||||
SSLParameters params = ssocket.getSSLParameters();
|
||||
params.setApplicationProtocols(new String[]{"h2"});
|
||||
ssocket.setSSLParameters(params);
|
||||
ready = true;
|
||||
try (SSLSocket socket = (SSLSocket) ssocket.accept()) {
|
||||
|
||||
// just read forever
|
||||
System.out.println("server: accepted");
|
||||
while (true) {
|
||||
socket.getInputStream().read();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// ignore exceptions on server side
|
||||
System.out.println("server: exception: " + e);
|
||||
}
|
||||
}
|
||||
});
|
||||
server.setDaemon(true);
|
||||
server.start();
|
||||
|
||||
// wait for server is ready
|
||||
do {
|
||||
Thread.sleep(1000);
|
||||
} while (!ready);
|
||||
|
||||
String uri = "https://localhost:" + ssocket.getLocalPort();
|
||||
if (async) {
|
||||
System.out.println(uri + ": Trying to connect asynchronously");
|
||||
connectAsync(uri);
|
||||
} else {
|
||||
System.out.println(uri + ": Trying to connect synchronously");
|
||||
connect(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void connect(String server) throws Exception {
|
||||
try {
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_2)
|
||||
.build();
|
||||
HttpRequest request = HttpRequest.newBuilder(new URI(server))
|
||||
.timeout(Duration.ofMillis(TIMEOUT))
|
||||
.POST(BodyPublishers.ofString("body"))
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
|
||||
System.out.println("Received unexpected reply: " + response.statusCode());
|
||||
throw new RuntimeException("unexpected successful connection");
|
||||
} catch (HttpTimeoutException e) {
|
||||
System.out.println("expected exception: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void connectAsync(String server) throws Exception {
|
||||
try {
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_2)
|
||||
.build();
|
||||
HttpRequest request = HttpRequest.newBuilder(new URI(server))
|
||||
.timeout(Duration.ofMillis(TIMEOUT))
|
||||
.POST(BodyPublishers.ofString("body"))
|
||||
.build();
|
||||
HttpResponse<String> response = client.sendAsync(request, BodyHandlers.ofString()).join();
|
||||
System.out.println("Received unexpected reply: " + response.statusCode());
|
||||
throw new RuntimeException("unexpected successful connection");
|
||||
} catch (CompletionException e) {
|
||||
if (e.getCause() instanceof HttpTimeoutException) {
|
||||
System.out.println("expected exception: " + e.getCause());
|
||||
} else {
|
||||
throw new RuntimeException("Unexpected exception received: " + e.getCause(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
324
test/jdk/java/net/httpclient/http2/TrailingHeadersTest.java
Normal file
324
test/jdk/java/net/httpclient/http2/TrailingHeadersTest.java
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
/*
|
||||
* Copyright (c) 2023, 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
|
||||
* @summary Trailing headers should be ignored by the client when using HTTP/2
|
||||
* and not affect the rest of the exchange.
|
||||
* @bug 8296410
|
||||
* @library /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* @run junit/othervm -Djdk.httpclient.HttpClient.log=all TrailingHeadersTest
|
||||
*/
|
||||
|
||||
import jdk.httpclient.test.lib.http2.OutgoingPushPromise;
|
||||
import jdk.internal.net.http.common.HttpHeadersBuilder;
|
||||
import jdk.internal.net.http.frame.DataFrame;
|
||||
import jdk.internal.net.http.frame.HeaderFrame;
|
||||
import jdk.internal.net.http.frame.HeadersFrame;
|
||||
|
||||
import javax.net.ssl.SSLSession;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
import jdk.httpclient.test.lib.http2.BodyOutputStream;
|
||||
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class TrailingHeadersTest {
|
||||
|
||||
private static Http2TestServer http2TestServer;
|
||||
private static URI trailingURI, trailng1xxURI, trailingPushPromiseURI, warmupURI;
|
||||
static PrintStream testLog = System.err;
|
||||
|
||||
// Set up simple client-side push promise handler
|
||||
private static ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>> pushPromiseMap = new ConcurrentHashMap<>();
|
||||
|
||||
@BeforeEach
|
||||
public void beforeMethod() {
|
||||
pushPromiseMap = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws Exception {
|
||||
Properties props = new Properties();
|
||||
// For triggering trailing headers to send after Push Promise Response headers are sent
|
||||
props.setProperty("sendTrailingHeadersAfterPushPromise", "1");
|
||||
|
||||
http2TestServer = new Http2TestServer("Test_Server",
|
||||
false,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
props,
|
||||
null);
|
||||
http2TestServer.setExchangeSupplier(TrailingHeadersExchange::new);
|
||||
http2TestServer.addHandler(new ResponseTrailersHandler(), "/ResponseTrailingHeaders");
|
||||
http2TestServer.addHandler(new InformationalTrailersHandler(), "/InfoRespTrailingHeaders");
|
||||
http2TestServer.addHandler(new PushPromiseTrailersHandler(), "/PushPromiseTrailingHeaders");
|
||||
http2TestServer.addHandler(new WarmupHandler(), "/WarmupHandler");
|
||||
|
||||
http2TestServer.start();
|
||||
|
||||
trailingURI = URI.create("http://" + http2TestServer.serverAuthority() + "/ResponseTrailingHeaders");
|
||||
trailng1xxURI = URI.create("http://" + http2TestServer.serverAuthority() + "/InfoRespTrailingHeaders");
|
||||
trailingPushPromiseURI = URI.create("http://" + http2TestServer.serverAuthority() + "/PushPromiseTrailingHeaders");
|
||||
|
||||
// Used to ensure HTTP/2 upgrade takes place
|
||||
warmupURI = URI.create("http://" + http2TestServer.serverAuthority() + "/WarmupHandler");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void teardown() {
|
||||
http2TestServer.stop();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("uris")
|
||||
public void testTrailingHeaders(String description, HttpRequest hRequest, HttpResponse.PushPromiseHandler<String> pph) {
|
||||
testLog.println("testTrailingHeaders(): " + description);
|
||||
HttpClient httpClient = HttpClient.newBuilder().build();
|
||||
performWarmupRequest(httpClient);
|
||||
CompletableFuture<HttpResponse<String>> cf = httpClient.sendAsync(hRequest, BodyHandlers.ofString(UTF_8), pph);
|
||||
|
||||
testLog.println("testTrailingHeaders(): Performing request: " + hRequest);
|
||||
HttpResponse<String> resp = cf.join();
|
||||
|
||||
assertEquals(200, resp.statusCode(), "Status code of response should be 200");
|
||||
|
||||
// Verify Push Promise was successful if necessary
|
||||
if (pph != null)
|
||||
verifyPushPromise();
|
||||
|
||||
testLog.println("testTrailingHeaders(): Request successfully completed");
|
||||
}
|
||||
|
||||
private void verifyPushPromise() {
|
||||
assertEquals(1, pushPromiseMap.size(), "Push Promise should not be greater than 1");
|
||||
// This will only iterate once
|
||||
for (HttpRequest r : pushPromiseMap.keySet()) {
|
||||
CompletableFuture<HttpResponse<String>> serverPushResp = pushPromiseMap.get(r);
|
||||
// Get the push promise HttpResponse result if present
|
||||
HttpResponse<String> resp = serverPushResp.join();
|
||||
assertEquals("Sample_Push_Data", resp.body(), "Unexpected Push Promise response body");
|
||||
assertEquals(200, resp.statusCode(), "Status code of Push Promise response should be 200");
|
||||
}
|
||||
}
|
||||
|
||||
private void performWarmupRequest(HttpClient httpClient) {
|
||||
HttpRequest warmupReq = HttpRequest.newBuilder(warmupURI).version(HTTP_2)
|
||||
.GET()
|
||||
.build();
|
||||
httpClient.sendAsync(warmupReq, BodyHandlers.discarding()).join();
|
||||
}
|
||||
|
||||
public static Object[][] uris() {
|
||||
HttpResponse.PushPromiseHandler<String> pph = (initial, pushRequest, acceptor) -> {
|
||||
HttpResponse.BodyHandler<String> s = HttpResponse.BodyHandlers.ofString(UTF_8);
|
||||
TrailingHeadersTest.pushPromiseMap.put(pushRequest, acceptor.apply(s));
|
||||
};
|
||||
|
||||
HttpRequest httpGetTrailing = HttpRequest.newBuilder(trailingURI).version(HTTP_2)
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpRequest httpPost1xxTrailing = HttpRequest.newBuilder(trailng1xxURI).version(HTTP_2)
|
||||
.POST(HttpRequest.BodyPublishers.ofString("Test Post"))
|
||||
.expectContinue(true)
|
||||
.build();
|
||||
|
||||
HttpRequest httpGetPushPromiseTrailing = HttpRequest.newBuilder(trailingPushPromiseURI).version(HTTP_2)
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
return new Object[][] {
|
||||
{ "Test GET with Trailing Headers", httpGetTrailing, null },
|
||||
{ "Test POST with 1xx response & Trailing Headers", httpPost1xxTrailing, null },
|
||||
{ "Test Push Promise with Trailing Headers", httpGetPushPromiseTrailing, pph }
|
||||
};
|
||||
}
|
||||
|
||||
static class TrailingHeadersExchange extends Http2TestExchangeImpl {
|
||||
|
||||
byte[] resp = "Sample_Data".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
|
||||
TrailingHeadersExchange(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder,
|
||||
URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os,
|
||||
Http2TestServerConnection conn, boolean pushAllowed) {
|
||||
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
|
||||
}
|
||||
|
||||
public void sendResponseThenTrailers() throws IOException {
|
||||
/*
|
||||
HttpHeadersBuilder hb = this.conn.createNewHeadersBuilder();
|
||||
hb.setHeader("x-sample", "val");
|
||||
HeaderFrame headerFrame = new HeadersFrame(this.streamid, 0, this.conn.encodeHeaders(hb.build()));
|
||||
*/
|
||||
// TODO: see if there is a safe way to encode headers without interrupting connection thread
|
||||
HeaderFrame headerFrame = new HeadersFrame(this.streamid, 0, List.of());
|
||||
headerFrame.setFlag(HeaderFrame.END_HEADERS);
|
||||
headerFrame.setFlag(HeaderFrame.END_STREAM);
|
||||
|
||||
this.sendResponseHeaders(200, resp.length);
|
||||
DataFrame dataFrame = new DataFrame(this.streamid, 0, ByteBuffer.wrap(resp));
|
||||
this.conn.addToOutputQ(dataFrame);
|
||||
this.conn.addToOutputQ(headerFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) {
|
||||
HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder();
|
||||
headersBuilder.setHeader(":method", "GET");
|
||||
headersBuilder.setHeader(":scheme", uri.getScheme());
|
||||
headersBuilder.setHeader(":authority", uri.getAuthority());
|
||||
headersBuilder.setHeader(":path", uri.getPath());
|
||||
for (Map.Entry<String,List<String>> entry : reqHeaders.map().entrySet()) {
|
||||
for (String value : entry.getValue())
|
||||
headersBuilder.addHeader(entry.getKey(), value);
|
||||
}
|
||||
HttpHeaders combinedHeaders = headersBuilder.build();
|
||||
OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, rspHeaders, content);
|
||||
pp.setFlag(HeaderFrame.END_HEADERS);
|
||||
|
||||
try {
|
||||
this.conn.addToOutputQ(pp);
|
||||
} catch (IOException ex) {
|
||||
testLog.println("serverPush(): pushPromise exception: " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class WarmupHandler implements Http2Handler {
|
||||
|
||||
@Override
|
||||
public void handle(Http2TestExchange exchange) throws IOException {
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static class ResponseTrailersHandler implements Http2Handler {
|
||||
|
||||
@Override
|
||||
public void handle(Http2TestExchange exchange) throws IOException {
|
||||
if (exchange.getProtocol().equals("HTTP/2")) {
|
||||
if (exchange instanceof TrailingHeadersExchange trailingHeadersExchange) {
|
||||
trailingHeadersExchange.sendResponseThenTrailers();
|
||||
}
|
||||
} else {
|
||||
testLog.println("ResponseTrailersHandler: Incorrect protocol version");
|
||||
exchange.sendResponseHeaders(400, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class InformationalTrailersHandler implements Http2Handler {
|
||||
|
||||
@Override
|
||||
public void handle(Http2TestExchange exchange) throws IOException {
|
||||
if (exchange.getProtocol().equals("HTTP/2")) {
|
||||
if (exchange instanceof TrailingHeadersExchange trailingHeadersExchange) {
|
||||
testLog.println(this.getClass().getCanonicalName() + ": Sending status 100");
|
||||
trailingHeadersExchange.sendResponseHeaders(100, 0);
|
||||
|
||||
try (InputStream is = exchange.getRequestBody()) {
|
||||
is.readAllBytes();
|
||||
trailingHeadersExchange.sendResponseThenTrailers();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
testLog.println("InformationalTrailersHandler: Incorrect protocol version");
|
||||
exchange.sendResponseHeaders(400, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class PushPromiseTrailersHandler implements Http2Handler {
|
||||
|
||||
@Override
|
||||
public void handle(Http2TestExchange exchange) throws IOException {
|
||||
if (exchange.getProtocol().equals("HTTP/2")) {
|
||||
if (exchange instanceof TrailingHeadersExchange trailingHeadersExchange) {
|
||||
try (InputStream is = exchange.getRequestBody()) {
|
||||
is.readAllBytes();
|
||||
}
|
||||
|
||||
if (exchange.serverPushAllowed()) {
|
||||
pushPromise(trailingHeadersExchange);
|
||||
}
|
||||
|
||||
try (OutputStream os = trailingHeadersExchange.getResponseBody()) {
|
||||
byte[] bytes = "Sample_Data".getBytes(UTF_8);
|
||||
trailingHeadersExchange.sendResponseHeaders(200, bytes.length);
|
||||
os.write(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
|
||||
|
||||
private void pushPromise(Http2TestExchange exchange) throws IOException {
|
||||
URI requestURI = exchange.getRequestURI();
|
||||
URI uri = requestURI.resolve("/promise");
|
||||
InputStream is = new ByteArrayInputStream("Sample_Push_Data".getBytes(UTF_8));
|
||||
Map<String, List<String>> map = new HashMap<>();
|
||||
map.put("x-promise", List.of("promise-header"));
|
||||
HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
|
||||
exchange.serverPush(uri, headers, is);
|
||||
testLog.println("PushPromiseTrailersHandler: Push Promise complete");
|
||||
}
|
||||
}
|
||||
}
|
||||
126
test/jdk/java/net/httpclient/http2/UserInfoTest.java
Normal file
126
test/jdk/java/net/httpclient/http2/UserInfoTest.java
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright (c) 2022, 2025, 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.
|
||||
*/
|
||||
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
import jdk.test.lib.net.URIBuilder;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Version;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @bug 8292876
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
||||
* @build jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* jdk.test.lib.net.SimpleSSLContext
|
||||
* jdk.httpclient.test.lib.http2.Http2TestExchange
|
||||
* @compile ../ReferenceTracker.java
|
||||
* @run junit UserInfoTest
|
||||
*/
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
public class UserInfoTest {
|
||||
|
||||
static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
|
||||
Http2TestServer server;
|
||||
int port;
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
|
||||
@BeforeAll
|
||||
void before() throws Exception {
|
||||
server = createServer(sslContext);
|
||||
port = server.getAddress().getPort();
|
||||
server.start();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
void after() throws Exception {
|
||||
server.close();
|
||||
}
|
||||
|
||||
static class Http2TestHandler implements Http2Handler {
|
||||
@Override
|
||||
public void handle(Http2TestExchange e) throws IOException {
|
||||
String authorityHeader = e.getRequestHeaders().firstValue(":authority").orElse(null);
|
||||
if (authorityHeader == null || authorityHeader.contains("user@")) {
|
||||
e.sendResponseHeaders(500, -1);
|
||||
} else {
|
||||
e.sendResponseHeaders(200, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Http2TestServer createServer(SSLContext sslContext) throws Exception {
|
||||
Http2TestServer http2TestServer = new Http2TestServer("localhost", true, sslContext);
|
||||
Http2TestHandler handler = new Http2TestHandler();
|
||||
http2TestServer.addHandler(handler, "/");
|
||||
return http2TestServer;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthorityHeader() throws Exception {
|
||||
HttpClient client = HttpClient
|
||||
.newBuilder()
|
||||
.proxy(HttpClient.Builder.NO_PROXY)
|
||||
.sslContext(sslContext)
|
||||
.build();
|
||||
TRACKER.track(client);
|
||||
|
||||
URI uri = URIBuilder.newBuilder()
|
||||
.scheme("https")
|
||||
.userInfo("user")
|
||||
.loopback()
|
||||
.port(port)
|
||||
.build();
|
||||
|
||||
HttpRequest request = HttpRequest
|
||||
.newBuilder(uri)
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
assertEquals(200, response.statusCode(), "Test Failed : " + response.uri().getAuthority());
|
||||
|
||||
client = null;
|
||||
System.gc();
|
||||
var error = TRACKER.check(500);
|
||||
if (error != null) throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
/*
|
||||
* Copyright (c) 2014, 2025, 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.
|
||||
*/
|
||||
package jdk.internal.net.http.hpack;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static jdk.internal.net.http.hpack.BuffersTestingKit.*;
|
||||
import static jdk.internal.net.http.hpack.TestHelper.newRandom;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
//
|
||||
// Some of the tests below overlap in what they test. This allows to diagnose
|
||||
// bugs quicker and with less pain by simply ruling out common working bits.
|
||||
//
|
||||
public final class BinaryPrimitivesTest {
|
||||
|
||||
private final Random random = newRandom();
|
||||
|
||||
@Test
|
||||
public void integerRead1() {
|
||||
verifyRead(bytes(0b00011111, 0b10011010, 0b00001010), 1337, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integerRead2() {
|
||||
verifyRead(bytes(0b00001010), 10, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integerRead3() {
|
||||
verifyRead(bytes(0b00101010), 42, 8);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integerWrite1() {
|
||||
verifyWrite(bytes(0b00011111, 0b10011010, 0b00001010), 1337, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integerWrite2() {
|
||||
verifyWrite(bytes(0b00001010), 10, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integerWrite3() {
|
||||
verifyWrite(bytes(0b00101010), 42, 8);
|
||||
}
|
||||
|
||||
//
|
||||
// Since readInteger(x) is the inverse of writeInteger(x), thus:
|
||||
//
|
||||
// for all x: readInteger(writeInteger(x)) == x
|
||||
//
|
||||
@Test
|
||||
public void integerIdentity() throws IOException {
|
||||
final int MAX_VALUE = 1 << 22;
|
||||
int totalCases = 0;
|
||||
int maxFilling = 0;
|
||||
IntegerReader r = new IntegerReader();
|
||||
IntegerWriter w = new IntegerWriter();
|
||||
ByteBuffer buf = ByteBuffer.allocate(8);
|
||||
for (int N = 1; N < 9; N++) {
|
||||
for (int expected = 0; expected <= MAX_VALUE; expected++) {
|
||||
w.reset().configure(expected, N, 1).write(buf);
|
||||
buf.flip();
|
||||
totalCases++;
|
||||
maxFilling = Math.max(maxFilling, buf.remaining());
|
||||
r.reset().configure(N).read(buf);
|
||||
assertEquals(expected, r.get());
|
||||
buf.clear();
|
||||
}
|
||||
}
|
||||
// System.out.printf("totalCases: %,d, maxFilling: %,d, maxValue: %,d%n",
|
||||
// totalCases, maxFilling, MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integerReadChunked() {
|
||||
final int NUM_TESTS = 1024;
|
||||
IntegerReader r = new IntegerReader();
|
||||
ByteBuffer bb = ByteBuffer.allocate(8);
|
||||
IntegerWriter w = new IntegerWriter();
|
||||
for (int i = 0; i < NUM_TESTS; i++) {
|
||||
final int N = 1 + random.nextInt(8);
|
||||
final int expected = random.nextInt(Integer.MAX_VALUE) + 1;
|
||||
w.reset().configure(expected, N, random.nextInt()).write(bb);
|
||||
bb.flip();
|
||||
|
||||
forEachSplit(bb,
|
||||
(buffers) -> {
|
||||
Iterable<? extends ByteBuffer> buf = relocateBuffers(injectEmptyBuffers(buffers));
|
||||
r.configure(N);
|
||||
for (ByteBuffer b : buf) {
|
||||
try {
|
||||
r.read(b);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
assertEquals(expected, r.get());
|
||||
r.reset();
|
||||
});
|
||||
bb.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: use maxValue in the test
|
||||
|
||||
@Test
|
||||
// FIXME: tune values for better coverage
|
||||
public void integerWriteChunked() {
|
||||
ByteBuffer bb = ByteBuffer.allocate(6);
|
||||
IntegerWriter w = new IntegerWriter();
|
||||
IntegerReader r = new IntegerReader();
|
||||
for (int i = 0; i < 1024; i++) { // number of tests
|
||||
final int N = 1 + random.nextInt(8);
|
||||
final int payload = random.nextInt(255);
|
||||
final int expected = random.nextInt(Integer.MAX_VALUE) + 1;
|
||||
|
||||
forEachSplit(bb,
|
||||
(buffers) -> {
|
||||
List<ByteBuffer> buf = new ArrayList<>();
|
||||
relocateBuffers(injectEmptyBuffers(buffers)).forEach(buf::add);
|
||||
boolean written = false;
|
||||
w.configure(expected, N, payload); // TODO: test for payload it can be read after written
|
||||
for (ByteBuffer b : buf) {
|
||||
int pos = b.position();
|
||||
written = w.write(b);
|
||||
b.position(pos);
|
||||
}
|
||||
if (!written) {
|
||||
fail("please increase bb size");
|
||||
}
|
||||
try {
|
||||
r.configure(N).read(concat(buf));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
// TODO: check payload here
|
||||
assertEquals(expected, r.get());
|
||||
w.reset();
|
||||
r.reset();
|
||||
bb.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Since readString(x) is the inverse of writeString(x), thus:
|
||||
//
|
||||
// for all x: readString(writeString(x)) == x
|
||||
//
|
||||
@Test
|
||||
public void stringIdentity() throws IOException {
|
||||
final int MAX_STRING_LENGTH = 4096;
|
||||
ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6); // it takes 6 bytes to encode string length of Integer.MAX_VALUE
|
||||
CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH);
|
||||
StringReader reader = new StringReader();
|
||||
StringWriter writer = new StringWriter();
|
||||
for (int len = 0; len <= MAX_STRING_LENGTH; len++) {
|
||||
for (int i = 0; i < 64; i++) {
|
||||
// not so much "test in isolation", I know... we're testing .reset() as well
|
||||
bytes.clear();
|
||||
chars.clear();
|
||||
|
||||
byte[] b = new byte[len];
|
||||
random.nextBytes(b);
|
||||
|
||||
String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string
|
||||
|
||||
boolean written = writer
|
||||
.configure(CharBuffer.wrap(expected), 0, expected.length(), false)
|
||||
.write(bytes);
|
||||
|
||||
if (!written) {
|
||||
fail("please increase 'bytes' size");
|
||||
}
|
||||
bytes.flip();
|
||||
reader.read(bytes, chars);
|
||||
chars.flip();
|
||||
assertEquals(expected, chars.toString());
|
||||
reader.reset();
|
||||
writer.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void huffmanStringWriteChunked() {
|
||||
// fail();
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// public void huffmanStringReadChunked() {
|
||||
// fail();
|
||||
// }
|
||||
|
||||
@Test
|
||||
public void stringWriteChunked() {
|
||||
final int MAX_STRING_LENGTH = 8;
|
||||
final ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6);
|
||||
final CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH);
|
||||
final StringReader reader = new StringReader();
|
||||
final StringWriter writer = new StringWriter();
|
||||
for (int len = 0; len <= MAX_STRING_LENGTH; len++) {
|
||||
|
||||
byte[] b = new byte[len];
|
||||
random.nextBytes(b);
|
||||
|
||||
String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string
|
||||
|
||||
forEachSplit(bytes, (buffers) -> {
|
||||
writer.configure(expected, 0, expected.length(), false);
|
||||
boolean written = false;
|
||||
for (ByteBuffer buf : buffers) {
|
||||
int p0 = buf.position();
|
||||
written = writer.write(buf);
|
||||
buf.position(p0);
|
||||
}
|
||||
if (!written) {
|
||||
fail("please increase 'bytes' size");
|
||||
}
|
||||
try {
|
||||
reader.read(concat(buffers), chars);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
chars.flip();
|
||||
assertEquals(expected, chars.toString());
|
||||
reader.reset();
|
||||
writer.reset();
|
||||
chars.clear();
|
||||
bytes.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stringReadChunked() {
|
||||
final int MAX_STRING_LENGTH = 16;
|
||||
final ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6);
|
||||
final CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH);
|
||||
final StringReader reader = new StringReader();
|
||||
final StringWriter writer = new StringWriter();
|
||||
for (int len = 0; len <= MAX_STRING_LENGTH; len++) {
|
||||
|
||||
byte[] b = new byte[len];
|
||||
random.nextBytes(b);
|
||||
|
||||
String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string
|
||||
|
||||
boolean written = writer
|
||||
.configure(CharBuffer.wrap(expected), 0, expected.length(), false)
|
||||
.write(bytes);
|
||||
writer.reset();
|
||||
|
||||
if (!written) {
|
||||
fail("please increase 'bytes' size");
|
||||
}
|
||||
bytes.flip();
|
||||
|
||||
forEachSplit(bytes, (buffers) -> {
|
||||
for (ByteBuffer buf : buffers) {
|
||||
int p0 = buf.position();
|
||||
try {
|
||||
reader.read(buf, chars);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
buf.position(p0);
|
||||
}
|
||||
chars.flip();
|
||||
assertEquals(expected, chars.toString());
|
||||
reader.reset();
|
||||
chars.clear();
|
||||
});
|
||||
|
||||
bytes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void test_Huffman_String_Identity() {
|
||||
// StringWriter writer = new StringWriter();
|
||||
// StringReader reader = new StringReader();
|
||||
// // 256 * 8 gives 2048 bits in case of plain 8 bit coding
|
||||
// // 256 * 30 gives you 7680 bits or 960 bytes in case of almost
|
||||
// // improbable event of 256 30 bits symbols in a row
|
||||
// ByteBuffer binary = ByteBuffer.allocate(960);
|
||||
// CharBuffer text = CharBuffer.allocate(960 / 5); // 5 = minimum code length
|
||||
// for (int len = 0; len < 128; len++) {
|
||||
// for (int i = 0; i < 256; i++) {
|
||||
// // not so much "test in isolation", I know...
|
||||
// binary.clear();
|
||||
//
|
||||
// byte[] bytes = new byte[len];
|
||||
// random.nextBytes(bytes);
|
||||
//
|
||||
// String s = new String(bytes, StandardCharsets.ISO_8859_1);
|
||||
//
|
||||
// writer.write(CharBuffer.wrap(s), binary, true);
|
||||
// binary.flip();
|
||||
// reader.read(binary, text);
|
||||
// text.flip();
|
||||
// assertEquals(text.toString(), s);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// TODO: atomic failures: e.g. readonly/overflow
|
||||
|
||||
private static byte[] bytes(int... data) {
|
||||
byte[] bytes = new byte[data.length];
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
bytes[i] = (byte) data[i];
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static void verifyRead(byte[] data, int expected, int N) {
|
||||
ByteBuffer buf = ByteBuffer.wrap(data, 0, data.length);
|
||||
IntegerReader reader = new IntegerReader();
|
||||
try {
|
||||
reader.configure(N).read(buf);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
assertEquals(expected, reader.get());
|
||||
}
|
||||
|
||||
private void verifyWrite(byte[] expected, int data, int N) {
|
||||
IntegerWriter w = new IntegerWriter();
|
||||
ByteBuffer buf = ByteBuffer.allocate(2 * expected.length);
|
||||
w.configure(data, N, 1).write(buf);
|
||||
buf.flip();
|
||||
assertEquals(ByteBuffer.wrap(expected), buf);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* Copyright (c) 2015, 2018, 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.
|
||||
*/
|
||||
package jdk.internal.net.http.hpack;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.nio.ByteBuffer.allocate;
|
||||
|
||||
public final class BuffersTestingKit {
|
||||
|
||||
/**
|
||||
* Relocates a {@code [position, limit)} region of the given buffer to
|
||||
* corresponding region in a new buffer starting with provided {@code
|
||||
* newPosition}.
|
||||
*
|
||||
* <p> Might be useful to make sure ByteBuffer's users do not rely on any
|
||||
* absolute positions, but solely on what's reported by position(), limit().
|
||||
*
|
||||
* <p> The contents between the given buffer and the returned one are not
|
||||
* shared.
|
||||
*/
|
||||
public static ByteBuffer relocate(ByteBuffer buffer, int newPosition,
|
||||
int newCapacity) {
|
||||
int oldPosition = buffer.position();
|
||||
int oldLimit = buffer.limit();
|
||||
|
||||
if (newPosition + oldLimit - oldPosition > newCapacity) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
ByteBuffer result;
|
||||
if (buffer.isDirect()) {
|
||||
result = ByteBuffer.allocateDirect(newCapacity);
|
||||
} else {
|
||||
result = allocate(newCapacity);
|
||||
}
|
||||
|
||||
result.position(newPosition);
|
||||
result.put(buffer).limit(result.position()).position(newPosition);
|
||||
buffer.position(oldPosition);
|
||||
|
||||
if (buffer.isReadOnly()) {
|
||||
return result.asReadOnlyBuffer();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Iterable<? extends ByteBuffer> relocateBuffers(
|
||||
Iterable<? extends ByteBuffer> source) {
|
||||
return () ->
|
||||
new Iterator<ByteBuffer>() {
|
||||
|
||||
private final Iterator<? extends ByteBuffer> it = source.iterator();
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return it.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer next() {
|
||||
ByteBuffer buf = it.next();
|
||||
int remaining = buf.remaining();
|
||||
int newCapacity = remaining + random.nextInt(17);
|
||||
int newPosition = random.nextInt(newCapacity - remaining + 1);
|
||||
return relocate(buf, newPosition, newCapacity);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: not always of size 0 (it's fine for buffer to report !b.hasRemaining())
|
||||
public static Iterable<? extends ByteBuffer> injectEmptyBuffers(
|
||||
Iterable<? extends ByteBuffer> source) {
|
||||
return injectEmptyBuffers(source, () -> allocate(0));
|
||||
}
|
||||
|
||||
public static Iterable<? extends ByteBuffer> injectEmptyBuffers(
|
||||
Iterable<? extends ByteBuffer> source,
|
||||
Supplier<? extends ByteBuffer> emptyBufferFactory) {
|
||||
|
||||
return () ->
|
||||
new Iterator<ByteBuffer>() {
|
||||
|
||||
private final Iterator<? extends ByteBuffer> it = source.iterator();
|
||||
private ByteBuffer next = calculateNext();
|
||||
|
||||
private ByteBuffer calculateNext() {
|
||||
if (random.nextBoolean()) {
|
||||
return emptyBufferFactory.get();
|
||||
} else if (it.hasNext()) {
|
||||
return it.next();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return next != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer next() {
|
||||
if (!hasNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
ByteBuffer next = this.next;
|
||||
this.next = calculateNext();
|
||||
return next;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ByteBuffer concat(Iterable<? extends ByteBuffer> split) {
|
||||
return concat(split, ByteBuffer::allocate);
|
||||
}
|
||||
|
||||
public static ByteBuffer concat(Iterable<? extends ByteBuffer> split,
|
||||
Function<? super Integer, ? extends ByteBuffer> concatBufferFactory) {
|
||||
int size = 0;
|
||||
for (ByteBuffer bb : split) {
|
||||
size += bb.remaining();
|
||||
}
|
||||
|
||||
ByteBuffer result = concatBufferFactory.apply(size);
|
||||
for (ByteBuffer bb : split) {
|
||||
result.put(bb);
|
||||
}
|
||||
|
||||
result.flip();
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void forEachSplit(ByteBuffer bb,
|
||||
Consumer<? super Iterable<? extends ByteBuffer>> action) {
|
||||
forEachSplit(bb.remaining(),
|
||||
(lengths) -> {
|
||||
int end = bb.position();
|
||||
List<ByteBuffer> buffers = new LinkedList<>();
|
||||
for (int len : lengths) {
|
||||
ByteBuffer d = bb.duplicate();
|
||||
d.position(end);
|
||||
d.limit(end + len);
|
||||
end += len;
|
||||
buffers.add(d);
|
||||
}
|
||||
action.accept(buffers);
|
||||
});
|
||||
}
|
||||
|
||||
private static void forEachSplit(int n, Consumer<? super Iterable<? extends Integer>> action) {
|
||||
forEachSplit(n, new Stack<>(), action);
|
||||
}
|
||||
|
||||
private static void forEachSplit(int n, Stack<Integer> path,
|
||||
Consumer<? super Iterable<? extends Integer>> action) {
|
||||
if (n == 0) {
|
||||
action.accept(path);
|
||||
} else {
|
||||
for (int i = 1; i <= n; i++) {
|
||||
path.push(i);
|
||||
forEachSplit(n - i, path, action);
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final Random random = new Random();
|
||||
|
||||
private BuffersTestingKit() {
|
||||
throw new InternalError();
|
||||
}
|
||||
|
||||
// public static void main(String[] args) {
|
||||
//
|
||||
// List<ByteBuffer> buffers = Arrays.asList(
|
||||
// (ByteBuffer) allocate(3).position(1).limit(2),
|
||||
// allocate(0),
|
||||
// allocate(7));
|
||||
//
|
||||
// Iterable<? extends ByteBuffer> buf = relocateBuffers(injectEmptyBuffers(buffers));
|
||||
// List<ByteBuffer> result = new ArrayList<>();
|
||||
// buf.forEach(result::add);
|
||||
// System.out.println(result);
|
||||
// }
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2025, 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.
|
||||
*/
|
||||
package jdk.internal.net.http.hpack;
|
||||
|
||||
import jdk.internal.net.http.hpack.SimpleHeaderTable.CircularBuffer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Queue;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
|
||||
import static jdk.internal.net.http.common.Utils.pow2Size;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static jdk.internal.net.http.hpack.TestHelper.assertVoidThrows;
|
||||
import static jdk.internal.net.http.hpack.TestHelper.newRandom;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public final class CircularBufferTest {
|
||||
|
||||
private final Random random = newRandom();
|
||||
|
||||
@Test
|
||||
public void queue() {
|
||||
for (int capacity = 1; capacity <= 2048; capacity++) {
|
||||
queueOnce(capacity, 32);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resize() {
|
||||
for (int capacity = 1; capacity <= 4096; capacity++) {
|
||||
resizeOnce(capacity);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downSizeEmptyBuffer() {
|
||||
CircularBuffer<Integer> buffer = new CircularBuffer<>(16);
|
||||
buffer.resize(15);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void newCapacityLessThanCurrentSize1() {
|
||||
CircularBuffer<Integer> buffer = new CircularBuffer<>(0);
|
||||
buffer.resize(5);
|
||||
buffer.add(1);
|
||||
buffer.add(1);
|
||||
buffer.add(1);
|
||||
assertVoidThrows(IllegalStateException.class, () -> buffer.resize(2));
|
||||
assertVoidThrows(IllegalStateException.class, () -> buffer.resize(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void newCapacityLessThanCurrentSize2() {
|
||||
CircularBuffer<Integer> buffer = new CircularBuffer<>(5);
|
||||
buffer.add(1);
|
||||
buffer.add(1);
|
||||
buffer.add(1);
|
||||
assertVoidThrows(IllegalStateException.class, () -> buffer.resize(2));
|
||||
assertVoidThrows(IllegalStateException.class, () -> buffer.resize(1));
|
||||
}
|
||||
|
||||
private void resizeOnce(int capacity) {
|
||||
|
||||
capacity = pow2Size(capacity);
|
||||
|
||||
int nextNumberToPut = 0;
|
||||
|
||||
Queue<Integer> referenceQueue = new ArrayBlockingQueue<>(capacity);
|
||||
CircularBuffer<Integer> buffer = new CircularBuffer<>(capacity);
|
||||
|
||||
// Fill full, so the next add will wrap
|
||||
for (int i = 0; i < capacity; i++, nextNumberToPut++) {
|
||||
buffer.add(nextNumberToPut);
|
||||
referenceQueue.add(nextNumberToPut);
|
||||
}
|
||||
int gets = random.nextInt(capacity); // [0, capacity)
|
||||
for (int i = 0; i < gets; i++) {
|
||||
referenceQueue.poll();
|
||||
buffer.remove();
|
||||
}
|
||||
int puts = random.nextInt(gets + 1); // [0, gets]
|
||||
for (int i = 0; i < puts; i++, nextNumberToPut++) {
|
||||
buffer.add(nextNumberToPut);
|
||||
referenceQueue.add(nextNumberToPut);
|
||||
}
|
||||
|
||||
Integer[] expected = referenceQueue.toArray(new Integer[0]);
|
||||
buffer.resize(expected.length);
|
||||
|
||||
boolean equals = Arrays.equals(buffer.elements, 0, buffer.size,
|
||||
expected, 0, expected.length);
|
||||
assertTrue(equals);
|
||||
}
|
||||
|
||||
private void queueOnce(int capacity, int numWraps) {
|
||||
|
||||
capacity = pow2Size(capacity);
|
||||
|
||||
Queue<Integer> referenceQueue = new ArrayBlockingQueue<>(capacity);
|
||||
CircularBuffer<Integer> buffer = new CircularBuffer<>(capacity);
|
||||
|
||||
int nextNumberToPut = 0;
|
||||
int totalPuts = 0;
|
||||
int putsLimit = capacity * numWraps;
|
||||
int remainingCapacity = capacity;
|
||||
int size = 0;
|
||||
|
||||
while (totalPuts < putsLimit) {
|
||||
assert remainingCapacity + size == capacity;
|
||||
int puts = random.nextInt(remainingCapacity + 1); // [0, remainingCapacity]
|
||||
remainingCapacity -= puts;
|
||||
size += puts;
|
||||
for (int i = 0; i < puts; i++, nextNumberToPut++) {
|
||||
referenceQueue.add(nextNumberToPut);
|
||||
buffer.add(nextNumberToPut);
|
||||
}
|
||||
totalPuts += puts;
|
||||
int gets = random.nextInt(size + 1); // [0, size]
|
||||
size -= gets;
|
||||
remainingCapacity += gets;
|
||||
for (int i = 0; i < gets; i++) {
|
||||
Integer expected = referenceQueue.poll();
|
||||
Integer actual = buffer.remove();
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,726 @@
|
|||
/*
|
||||
* Copyright (c) 2015, 2025, 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.
|
||||
*/
|
||||
package jdk.internal.net.http.hpack;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static jdk.internal.net.http.hpack.TestHelper.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
//
|
||||
// Tests whose names start with "testX" are the ones captured from real HPACK
|
||||
// use cases
|
||||
//
|
||||
public final class DecoderTest {
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.2.1
|
||||
//
|
||||
@Test
|
||||
public void example1() {
|
||||
// @formatter:off
|
||||
test("400a 6375 7374 6f6d 2d6b 6579 0d63 7573\n" +
|
||||
"746f 6d2d 6865 6164 6572",
|
||||
|
||||
"[ 1] (s = 55) custom-key: custom-header\n" +
|
||||
" Table size: 55",
|
||||
|
||||
"custom-key: custom-header");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.2.2
|
||||
//
|
||||
@Test
|
||||
public void example2() {
|
||||
// @formatter:off
|
||||
test("040c 2f73 616d 706c 652f 7061 7468",
|
||||
"empty.",
|
||||
":path: /sample/path");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.2.3
|
||||
//
|
||||
@Test
|
||||
public void example3() {
|
||||
// @formatter:off
|
||||
test("1008 7061 7373 776f 7264 0673 6563 7265\n" +
|
||||
"74",
|
||||
"empty.",
|
||||
"password: secret");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.2.4
|
||||
//
|
||||
@Test
|
||||
public void example4() {
|
||||
// @formatter:off
|
||||
test("82",
|
||||
"empty.",
|
||||
":method: GET");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.3
|
||||
//
|
||||
@Test
|
||||
public void example5() {
|
||||
// @formatter:off
|
||||
Decoder d = new Decoder(256);
|
||||
|
||||
test(d, "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" +
|
||||
"2e63 6f6d",
|
||||
|
||||
"[ 1] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 57",
|
||||
|
||||
":method: GET\n" +
|
||||
":scheme: http\n" +
|
||||
":path: /\n" +
|
||||
":authority: www.example.com");
|
||||
|
||||
test(d, "8286 84be 5808 6e6f 2d63 6163 6865",
|
||||
|
||||
"[ 1] (s = 53) cache-control: no-cache\n" +
|
||||
"[ 2] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 110",
|
||||
|
||||
":method: GET\n" +
|
||||
":scheme: http\n" +
|
||||
":path: /\n" +
|
||||
":authority: www.example.com\n" +
|
||||
"cache-control: no-cache");
|
||||
|
||||
test(d, "8287 85bf 400a 6375 7374 6f6d 2d6b 6579\n" +
|
||||
"0c63 7573 746f 6d2d 7661 6c75 65",
|
||||
|
||||
"[ 1] (s = 54) custom-key: custom-value\n" +
|
||||
"[ 2] (s = 53) cache-control: no-cache\n" +
|
||||
"[ 3] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 164",
|
||||
|
||||
":method: GET\n" +
|
||||
":scheme: https\n" +
|
||||
":path: /index.html\n" +
|
||||
":authority: www.example.com\n" +
|
||||
"custom-key: custom-value");
|
||||
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void example5AllSplits() {
|
||||
// @formatter:off
|
||||
testAllSplits(
|
||||
"8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" +
|
||||
"2e63 6f6d",
|
||||
|
||||
"[ 1] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 57",
|
||||
|
||||
":method: GET\n" +
|
||||
":scheme: http\n" +
|
||||
":path: /\n" +
|
||||
":authority: www.example.com");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.4
|
||||
//
|
||||
@Test
|
||||
public void example6() {
|
||||
// @formatter:off
|
||||
Decoder d = new Decoder(256);
|
||||
|
||||
test(d, "8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4\n" +
|
||||
"ff",
|
||||
|
||||
"[ 1] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 57",
|
||||
|
||||
":method: GET\n" +
|
||||
":scheme: http\n" +
|
||||
":path: /\n" +
|
||||
":authority: www.example.com");
|
||||
|
||||
test(d, "8286 84be 5886 a8eb 1064 9cbf",
|
||||
|
||||
"[ 1] (s = 53) cache-control: no-cache\n" +
|
||||
"[ 2] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 110",
|
||||
|
||||
":method: GET\n" +
|
||||
":scheme: http\n" +
|
||||
":path: /\n" +
|
||||
":authority: www.example.com\n" +
|
||||
"cache-control: no-cache");
|
||||
|
||||
test(d, "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925\n" +
|
||||
"a849 e95b b8e8 b4bf",
|
||||
|
||||
"[ 1] (s = 54) custom-key: custom-value\n" +
|
||||
"[ 2] (s = 53) cache-control: no-cache\n" +
|
||||
"[ 3] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 164",
|
||||
|
||||
":method: GET\n" +
|
||||
":scheme: https\n" +
|
||||
":path: /index.html\n" +
|
||||
":authority: www.example.com\n" +
|
||||
"custom-key: custom-value");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.5
|
||||
//
|
||||
@Test
|
||||
public void example7() {
|
||||
// @formatter:off
|
||||
Decoder d = new Decoder(256);
|
||||
|
||||
test(d, "4803 3330 3258 0770 7269 7661 7465 611d\n" +
|
||||
"4d6f 6e2c 2032 3120 4f63 7420 3230 3133\n" +
|
||||
"2032 303a 3133 3a32 3120 474d 546e 1768\n" +
|
||||
"7474 7073 3a2f 2f77 7777 2e65 7861 6d70\n" +
|
||||
"6c65 2e63 6f6d",
|
||||
|
||||
"[ 1] (s = 63) location: https://www.example.com\n" +
|
||||
"[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
|
||||
"[ 3] (s = 52) cache-control: private\n" +
|
||||
"[ 4] (s = 42) :status: 302\n" +
|
||||
" Table size: 222",
|
||||
|
||||
":status: 302\n" +
|
||||
"cache-control: private\n" +
|
||||
"date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
|
||||
"location: https://www.example.com");
|
||||
|
||||
test(d, "4803 3330 37c1 c0bf",
|
||||
|
||||
"[ 1] (s = 42) :status: 307\n" +
|
||||
"[ 2] (s = 63) location: https://www.example.com\n" +
|
||||
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
|
||||
"[ 4] (s = 52) cache-control: private\n" +
|
||||
" Table size: 222",
|
||||
|
||||
":status: 307\n" +
|
||||
"cache-control: private\n" +
|
||||
"date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
|
||||
"location: https://www.example.com");
|
||||
|
||||
test(d, "88c1 611d 4d6f 6e2c 2032 3120 4f63 7420\n" +
|
||||
"3230 3133 2032 303a 3133 3a32 3220 474d\n" +
|
||||
"54c0 5a04 677a 6970 7738 666f 6f3d 4153\n" +
|
||||
"444a 4b48 514b 425a 584f 5157 454f 5049\n" +
|
||||
"5541 5851 5745 4f49 553b 206d 6178 2d61\n" +
|
||||
"6765 3d33 3630 303b 2076 6572 7369 6f6e\n" +
|
||||
"3d31",
|
||||
|
||||
"[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" +
|
||||
"[ 2] (s = 52) content-encoding: gzip\n" +
|
||||
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" +
|
||||
" Table size: 215",
|
||||
|
||||
":status: 200\n" +
|
||||
"cache-control: private\n" +
|
||||
"date: Mon, 21 Oct 2013 20:13:22 GMT\n" +
|
||||
"location: https://www.example.com\n" +
|
||||
"content-encoding: gzip\n" +
|
||||
"set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.6
|
||||
//
|
||||
@Test
|
||||
public void example8() {
|
||||
// @formatter:off
|
||||
Decoder d = new Decoder(256);
|
||||
|
||||
test(d, "4882 6402 5885 aec3 771a 4b61 96d0 7abe\n" +
|
||||
"9410 54d4 44a8 2005 9504 0b81 66e0 82a6\n" +
|
||||
"2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8\n" +
|
||||
"e9ae 82ae 43d3",
|
||||
|
||||
"[ 1] (s = 63) location: https://www.example.com\n" +
|
||||
"[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
|
||||
"[ 3] (s = 52) cache-control: private\n" +
|
||||
"[ 4] (s = 42) :status: 302\n" +
|
||||
" Table size: 222",
|
||||
|
||||
":status: 302\n" +
|
||||
"cache-control: private\n" +
|
||||
"date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
|
||||
"location: https://www.example.com");
|
||||
|
||||
test(d, "4883 640e ffc1 c0bf",
|
||||
|
||||
"[ 1] (s = 42) :status: 307\n" +
|
||||
"[ 2] (s = 63) location: https://www.example.com\n" +
|
||||
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
|
||||
"[ 4] (s = 52) cache-control: private\n" +
|
||||
" Table size: 222",
|
||||
|
||||
":status: 307\n" +
|
||||
"cache-control: private\n" +
|
||||
"date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
|
||||
"location: https://www.example.com");
|
||||
|
||||
test(d, "88c1 6196 d07a be94 1054 d444 a820 0595\n" +
|
||||
"040b 8166 e084 a62d 1bff c05a 839b d9ab\n" +
|
||||
"77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b\n" +
|
||||
"3960 d5af 2708 7f36 72c1 ab27 0fb5 291f\n" +
|
||||
"9587 3160 65c0 03ed 4ee5 b106 3d50 07",
|
||||
|
||||
"[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" +
|
||||
"[ 2] (s = 52) content-encoding: gzip\n" +
|
||||
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" +
|
||||
" Table size: 215",
|
||||
|
||||
":status: 200\n" +
|
||||
"cache-control: private\n" +
|
||||
"date: Mon, 21 Oct 2013 20:13:22 GMT\n" +
|
||||
"location: https://www.example.com\n" +
|
||||
"content-encoding: gzip\n" +
|
||||
"set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
// One of responses from Apache Server that helped to catch a bug
|
||||
public void testX() {
|
||||
Decoder d = new Decoder(4096);
|
||||
// @formatter:off
|
||||
test(d, "3fe1 1f88 6196 d07a be94 03ea 693f 7504\n" +
|
||||
"00b6 a05c b827 2e32 fa98 b46f 769e 86b1\n" +
|
||||
"9272 b025 da5c 2ea9 fd70 a8de 7fb5 3556\n" +
|
||||
"5ab7 6ece c057 02e2 2ad2 17bf 6c96 d07a\n" +
|
||||
"be94 0854 cb6d 4a08 0075 40bd 71b6 6e05\n" +
|
||||
"a531 68df 0f13 8efe 4522 cd32 21b6 5686\n" +
|
||||
"eb23 781f cf52 848f d24a 8f0f 0d02 3435\n" +
|
||||
"5f87 497c a589 d34d 1f",
|
||||
|
||||
"[ 1] (s = 53) content-type: text/html\n" +
|
||||
"[ 2] (s = 50) accept-ranges: bytes\n" +
|
||||
"[ 3] (s = 74) last-modified: Mon, 11 Jun 2007 18:53:14 GMT\n" +
|
||||
"[ 4] (s = 77) server: Apache/2.4.17 (Unix) OpenSSL/1.0.2e-dev\n" +
|
||||
"[ 5] (s = 65) date: Mon, 09 Nov 2015 16:26:39 GMT\n" +
|
||||
" Table size: 319",
|
||||
|
||||
":status: 200\n" +
|
||||
"date: Mon, 09 Nov 2015 16:26:39 GMT\n" +
|
||||
"server: Apache/2.4.17 (Unix) OpenSSL/1.0.2e-dev\n" +
|
||||
"last-modified: Mon, 11 Jun 2007 18:53:14 GMT\n" +
|
||||
"etag: \"2d-432a5e4a73a80\"\n" +
|
||||
"accept-ranges: bytes\n" +
|
||||
"content-length: 45\n" +
|
||||
"content-type: text/html");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testX1() {
|
||||
// Supplier of a decoder with a particular state
|
||||
Supplier<Decoder> s = () -> {
|
||||
Decoder d = new Decoder(4096);
|
||||
// @formatter:off
|
||||
test(d, "88 76 92 ca 54 a7 d7 f4 fa ec af ed 6d da 61 d7 bb 1e ad ff" +
|
||||
"df 61 97 c3 61 be 94 13 4a 65 b6 a5 04 00 b8 a0 5a b8 db 77" +
|
||||
"1b 71 4c 5a 37 ff 0f 0d 84 08 00 00 03",
|
||||
|
||||
"[ 1] (s = 65) date: Fri, 24 Jun 2016 14:55:56 GMT\n" +
|
||||
"[ 2] (s = 59) server: Jetty(9.3.z-SNAPSHOT)\n" +
|
||||
" Table size: 124",
|
||||
|
||||
":status: 200\n" +
|
||||
"server: Jetty(9.3.z-SNAPSHOT)\n" +
|
||||
"date: Fri, 24 Jun 2016 14:55:56 GMT\n" +
|
||||
"content-length: 100000"
|
||||
);
|
||||
// @formatter:on
|
||||
return d;
|
||||
};
|
||||
// For all splits of the following data fed to the supplied decoder we
|
||||
// must get what's expected
|
||||
// @formatter:off
|
||||
testAllSplits(s,
|
||||
"88 bf be 0f 0d 84 08 00 00 03",
|
||||
|
||||
"[ 1] (s = 65) date: Fri, 24 Jun 2016 14:55:56 GMT\n" +
|
||||
"[ 2] (s = 59) server: Jetty(9.3.z-SNAPSHOT)\n" +
|
||||
" Table size: 124",
|
||||
|
||||
":status: 200\n" +
|
||||
"server: Jetty(9.3.z-SNAPSHOT)\n" +
|
||||
"date: Fri, 24 Jun 2016 14:55:56 GMT\n" +
|
||||
"content-length: 100000");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// This test is missing in the spec
|
||||
//
|
||||
@Test
|
||||
public void sizeUpdate() throws IOException {
|
||||
Decoder d = new Decoder(4096);
|
||||
assertEquals(4096, d.getTable().maxSize());
|
||||
d.decode(ByteBuffer.wrap(new byte[]{0b00111110}), true, nopCallback()); // newSize = 30
|
||||
assertEquals(30, d.getTable().maxSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incorrectSizeUpdate() {
|
||||
ByteBuffer b = ByteBuffer.allocate(8);
|
||||
Encoder e = new Encoder(8192) {
|
||||
@Override
|
||||
protected int calculateCapacity(int maxCapacity) {
|
||||
return maxCapacity;
|
||||
}
|
||||
};
|
||||
e.header("a", "b");
|
||||
e.encode(b);
|
||||
b.flip();
|
||||
{
|
||||
Decoder d = new Decoder(4096);
|
||||
assertVoidThrows(IOException.class,
|
||||
() -> d.decode(b, true, (name, value) -> { }));
|
||||
}
|
||||
b.flip();
|
||||
{
|
||||
Decoder d = new Decoder(4096);
|
||||
assertVoidThrows(IOException.class,
|
||||
() -> d.decode(b, false, (name, value) -> { }));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void corruptedHeaderBlockInteger() {
|
||||
Decoder d = new Decoder(4096);
|
||||
ByteBuffer data = ByteBuffer.wrap(new byte[]{
|
||||
(byte) 0b11111111, // indexed
|
||||
(byte) 0b10011010 // 25 + ...
|
||||
});
|
||||
IOException e = assertVoidThrows(IOException.class,
|
||||
() -> d.decode(data, true, nopCallback()));
|
||||
assertExceptionMessageContains(e, "Unexpected end of header block");
|
||||
}
|
||||
|
||||
// 5.1. Integer Representation
|
||||
// ...
|
||||
// Integer encodings that exceed implementation limits -- in value or octet
|
||||
// length -- MUST be treated as decoding errors. Different limits can
|
||||
// be set for each of the different uses of integers, based on
|
||||
// implementation constraints.
|
||||
@Test
|
||||
public void headerBlockIntegerNoOverflow() {
|
||||
Decoder d = new Decoder(4096);
|
||||
ByteBuffer data = ByteBuffer.wrap(new byte[]{
|
||||
(byte) 0b11111111, // indexed + 127
|
||||
// Integer.MAX_VALUE - 127 (base 128, little-endian):
|
||||
(byte) 0b10000000,
|
||||
(byte) 0b11111111,
|
||||
(byte) 0b11111111,
|
||||
(byte) 0b11111111,
|
||||
(byte) 0b00000111
|
||||
});
|
||||
|
||||
IOException e = assertVoidThrows(IOException.class,
|
||||
() -> d.decode(data, true, nopCallback()));
|
||||
|
||||
assertExceptionMessageContains(e.getCause(), "index=2147483647");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerBlockIntegerOverflow() {
|
||||
Decoder d = new Decoder(4096);
|
||||
ByteBuffer data = ByteBuffer.wrap(new byte[]{
|
||||
(byte) 0b11111111, // indexed + 127
|
||||
// Integer.MAX_VALUE - 127 + 1 (base 128, little endian):
|
||||
(byte) 0b10000001,
|
||||
(byte) 0b11111111,
|
||||
(byte) 0b11111111,
|
||||
(byte) 0b11111111,
|
||||
(byte) 0b00000111
|
||||
});
|
||||
|
||||
IOException e = assertVoidThrows(IOException.class,
|
||||
() -> d.decode(data, true, nopCallback()));
|
||||
|
||||
assertExceptionMessageContains(e, "Integer overflow");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void corruptedHeaderBlockString1() {
|
||||
Decoder d = new Decoder(4096);
|
||||
ByteBuffer data = ByteBuffer.wrap(new byte[]{
|
||||
0b00001111, // literal, index=15
|
||||
0b00000000,
|
||||
0b00001000, // huffman=false, length=8
|
||||
0b00000000, // \
|
||||
0b00000000, // but only 3 octets available...
|
||||
0b00000000 // /
|
||||
});
|
||||
IOException e = assertVoidThrows(IOException.class,
|
||||
() -> d.decode(data, true, nopCallback()));
|
||||
assertExceptionMessageContains(e, "Unexpected end of header block");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void corruptedHeaderBlockString2() {
|
||||
Decoder d = new Decoder(4096);
|
||||
ByteBuffer data = ByteBuffer.wrap(new byte[]{
|
||||
0b00001111, // literal, index=15
|
||||
0b00000000,
|
||||
(byte) 0b10001000, // huffman=true, length=8
|
||||
0b00000000, // \
|
||||
0b00000000, // \
|
||||
0b00000000, // but only 5 octets available...
|
||||
0b00000000, // /
|
||||
0b00000000 // /
|
||||
});
|
||||
IOException e = assertVoidThrows(IOException.class,
|
||||
() -> d.decode(data, true, nopCallback()));
|
||||
assertExceptionMessageContains(e, "Unexpected end of header block");
|
||||
}
|
||||
|
||||
// 5.2. String Literal Representation
|
||||
// ...A Huffman-encoded string literal containing the EOS symbol MUST be
|
||||
// treated as a decoding error...
|
||||
@Test
|
||||
public void corruptedHeaderBlockHuffmanStringEOS() {
|
||||
Decoder d = new Decoder(4096);
|
||||
ByteBuffer data = ByteBuffer.wrap(new byte[]{
|
||||
0b00001111, // literal, index=15
|
||||
0b00000000,
|
||||
(byte) 0b10000110, // huffman=true, length=6
|
||||
0b00011001, 0b01001101, (byte) 0b11111111,
|
||||
(byte) 0b11111111, (byte) 0b11111111, (byte) 0b11111100
|
||||
});
|
||||
IOException e = assertVoidThrows(IOException.class,
|
||||
() -> d.decode(data, true, nopCallback()));
|
||||
|
||||
assertExceptionMessageContains(e, "Encountered EOS");
|
||||
}
|
||||
|
||||
// 5.2. String Literal Representation
|
||||
// ...A padding strictly longer than 7 bits MUST be treated as a decoding
|
||||
// error...
|
||||
@Test
|
||||
public void corruptedHeaderBlockHuffmanStringLongPadding1() {
|
||||
Decoder d = new Decoder(4096);
|
||||
ByteBuffer data = ByteBuffer.wrap(new byte[]{
|
||||
0b00001111, // literal, index=15
|
||||
0b00000000,
|
||||
(byte) 0b10000011, // huffman=true, length=3
|
||||
0b00011001, 0b01001101, (byte) 0b11111111
|
||||
// len("aei") + len(padding) = (5 + 5 + 5) + (9)
|
||||
});
|
||||
IOException e = assertVoidThrows(IOException.class,
|
||||
() -> d.decode(data, true, nopCallback()));
|
||||
|
||||
assertExceptionMessageContains(e, "Padding is too long", "len=9");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void corruptedHeaderBlockHuffmanStringLongPadding2() {
|
||||
Decoder d = new Decoder(4096);
|
||||
ByteBuffer data = ByteBuffer.wrap(new byte[]{
|
||||
0b00001111, // literal, index=15
|
||||
0b00000000,
|
||||
(byte) 0b10000011, // huffman=true, length=3
|
||||
0b00011001, 0b01111010, (byte) 0b11111111
|
||||
// len("aek") + len(padding) = (5 + 5 + 7) + (7)
|
||||
});
|
||||
assertVoidDoesNotThrow(() -> d.decode(data, true, nopCallback()));
|
||||
}
|
||||
|
||||
// 5.2. String Literal Representation
|
||||
// ...A padding not corresponding to the most significant bits of the code
|
||||
// for the EOS symbol MUST be treated as a decoding error...
|
||||
@Test
|
||||
public void corruptedHeaderBlockHuffmanStringNotEOSPadding() {
|
||||
Decoder d = new Decoder(4096);
|
||||
ByteBuffer data = ByteBuffer.wrap(new byte[]{
|
||||
0b00001111, // literal, index=15
|
||||
0b00000000,
|
||||
(byte) 0b10000011, // huffman=true, length=3
|
||||
0b00011001, 0b01111010, (byte) 0b11111110
|
||||
});
|
||||
IOException e = assertVoidThrows(IOException.class,
|
||||
() -> d.decode(data, true, nopCallback()));
|
||||
|
||||
assertExceptionMessageContains(e, "Not a EOS prefix");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void argsTestBiConsumerIsNull() {
|
||||
Decoder decoder = new Decoder(4096);
|
||||
assertVoidThrows(NullPointerException.class,
|
||||
() -> decoder.decode(ByteBuffer.allocate(16), true, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void argsTestByteBufferIsNull() {
|
||||
Decoder decoder = new Decoder(4096);
|
||||
assertVoidThrows(NullPointerException.class,
|
||||
() -> decoder.decode(null, true, nopCallback()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void argsTestBothAreNull() {
|
||||
Decoder decoder = new Decoder(4096);
|
||||
assertVoidThrows(NullPointerException.class,
|
||||
() -> decoder.decode(null, true, null));
|
||||
}
|
||||
|
||||
private static void test(String hexdump,
|
||||
String headerTable, String headerList) {
|
||||
test(new Decoder(4096), hexdump, headerTable, headerList);
|
||||
}
|
||||
|
||||
private static void testAllSplits(String hexdump,
|
||||
String expectedHeaderTable,
|
||||
String expectedHeaderList) {
|
||||
testAllSplits(() -> new Decoder(256), hexdump, expectedHeaderTable, expectedHeaderList);
|
||||
}
|
||||
|
||||
private static void testAllSplits(Supplier<Decoder> supplier,
|
||||
String hexdump,
|
||||
String expectedHeaderTable,
|
||||
String expectedHeaderList) {
|
||||
ByteBuffer source = SpecHelper.toBytes(hexdump);
|
||||
|
||||
BuffersTestingKit.forEachSplit(source, iterable -> {
|
||||
List<String> actual = new LinkedList<>();
|
||||
Iterator<? extends ByteBuffer> i = iterable.iterator();
|
||||
if (!i.hasNext()) {
|
||||
return;
|
||||
}
|
||||
Decoder d = supplier.get();
|
||||
do {
|
||||
ByteBuffer n = i.next();
|
||||
try {
|
||||
d.decode(n, !i.hasNext(), (name, value) -> {
|
||||
if (value == null) {
|
||||
actual.add(name.toString());
|
||||
} else {
|
||||
actual.add(name + ": " + value);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
} while (i.hasNext());
|
||||
assertEquals(expectedHeaderTable, d.getTable().getStateString());
|
||||
assertEquals(expectedHeaderList, actual.stream().collect(Collectors.joining("\n")));
|
||||
});
|
||||
|
||||
// Now introduce last ByteBuffer which is empty and EOF (mimics idiom
|
||||
// I've found in HttpClient code)
|
||||
BuffersTestingKit.forEachSplit(source, iterable -> {
|
||||
List<String> actual = new LinkedList<>();
|
||||
Iterator<? extends ByteBuffer> i = iterable.iterator();
|
||||
if (!i.hasNext()) {
|
||||
return;
|
||||
}
|
||||
Decoder d = supplier.get();
|
||||
do {
|
||||
ByteBuffer n = i.next();
|
||||
try {
|
||||
d.decode(n, false, (name, value) -> {
|
||||
if (value == null) {
|
||||
actual.add(name.toString());
|
||||
} else {
|
||||
actual.add(name + ": " + value);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
} while (i.hasNext());
|
||||
|
||||
try {
|
||||
d.decode(ByteBuffer.allocate(0), false, (name, value) -> {
|
||||
if (value == null) {
|
||||
actual.add(name.toString());
|
||||
} else {
|
||||
actual.add(name + ": " + value);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
|
||||
assertEquals(expectedHeaderTable, d.getTable().getStateString());
|
||||
assertEquals(expectedHeaderList, actual.stream().collect(Collectors.joining("\n")));
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Sometimes we need to keep the same decoder along several runs,
|
||||
// as it models the same connection
|
||||
//
|
||||
private static void test(Decoder d, String hexdump,
|
||||
String expectedHeaderTable, String expectedHeaderList) {
|
||||
|
||||
ByteBuffer source = SpecHelper.toBytes(hexdump);
|
||||
|
||||
List<String> actual = new LinkedList<>();
|
||||
try {
|
||||
d.decode(source, true, (name, value) -> {
|
||||
if (value == null) {
|
||||
actual.add(name.toString());
|
||||
} else {
|
||||
actual.add(name + ": " + value);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
|
||||
assertEquals(expectedHeaderTable, d.getTable().getStateString());
|
||||
assertEquals(expectedHeaderList, actual.stream().collect(Collectors.joining("\n")));
|
||||
}
|
||||
|
||||
private static DecodingCallback nopCallback() {
|
||||
return (t, u) -> { };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,693 @@
|
|||
/*
|
||||
* Copyright (c) 2014, 2025, 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.
|
||||
*/
|
||||
package jdk.internal.net.http.hpack;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.Buffer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static jdk.internal.net.http.hpack.BuffersTestingKit.concat;
|
||||
import static jdk.internal.net.http.hpack.BuffersTestingKit.forEachSplit;
|
||||
import static jdk.internal.net.http.hpack.SpecHelper.toHexdump;
|
||||
import static jdk.internal.net.http.hpack.TestHelper.assertVoidThrows;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
// TODO: map textual representation of commands from the spec to actual
|
||||
// calls to encoder (actually, this is a good idea for decoder as well)
|
||||
public final class EncoderTest {
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.2.1
|
||||
//
|
||||
@Test
|
||||
public void example1() {
|
||||
|
||||
Encoder e = newCustomEncoder(256);
|
||||
drainInitialUpdate(e);
|
||||
|
||||
e.literalWithIndexing("custom-key", false, "custom-header", false);
|
||||
// @formatter:off
|
||||
test(e,
|
||||
|
||||
"400a 6375 7374 6f6d 2d6b 6579 0d63 7573\n" +
|
||||
"746f 6d2d 6865 6164 6572",
|
||||
|
||||
"[ 1] (s = 55) custom-key: custom-header\n" +
|
||||
" Table size: 55");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.2.2
|
||||
//
|
||||
@Test
|
||||
public void example2() {
|
||||
|
||||
Encoder e = newCustomEncoder(256);
|
||||
drainInitialUpdate(e);
|
||||
|
||||
e.literal(4, "/sample/path", false);
|
||||
// @formatter:off
|
||||
test(e,
|
||||
|
||||
"040c 2f73 616d 706c 652f 7061 7468",
|
||||
|
||||
"empty.");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.2.3
|
||||
//
|
||||
@Test
|
||||
public void example3() {
|
||||
|
||||
Encoder e = newCustomEncoder(256);
|
||||
drainInitialUpdate(e);
|
||||
|
||||
e.literalNeverIndexed("password", false, "secret", false);
|
||||
// @formatter:off
|
||||
test(e,
|
||||
|
||||
"1008 7061 7373 776f 7264 0673 6563 7265\n" +
|
||||
"74",
|
||||
|
||||
"empty.");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.2.4
|
||||
//
|
||||
@Test
|
||||
public void example4() {
|
||||
|
||||
Encoder e = newCustomEncoder(256);
|
||||
drainInitialUpdate(e);
|
||||
|
||||
e.indexed(2);
|
||||
// @formatter:off
|
||||
test(e,
|
||||
|
||||
"82",
|
||||
|
||||
"empty.");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.3
|
||||
//
|
||||
@Test
|
||||
public void example5() {
|
||||
Encoder e = newCustomEncoder(256);
|
||||
drainInitialUpdate(e);
|
||||
|
||||
ByteBuffer output = ByteBuffer.allocate(64);
|
||||
e.indexed(2);
|
||||
e.encode(output);
|
||||
e.indexed(6);
|
||||
e.encode(output);
|
||||
e.indexed(4);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(1, "www.example.com", false);
|
||||
e.encode(output);
|
||||
|
||||
output.flip();
|
||||
|
||||
// @formatter:off
|
||||
test(e, output,
|
||||
|
||||
"8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" +
|
||||
"2e63 6f6d",
|
||||
|
||||
"[ 1] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 57");
|
||||
|
||||
output.clear();
|
||||
|
||||
e.indexed( 2);
|
||||
e.encode(output);
|
||||
e.indexed( 6);
|
||||
e.encode(output);
|
||||
e.indexed( 4);
|
||||
e.encode(output);
|
||||
e.indexed(62);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(24, "no-cache", false);
|
||||
e.encode(output);
|
||||
|
||||
output.flip();
|
||||
|
||||
test(e, output,
|
||||
|
||||
"8286 84be 5808 6e6f 2d63 6163 6865",
|
||||
|
||||
"[ 1] (s = 53) cache-control: no-cache\n" +
|
||||
"[ 2] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 110");
|
||||
|
||||
output.clear();
|
||||
|
||||
e.indexed( 2);
|
||||
e.encode(output);
|
||||
e.indexed( 7);
|
||||
e.encode(output);
|
||||
e.indexed( 5);
|
||||
e.encode(output);
|
||||
e.indexed(63);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing("custom-key", false, "custom-value", false);
|
||||
e.encode(output);
|
||||
|
||||
output.flip();
|
||||
|
||||
test(e, output,
|
||||
|
||||
"8287 85bf 400a 6375 7374 6f6d 2d6b 6579\n" +
|
||||
"0c63 7573 746f 6d2d 7661 6c75 65",
|
||||
|
||||
"[ 1] (s = 54) custom-key: custom-value\n" +
|
||||
"[ 2] (s = 53) cache-control: no-cache\n" +
|
||||
"[ 3] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 164");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void example5AllSplits() {
|
||||
|
||||
List<Consumer<Encoder>> actions = new LinkedList<>();
|
||||
actions.add(e -> e.indexed(2));
|
||||
actions.add(e -> e.indexed(6));
|
||||
actions.add(e -> e.indexed(4));
|
||||
actions.add(e -> e.literalWithIndexing(1, "www.example.com", false));
|
||||
|
||||
encodeAllSplits(
|
||||
actions,
|
||||
|
||||
"8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" +
|
||||
"2e63 6f6d",
|
||||
|
||||
"[ 1] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 57");
|
||||
}
|
||||
|
||||
private static void encodeAllSplits(Iterable<Consumer<Encoder>> consumers,
|
||||
String expectedHexdump,
|
||||
String expectedTableState) {
|
||||
ByteBuffer buffer = SpecHelper.toBytes(expectedHexdump);
|
||||
erase(buffer); // Zeroed buffer of size needed to hold the encoding
|
||||
forEachSplit(buffer, iterable -> {
|
||||
List<ByteBuffer> copy = new LinkedList<>();
|
||||
iterable.forEach(b -> copy.add(ByteBuffer.allocate(b.remaining())));
|
||||
Iterator<ByteBuffer> output = copy.iterator();
|
||||
if (!output.hasNext()) {
|
||||
throw new IllegalStateException("No buffers to encode to");
|
||||
}
|
||||
Encoder e = newCustomEncoder(256); // FIXME: pull up (as a parameter)
|
||||
drainInitialUpdate(e);
|
||||
boolean encoded;
|
||||
ByteBuffer b = output.next();
|
||||
for (Consumer<Encoder> c : consumers) {
|
||||
c.accept(e);
|
||||
do {
|
||||
encoded = e.encode(b);
|
||||
if (!encoded) {
|
||||
if (output.hasNext()) {
|
||||
b = output.next();
|
||||
} else {
|
||||
throw new IllegalStateException("No room for encoding");
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!encoded);
|
||||
}
|
||||
copy.forEach(Buffer::flip);
|
||||
ByteBuffer data = concat(copy);
|
||||
test(e, data, expectedHexdump, expectedTableState);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.4
|
||||
//
|
||||
@Test
|
||||
public void example6() {
|
||||
Encoder e = newCustomEncoder(256);
|
||||
drainInitialUpdate(e);
|
||||
|
||||
ByteBuffer output = ByteBuffer.allocate(64);
|
||||
e.indexed(2);
|
||||
e.encode(output);
|
||||
e.indexed(6);
|
||||
e.encode(output);
|
||||
e.indexed(4);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(1, "www.example.com", true);
|
||||
e.encode(output);
|
||||
|
||||
output.flip();
|
||||
|
||||
// @formatter:off
|
||||
test(e, output,
|
||||
|
||||
"8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4\n" +
|
||||
"ff",
|
||||
|
||||
"[ 1] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 57");
|
||||
|
||||
output.clear();
|
||||
|
||||
e.indexed( 2);
|
||||
e.encode(output);
|
||||
e.indexed( 6);
|
||||
e.encode(output);
|
||||
e.indexed( 4);
|
||||
e.encode(output);
|
||||
e.indexed(62);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(24, "no-cache", true);
|
||||
e.encode(output);
|
||||
|
||||
output.flip();
|
||||
|
||||
test(e, output,
|
||||
|
||||
"8286 84be 5886 a8eb 1064 9cbf",
|
||||
|
||||
"[ 1] (s = 53) cache-control: no-cache\n" +
|
||||
"[ 2] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 110");
|
||||
|
||||
output.clear();
|
||||
|
||||
e.indexed( 2);
|
||||
e.encode(output);
|
||||
e.indexed( 7);
|
||||
e.encode(output);
|
||||
e.indexed( 5);
|
||||
e.encode(output);
|
||||
e.indexed(63);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing("custom-key", true, "custom-value", true);
|
||||
e.encode(output);
|
||||
|
||||
output.flip();
|
||||
|
||||
test(e, output,
|
||||
|
||||
"8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925\n" +
|
||||
"a849 e95b b8e8 b4bf",
|
||||
|
||||
"[ 1] (s = 54) custom-key: custom-value\n" +
|
||||
"[ 2] (s = 53) cache-control: no-cache\n" +
|
||||
"[ 3] (s = 57) :authority: www.example.com\n" +
|
||||
" Table size: 164");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.5
|
||||
//
|
||||
@Test
|
||||
public void example7() {
|
||||
Encoder e = newCustomEncoder(256);
|
||||
drainInitialUpdate(e);
|
||||
|
||||
ByteBuffer output = ByteBuffer.allocate(128);
|
||||
// @formatter:off
|
||||
e.literalWithIndexing( 8, "302", false);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(24, "private", false);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:21 GMT", false);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(46, "https://www.example.com", false);
|
||||
e.encode(output);
|
||||
|
||||
output.flip();
|
||||
|
||||
test(e, output,
|
||||
|
||||
"4803 3330 3258 0770 7269 7661 7465 611d\n" +
|
||||
"4d6f 6e2c 2032 3120 4f63 7420 3230 3133\n" +
|
||||
"2032 303a 3133 3a32 3120 474d 546e 1768\n" +
|
||||
"7474 7073 3a2f 2f77 7777 2e65 7861 6d70\n" +
|
||||
"6c65 2e63 6f6d",
|
||||
|
||||
"[ 1] (s = 63) location: https://www.example.com\n" +
|
||||
"[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
|
||||
"[ 3] (s = 52) cache-control: private\n" +
|
||||
"[ 4] (s = 42) :status: 302\n" +
|
||||
" Table size: 222");
|
||||
|
||||
output.clear();
|
||||
|
||||
e.literalWithIndexing( 8, "307", false);
|
||||
e.encode(output);
|
||||
e.indexed(65);
|
||||
e.encode(output);
|
||||
e.indexed(64);
|
||||
e.encode(output);
|
||||
e.indexed(63);
|
||||
e.encode(output);
|
||||
|
||||
output.flip();
|
||||
|
||||
test(e, output,
|
||||
|
||||
"4803 3330 37c1 c0bf",
|
||||
|
||||
"[ 1] (s = 42) :status: 307\n" +
|
||||
"[ 2] (s = 63) location: https://www.example.com\n" +
|
||||
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
|
||||
"[ 4] (s = 52) cache-control: private\n" +
|
||||
" Table size: 222");
|
||||
|
||||
output.clear();
|
||||
|
||||
e.indexed( 8);
|
||||
e.encode(output);
|
||||
e.indexed(65);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:22 GMT", false);
|
||||
e.encode(output);
|
||||
e.indexed(64);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(26, "gzip", false);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(55, "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", false);
|
||||
e.encode(output);
|
||||
|
||||
output.flip();
|
||||
|
||||
test(e, output,
|
||||
|
||||
"88c1 611d 4d6f 6e2c 2032 3120 4f63 7420\n" +
|
||||
"3230 3133 2032 303a 3133 3a32 3220 474d\n" +
|
||||
"54c0 5a04 677a 6970 7738 666f 6f3d 4153\n" +
|
||||
"444a 4b48 514b 425a 584f 5157 454f 5049\n" +
|
||||
"5541 5851 5745 4f49 553b 206d 6178 2d61\n" +
|
||||
"6765 3d33 3630 303b 2076 6572 7369 6f6e\n" +
|
||||
"3d31",
|
||||
|
||||
"[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" +
|
||||
"[ 2] (s = 52) content-encoding: gzip\n" +
|
||||
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" +
|
||||
" Table size: 215");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.6
|
||||
//
|
||||
@Test
|
||||
public void example8() {
|
||||
Encoder e = newCustomEncoder(256);
|
||||
drainInitialUpdate(e);
|
||||
|
||||
ByteBuffer output = ByteBuffer.allocate(128);
|
||||
// @formatter:off
|
||||
e.literalWithIndexing( 8, "302", true);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(24, "private", true);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:21 GMT", true);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(46, "https://www.example.com", true);
|
||||
e.encode(output);
|
||||
|
||||
output.flip();
|
||||
|
||||
test(e, output,
|
||||
|
||||
"4882 6402 5885 aec3 771a 4b61 96d0 7abe\n" +
|
||||
"9410 54d4 44a8 2005 9504 0b81 66e0 82a6\n" +
|
||||
"2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8\n" +
|
||||
"e9ae 82ae 43d3",
|
||||
|
||||
"[ 1] (s = 63) location: https://www.example.com\n" +
|
||||
"[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
|
||||
"[ 3] (s = 52) cache-control: private\n" +
|
||||
"[ 4] (s = 42) :status: 302\n" +
|
||||
" Table size: 222");
|
||||
|
||||
output.clear();
|
||||
|
||||
e.literalWithIndexing( 8, "307", true);
|
||||
e.encode(output);
|
||||
e.indexed(65);
|
||||
e.encode(output);
|
||||
e.indexed(64);
|
||||
e.encode(output);
|
||||
e.indexed(63);
|
||||
e.encode(output);
|
||||
|
||||
output.flip();
|
||||
|
||||
test(e, output,
|
||||
|
||||
"4883 640e ffc1 c0bf",
|
||||
|
||||
"[ 1] (s = 42) :status: 307\n" +
|
||||
"[ 2] (s = 63) location: https://www.example.com\n" +
|
||||
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" +
|
||||
"[ 4] (s = 52) cache-control: private\n" +
|
||||
" Table size: 222");
|
||||
|
||||
output.clear();
|
||||
|
||||
e.indexed( 8);
|
||||
e.encode(output);
|
||||
e.indexed(65);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:22 GMT", true);
|
||||
e.encode(output);
|
||||
e.indexed(64);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(26, "gzip", true);
|
||||
e.encode(output);
|
||||
e.literalWithIndexing(55, "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", true);
|
||||
e.encode(output);
|
||||
|
||||
output.flip();
|
||||
|
||||
test(e, output,
|
||||
|
||||
"88c1 6196 d07a be94 1054 d444 a820 0595\n" +
|
||||
"040b 8166 e084 a62d 1bff c05a 839b d9ab\n" +
|
||||
"77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b\n" +
|
||||
"3960 d5af 2708 7f36 72c1 ab27 0fb5 291f\n" +
|
||||
"9587 3160 65c0 03ed 4ee5 b106 3d50 07",
|
||||
|
||||
"[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" +
|
||||
"[ 2] (s = 52) content-encoding: gzip\n" +
|
||||
"[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" +
|
||||
" Table size: 215");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initialSizeUpdateDefaultEncoder() throws IOException {
|
||||
Function<Integer, Encoder> e = Encoder::new;
|
||||
testSizeUpdate(e, 1024, asList(), asList(0));
|
||||
testSizeUpdate(e, 1024, asList(1024), asList(0));
|
||||
testSizeUpdate(e, 1024, asList(1024, 1024), asList(0));
|
||||
testSizeUpdate(e, 1024, asList(1024, 512), asList(0));
|
||||
testSizeUpdate(e, 1024, asList(512, 1024), asList(0));
|
||||
testSizeUpdate(e, 1024, asList(512, 2048), asList(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initialSizeUpdateCustomEncoder() throws IOException {
|
||||
Function<Integer, Encoder> e = EncoderTest::newCustomEncoder;
|
||||
testSizeUpdate(e, 1024, asList(), asList(1024));
|
||||
testSizeUpdate(e, 1024, asList(1024), asList(1024));
|
||||
testSizeUpdate(e, 1024, asList(1024, 1024), asList(1024));
|
||||
testSizeUpdate(e, 1024, asList(1024, 512), asList(512));
|
||||
testSizeUpdate(e, 1024, asList(512, 1024), asList(1024));
|
||||
testSizeUpdate(e, 1024, asList(512, 2048), asList(2048));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seriesOfSizeUpdatesDefaultEncoder() throws IOException {
|
||||
Function<Integer, Encoder> e = c -> {
|
||||
Encoder encoder = new Encoder(c);
|
||||
drainInitialUpdate(encoder);
|
||||
return encoder;
|
||||
};
|
||||
testSizeUpdate(e, 0, asList(0), asList());
|
||||
testSizeUpdate(e, 1024, asList(1024), asList());
|
||||
testSizeUpdate(e, 1024, asList(2048), asList());
|
||||
testSizeUpdate(e, 1024, asList(512), asList());
|
||||
testSizeUpdate(e, 1024, asList(1024, 1024), asList());
|
||||
testSizeUpdate(e, 1024, asList(1024, 2048), asList());
|
||||
testSizeUpdate(e, 1024, asList(2048, 1024), asList());
|
||||
testSizeUpdate(e, 1024, asList(1024, 512), asList());
|
||||
testSizeUpdate(e, 1024, asList(512, 1024), asList());
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#section-4.2
|
||||
//
|
||||
@Test
|
||||
public void seriesOfSizeUpdatesCustomEncoder() throws IOException {
|
||||
Function<Integer, Encoder> e = c -> {
|
||||
Encoder encoder = newCustomEncoder(c);
|
||||
drainInitialUpdate(encoder);
|
||||
return encoder;
|
||||
};
|
||||
testSizeUpdate(e, 0, asList(0), asList());
|
||||
testSizeUpdate(e, 1024, asList(1024), asList());
|
||||
testSizeUpdate(e, 1024, asList(2048), asList(2048));
|
||||
testSizeUpdate(e, 1024, asList(512), asList(512));
|
||||
testSizeUpdate(e, 1024, asList(1024, 1024), asList());
|
||||
testSizeUpdate(e, 1024, asList(1024, 2048), asList(2048));
|
||||
testSizeUpdate(e, 1024, asList(2048, 1024), asList());
|
||||
testSizeUpdate(e, 1024, asList(1024, 512), asList(512));
|
||||
testSizeUpdate(e, 1024, asList(512, 1024), asList(512, 1024));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void callSequenceViolations() {
|
||||
{ // Hasn't set up a header
|
||||
Encoder e = new Encoder(0);
|
||||
assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16)));
|
||||
}
|
||||
{ // Can't set up header while there's an unfinished encoding
|
||||
Encoder e = new Encoder(0);
|
||||
e.indexed(32);
|
||||
assertVoidThrows(IllegalStateException.class, () -> e.indexed(32));
|
||||
}
|
||||
{ // Can't setMaxCapacity while there's an unfinished encoding
|
||||
Encoder e = new Encoder(0);
|
||||
e.indexed(32);
|
||||
assertVoidThrows(IllegalStateException.class, () -> e.setMaxCapacity(512));
|
||||
}
|
||||
{ // Hasn't set up a header
|
||||
Encoder e = new Encoder(0);
|
||||
e.setMaxCapacity(256);
|
||||
assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16)));
|
||||
}
|
||||
{ // Hasn't set up a header after the previous encoding
|
||||
Encoder e = new Encoder(0);
|
||||
e.indexed(0);
|
||||
boolean encoded = e.encode(ByteBuffer.allocate(16));
|
||||
assertTrue(encoded); // assumption
|
||||
assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void test(Encoder encoder,
|
||||
String expectedTableState,
|
||||
String expectedHexdump) {
|
||||
|
||||
ByteBuffer b = ByteBuffer.allocate(128);
|
||||
encoder.encode(b);
|
||||
b.flip();
|
||||
test(encoder, b, expectedTableState, expectedHexdump);
|
||||
}
|
||||
|
||||
private static void test(Encoder encoder,
|
||||
ByteBuffer output,
|
||||
String expectedHexdump,
|
||||
String expectedTableState) {
|
||||
|
||||
String actualTableState = encoder.getHeaderTable().getStateString();
|
||||
assertEquals(expectedTableState, actualTableState);
|
||||
|
||||
String actualHexdump = toHexdump(output);
|
||||
assertEquals(expectedHexdump.replaceAll("\\n", " "), actualHexdump);
|
||||
}
|
||||
|
||||
// initial size - the size encoder is constructed with
|
||||
// updates - a sequence of values for consecutive calls to encoder.setMaxCapacity
|
||||
// expected - a sequence of values expected to be decoded by a decoder
|
||||
private void testSizeUpdate(Function<Integer, Encoder> encoder,
|
||||
int initialSize,
|
||||
List<Integer> updates,
|
||||
List<Integer> expected) throws IOException {
|
||||
Encoder e = encoder.apply(initialSize);
|
||||
updates.forEach(e::setMaxCapacity);
|
||||
ByteBuffer b = ByteBuffer.allocate(64);
|
||||
e.header("a", "b");
|
||||
e.encode(b);
|
||||
b.flip();
|
||||
Decoder d = new Decoder(updates.isEmpty() ? initialSize : Collections.max(updates));
|
||||
List<Integer> actual = new ArrayList<>();
|
||||
d.decode(b, true, new DecodingCallback() {
|
||||
@Override
|
||||
public void onDecoded(CharSequence name, CharSequence value) { }
|
||||
|
||||
@Override
|
||||
public void onSizeUpdate(int capacity) {
|
||||
actual.add(capacity);
|
||||
}
|
||||
});
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
//
|
||||
// Default encoder does not need any table, therefore a subclass that
|
||||
// behaves differently is needed
|
||||
//
|
||||
private static Encoder newCustomEncoder(int maxCapacity) {
|
||||
return new Encoder(maxCapacity) {
|
||||
@Override
|
||||
protected int calculateCapacity(int maxCapacity) {
|
||||
return maxCapacity;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void drainInitialUpdate(Encoder e) {
|
||||
ByteBuffer b = ByteBuffer.allocate(4);
|
||||
e.header("a", "b");
|
||||
boolean done;
|
||||
do {
|
||||
done = e.encode(b);
|
||||
b.flip();
|
||||
} while (!done);
|
||||
}
|
||||
|
||||
private static void erase(ByteBuffer buffer) {
|
||||
buffer.clear();
|
||||
while (buffer.hasRemaining()) {
|
||||
buffer.put((byte) 0);
|
||||
}
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright (c) 2014, 2025, 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.
|
||||
*/
|
||||
package jdk.internal.net.http.hpack;
|
||||
|
||||
import jdk.internal.net.http.hpack.SimpleHeaderTable.HeaderField;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class HeaderTableTest extends SimpleHeaderTableTest {
|
||||
|
||||
@Override
|
||||
protected HeaderTable createHeaderTable(int maxSize) {
|
||||
return new HeaderTable(maxSize, HPACK.getLogger());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staticData() {
|
||||
HeaderTable table = createHeaderTable(0);
|
||||
Map<Integer, HeaderField> staticHeaderFields = createStaticEntries();
|
||||
|
||||
Map<String, Set<Integer>> indexes = new HashMap<>();
|
||||
|
||||
for (Map.Entry<Integer, HeaderField> e : staticHeaderFields.entrySet()) {
|
||||
Integer idx = e.getKey();
|
||||
String name = e.getValue().name;
|
||||
indexes.merge(name, Set.of(idx), (v1, v2) -> {
|
||||
HashSet<Integer> s = new HashSet<>();
|
||||
s.addAll(v1);
|
||||
s.addAll(v2);
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
staticHeaderFields.forEach((key, expectedHeaderField) -> {
|
||||
// lookup
|
||||
HeaderField actualHeaderField = table.get(key);
|
||||
assertEquals(expectedHeaderField.name, actualHeaderField.name);
|
||||
assertEquals(expectedHeaderField.value, actualHeaderField.value);
|
||||
|
||||
// reverse lookup (name, value)
|
||||
String hName = expectedHeaderField.name;
|
||||
String hValue = expectedHeaderField.value;
|
||||
int expectedIndex = key;
|
||||
int actualIndex = table.indexOf(hName, hValue);
|
||||
|
||||
assertEquals(expectedIndex, actualIndex);
|
||||
|
||||
// reverse lookup (name)
|
||||
Set<Integer> expectedIndexes = indexes.get(hName);
|
||||
int actualMinimalIndex = table.indexOf(hName, "blah-blah");
|
||||
|
||||
assertTrue(expectedIndexes.contains(-actualMinimalIndex));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lowerIndexPriority() {
|
||||
HeaderTable table = createHeaderTable(256);
|
||||
int oldLength = table.length();
|
||||
table.put("bender", "rodriguez");
|
||||
table.put("bender", "rodriguez");
|
||||
table.put("bender", "rodriguez");
|
||||
|
||||
assertEquals(oldLength + 3, table.length()); // more like an assumption
|
||||
int i = table.indexOf("bender", "rodriguez");
|
||||
assertEquals(oldLength + 1, i);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void indexesAreNotLost2() {
|
||||
HeaderTable table = createHeaderTable(256);
|
||||
int oldLength = table.length();
|
||||
table.put("bender", "rodriguez");
|
||||
assertEquals(oldLength + 1, table.indexOf("bender", "rodriguez"));
|
||||
table.put("bender", "rodriguez");
|
||||
assertEquals(oldLength + 1, table.indexOf("bender", "rodriguez"));
|
||||
table.evictEntry();
|
||||
assertEquals(oldLength + 1, table.indexOf("bender", "rodriguez"));
|
||||
table.evictEntry();
|
||||
assertEquals(0, table.indexOf("bender", "rodriguez"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lowerIndexPriority2() {
|
||||
HeaderTable table = createHeaderTable(256);
|
||||
int oldLength = table.length();
|
||||
int idx = rnd.nextInt(oldLength) + 1;
|
||||
HeaderField f = table.get(idx);
|
||||
table.put(f.name, f.value);
|
||||
assertEquals(oldLength + 1, table.length());
|
||||
int i = table.indexOf(f.name, f.value);
|
||||
assertEquals(idx, i);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void indexOf() {
|
||||
// Let's put a series of header fields
|
||||
int NUM_HEADERS = 32;
|
||||
HeaderTable table =
|
||||
createHeaderTable((32 + 4) * NUM_HEADERS);
|
||||
// ^ ^
|
||||
// entry overhead symbols per entry (max 2x2 digits)
|
||||
for (int i = 1; i <= NUM_HEADERS; i++) {
|
||||
String s = String.valueOf(i);
|
||||
table.put(s, s);
|
||||
}
|
||||
// and verify indexOf (reverse lookup) returns correct indexes for
|
||||
// full lookup
|
||||
for (int j = 1; j <= NUM_HEADERS; j++) {
|
||||
String s = String.valueOf(j);
|
||||
int actualIndex = table.indexOf(s, s);
|
||||
int expectedIndex = STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1;
|
||||
assertEquals(expectedIndex, actualIndex);
|
||||
}
|
||||
// as well as for just a name lookup
|
||||
for (int j = 1; j <= NUM_HEADERS; j++) {
|
||||
String s = String.valueOf(j);
|
||||
int actualIndex = table.indexOf(s, "blah");
|
||||
int expectedIndex = -(STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1);
|
||||
assertEquals(expectedIndex, actualIndex);
|
||||
}
|
||||
// lookup for non-existent name returns 0
|
||||
assertEquals(0, table.indexOf("chupacabra", "1"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,839 @@
|
|||
/*
|
||||
* Copyright (c) 2015, 2025, 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.
|
||||
*/
|
||||
package jdk.internal.net.http.hpack;
|
||||
|
||||
import jdk.internal.net.http.hpack.Huffman.Reader;
|
||||
import jdk.internal.net.http.hpack.Huffman.Writer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static jdk.internal.net.http.hpack.HPACK.bytesForBits;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public final class HuffmanTest {
|
||||
|
||||
/*
|
||||
* Implementations of Huffman.Reader and Huffman.Writer under test.
|
||||
* Change them here.
|
||||
*/
|
||||
private static final Supplier<Reader> READER = QuickHuffman.Reader::new;
|
||||
private static final Supplier<Writer> WRITER = QuickHuffman.Writer::new;
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-B
|
||||
//
|
||||
private static final String SPECIFICATION =
|
||||
// @formatter:off
|
||||
" code as bits as hex len\n" +
|
||||
" sym aligned to MSB aligned in\n" +
|
||||
" to LSB bits\n" +
|
||||
" ( 0) |11111111|11000 1ff8 [13]\n" +
|
||||
" ( 1) |11111111|11111111|1011000 7fffd8 [23]\n" +
|
||||
" ( 2) |11111111|11111111|11111110|0010 fffffe2 [28]\n" +
|
||||
" ( 3) |11111111|11111111|11111110|0011 fffffe3 [28]\n" +
|
||||
" ( 4) |11111111|11111111|11111110|0100 fffffe4 [28]\n" +
|
||||
" ( 5) |11111111|11111111|11111110|0101 fffffe5 [28]\n" +
|
||||
" ( 6) |11111111|11111111|11111110|0110 fffffe6 [28]\n" +
|
||||
" ( 7) |11111111|11111111|11111110|0111 fffffe7 [28]\n" +
|
||||
" ( 8) |11111111|11111111|11111110|1000 fffffe8 [28]\n" +
|
||||
" ( 9) |11111111|11111111|11101010 ffffea [24]\n" +
|
||||
" ( 10) |11111111|11111111|11111111|111100 3ffffffc [30]\n" +
|
||||
" ( 11) |11111111|11111111|11111110|1001 fffffe9 [28]\n" +
|
||||
" ( 12) |11111111|11111111|11111110|1010 fffffea [28]\n" +
|
||||
" ( 13) |11111111|11111111|11111111|111101 3ffffffd [30]\n" +
|
||||
" ( 14) |11111111|11111111|11111110|1011 fffffeb [28]\n" +
|
||||
" ( 15) |11111111|11111111|11111110|1100 fffffec [28]\n" +
|
||||
" ( 16) |11111111|11111111|11111110|1101 fffffed [28]\n" +
|
||||
" ( 17) |11111111|11111111|11111110|1110 fffffee [28]\n" +
|
||||
" ( 18) |11111111|11111111|11111110|1111 fffffef [28]\n" +
|
||||
" ( 19) |11111111|11111111|11111111|0000 ffffff0 [28]\n" +
|
||||
" ( 20) |11111111|11111111|11111111|0001 ffffff1 [28]\n" +
|
||||
" ( 21) |11111111|11111111|11111111|0010 ffffff2 [28]\n" +
|
||||
" ( 22) |11111111|11111111|11111111|111110 3ffffffe [30]\n" +
|
||||
" ( 23) |11111111|11111111|11111111|0011 ffffff3 [28]\n" +
|
||||
" ( 24) |11111111|11111111|11111111|0100 ffffff4 [28]\n" +
|
||||
" ( 25) |11111111|11111111|11111111|0101 ffffff5 [28]\n" +
|
||||
" ( 26) |11111111|11111111|11111111|0110 ffffff6 [28]\n" +
|
||||
" ( 27) |11111111|11111111|11111111|0111 ffffff7 [28]\n" +
|
||||
" ( 28) |11111111|11111111|11111111|1000 ffffff8 [28]\n" +
|
||||
" ( 29) |11111111|11111111|11111111|1001 ffffff9 [28]\n" +
|
||||
" ( 30) |11111111|11111111|11111111|1010 ffffffa [28]\n" +
|
||||
" ( 31) |11111111|11111111|11111111|1011 ffffffb [28]\n" +
|
||||
" ' ' ( 32) |010100 14 [ 6]\n" +
|
||||
" '!' ( 33) |11111110|00 3f8 [10]\n" +
|
||||
" '\"' ( 34) |11111110|01 3f9 [10]\n" +
|
||||
" '#' ( 35) |11111111|1010 ffa [12]\n" +
|
||||
" '$' ( 36) |11111111|11001 1ff9 [13]\n" +
|
||||
" '%' ( 37) |010101 15 [ 6]\n" +
|
||||
" '&' ( 38) |11111000 f8 [ 8]\n" +
|
||||
" ''' ( 39) |11111111|010 7fa [11]\n" +
|
||||
" '(' ( 40) |11111110|10 3fa [10]\n" +
|
||||
" ')' ( 41) |11111110|11 3fb [10]\n" +
|
||||
" '*' ( 42) |11111001 f9 [ 8]\n" +
|
||||
" '+' ( 43) |11111111|011 7fb [11]\n" +
|
||||
" ',' ( 44) |11111010 fa [ 8]\n" +
|
||||
" '-' ( 45) |010110 16 [ 6]\n" +
|
||||
" '.' ( 46) |010111 17 [ 6]\n" +
|
||||
" '/' ( 47) |011000 18 [ 6]\n" +
|
||||
" '0' ( 48) |00000 0 [ 5]\n" +
|
||||
" '1' ( 49) |00001 1 [ 5]\n" +
|
||||
" '2' ( 50) |00010 2 [ 5]\n" +
|
||||
" '3' ( 51) |011001 19 [ 6]\n" +
|
||||
" '4' ( 52) |011010 1a [ 6]\n" +
|
||||
" '5' ( 53) |011011 1b [ 6]\n" +
|
||||
" '6' ( 54) |011100 1c [ 6]\n" +
|
||||
" '7' ( 55) |011101 1d [ 6]\n" +
|
||||
" '8' ( 56) |011110 1e [ 6]\n" +
|
||||
" '9' ( 57) |011111 1f [ 6]\n" +
|
||||
" ':' ( 58) |1011100 5c [ 7]\n" +
|
||||
" ';' ( 59) |11111011 fb [ 8]\n" +
|
||||
" '<' ( 60) |11111111|1111100 7ffc [15]\n" +
|
||||
" '=' ( 61) |100000 20 [ 6]\n" +
|
||||
" '>' ( 62) |11111111|1011 ffb [12]\n" +
|
||||
" '?' ( 63) |11111111|00 3fc [10]\n" +
|
||||
" '@' ( 64) |11111111|11010 1ffa [13]\n" +
|
||||
" 'A' ( 65) |100001 21 [ 6]\n" +
|
||||
" 'B' ( 66) |1011101 5d [ 7]\n" +
|
||||
" 'C' ( 67) |1011110 5e [ 7]\n" +
|
||||
" 'D' ( 68) |1011111 5f [ 7]\n" +
|
||||
" 'E' ( 69) |1100000 60 [ 7]\n" +
|
||||
" 'F' ( 70) |1100001 61 [ 7]\n" +
|
||||
" 'G' ( 71) |1100010 62 [ 7]\n" +
|
||||
" 'H' ( 72) |1100011 63 [ 7]\n" +
|
||||
" 'I' ( 73) |1100100 64 [ 7]\n" +
|
||||
" 'J' ( 74) |1100101 65 [ 7]\n" +
|
||||
" 'K' ( 75) |1100110 66 [ 7]\n" +
|
||||
" 'L' ( 76) |1100111 67 [ 7]\n" +
|
||||
" 'M' ( 77) |1101000 68 [ 7]\n" +
|
||||
" 'N' ( 78) |1101001 69 [ 7]\n" +
|
||||
" 'O' ( 79) |1101010 6a [ 7]\n" +
|
||||
" 'P' ( 80) |1101011 6b [ 7]\n" +
|
||||
" 'Q' ( 81) |1101100 6c [ 7]\n" +
|
||||
" 'R' ( 82) |1101101 6d [ 7]\n" +
|
||||
" 'S' ( 83) |1101110 6e [ 7]\n" +
|
||||
" 'T' ( 84) |1101111 6f [ 7]\n" +
|
||||
" 'U' ( 85) |1110000 70 [ 7]\n" +
|
||||
" 'V' ( 86) |1110001 71 [ 7]\n" +
|
||||
" 'W' ( 87) |1110010 72 [ 7]\n" +
|
||||
" 'X' ( 88) |11111100 fc [ 8]\n" +
|
||||
" 'Y' ( 89) |1110011 73 [ 7]\n" +
|
||||
" 'Z' ( 90) |11111101 fd [ 8]\n" +
|
||||
" '[' ( 91) |11111111|11011 1ffb [13]\n" +
|
||||
" '\\' ( 92) |11111111|11111110|000 7fff0 [19]\n" +
|
||||
" ']' ( 93) |11111111|11100 1ffc [13]\n" +
|
||||
" '^' ( 94) |11111111|111100 3ffc [14]\n" +
|
||||
" '_' ( 95) |100010 22 [ 6]\n" +
|
||||
" '`' ( 96) |11111111|1111101 7ffd [15]\n" +
|
||||
" 'a' ( 97) |00011 3 [ 5]\n" +
|
||||
" 'b' ( 98) |100011 23 [ 6]\n" +
|
||||
" 'c' ( 99) |00100 4 [ 5]\n" +
|
||||
" 'd' (100) |100100 24 [ 6]\n" +
|
||||
" 'e' (101) |00101 5 [ 5]\n" +
|
||||
" 'f' (102) |100101 25 [ 6]\n" +
|
||||
" 'g' (103) |100110 26 [ 6]\n" +
|
||||
" 'h' (104) |100111 27 [ 6]\n" +
|
||||
" 'i' (105) |00110 6 [ 5]\n" +
|
||||
" 'j' (106) |1110100 74 [ 7]\n" +
|
||||
" 'k' (107) |1110101 75 [ 7]\n" +
|
||||
" 'l' (108) |101000 28 [ 6]\n" +
|
||||
" 'm' (109) |101001 29 [ 6]\n" +
|
||||
" 'n' (110) |101010 2a [ 6]\n" +
|
||||
" 'o' (111) |00111 7 [ 5]\n" +
|
||||
" 'p' (112) |101011 2b [ 6]\n" +
|
||||
" 'q' (113) |1110110 76 [ 7]\n" +
|
||||
" 'r' (114) |101100 2c [ 6]\n" +
|
||||
" 's' (115) |01000 8 [ 5]\n" +
|
||||
" 't' (116) |01001 9 [ 5]\n" +
|
||||
" 'u' (117) |101101 2d [ 6]\n" +
|
||||
" 'v' (118) |1110111 77 [ 7]\n" +
|
||||
" 'w' (119) |1111000 78 [ 7]\n" +
|
||||
" 'x' (120) |1111001 79 [ 7]\n" +
|
||||
" 'y' (121) |1111010 7a [ 7]\n" +
|
||||
" 'z' (122) |1111011 7b [ 7]\n" +
|
||||
" '{' (123) |11111111|1111110 7ffe [15]\n" +
|
||||
" '|' (124) |11111111|100 7fc [11]\n" +
|
||||
" '}' (125) |11111111|111101 3ffd [14]\n" +
|
||||
" '~' (126) |11111111|11101 1ffd [13]\n" +
|
||||
" (127) |11111111|11111111|11111111|1100 ffffffc [28]\n" +
|
||||
" (128) |11111111|11111110|0110 fffe6 [20]\n" +
|
||||
" (129) |11111111|11111111|010010 3fffd2 [22]\n" +
|
||||
" (130) |11111111|11111110|0111 fffe7 [20]\n" +
|
||||
" (131) |11111111|11111110|1000 fffe8 [20]\n" +
|
||||
" (132) |11111111|11111111|010011 3fffd3 [22]\n" +
|
||||
" (133) |11111111|11111111|010100 3fffd4 [22]\n" +
|
||||
" (134) |11111111|11111111|010101 3fffd5 [22]\n" +
|
||||
" (135) |11111111|11111111|1011001 7fffd9 [23]\n" +
|
||||
" (136) |11111111|11111111|010110 3fffd6 [22]\n" +
|
||||
" (137) |11111111|11111111|1011010 7fffda [23]\n" +
|
||||
" (138) |11111111|11111111|1011011 7fffdb [23]\n" +
|
||||
" (139) |11111111|11111111|1011100 7fffdc [23]\n" +
|
||||
" (140) |11111111|11111111|1011101 7fffdd [23]\n" +
|
||||
" (141) |11111111|11111111|1011110 7fffde [23]\n" +
|
||||
" (142) |11111111|11111111|11101011 ffffeb [24]\n" +
|
||||
" (143) |11111111|11111111|1011111 7fffdf [23]\n" +
|
||||
" (144) |11111111|11111111|11101100 ffffec [24]\n" +
|
||||
" (145) |11111111|11111111|11101101 ffffed [24]\n" +
|
||||
" (146) |11111111|11111111|010111 3fffd7 [22]\n" +
|
||||
" (147) |11111111|11111111|1100000 7fffe0 [23]\n" +
|
||||
" (148) |11111111|11111111|11101110 ffffee [24]\n" +
|
||||
" (149) |11111111|11111111|1100001 7fffe1 [23]\n" +
|
||||
" (150) |11111111|11111111|1100010 7fffe2 [23]\n" +
|
||||
" (151) |11111111|11111111|1100011 7fffe3 [23]\n" +
|
||||
" (152) |11111111|11111111|1100100 7fffe4 [23]\n" +
|
||||
" (153) |11111111|11111110|11100 1fffdc [21]\n" +
|
||||
" (154) |11111111|11111111|011000 3fffd8 [22]\n" +
|
||||
" (155) |11111111|11111111|1100101 7fffe5 [23]\n" +
|
||||
" (156) |11111111|11111111|011001 3fffd9 [22]\n" +
|
||||
" (157) |11111111|11111111|1100110 7fffe6 [23]\n" +
|
||||
" (158) |11111111|11111111|1100111 7fffe7 [23]\n" +
|
||||
" (159) |11111111|11111111|11101111 ffffef [24]\n" +
|
||||
" (160) |11111111|11111111|011010 3fffda [22]\n" +
|
||||
" (161) |11111111|11111110|11101 1fffdd [21]\n" +
|
||||
" (162) |11111111|11111110|1001 fffe9 [20]\n" +
|
||||
" (163) |11111111|11111111|011011 3fffdb [22]\n" +
|
||||
" (164) |11111111|11111111|011100 3fffdc [22]\n" +
|
||||
" (165) |11111111|11111111|1101000 7fffe8 [23]\n" +
|
||||
" (166) |11111111|11111111|1101001 7fffe9 [23]\n" +
|
||||
" (167) |11111111|11111110|11110 1fffde [21]\n" +
|
||||
" (168) |11111111|11111111|1101010 7fffea [23]\n" +
|
||||
" (169) |11111111|11111111|011101 3fffdd [22]\n" +
|
||||
" (170) |11111111|11111111|011110 3fffde [22]\n" +
|
||||
" (171) |11111111|11111111|11110000 fffff0 [24]\n" +
|
||||
" (172) |11111111|11111110|11111 1fffdf [21]\n" +
|
||||
" (173) |11111111|11111111|011111 3fffdf [22]\n" +
|
||||
" (174) |11111111|11111111|1101011 7fffeb [23]\n" +
|
||||
" (175) |11111111|11111111|1101100 7fffec [23]\n" +
|
||||
" (176) |11111111|11111111|00000 1fffe0 [21]\n" +
|
||||
" (177) |11111111|11111111|00001 1fffe1 [21]\n" +
|
||||
" (178) |11111111|11111111|100000 3fffe0 [22]\n" +
|
||||
" (179) |11111111|11111111|00010 1fffe2 [21]\n" +
|
||||
" (180) |11111111|11111111|1101101 7fffed [23]\n" +
|
||||
" (181) |11111111|11111111|100001 3fffe1 [22]\n" +
|
||||
" (182) |11111111|11111111|1101110 7fffee [23]\n" +
|
||||
" (183) |11111111|11111111|1101111 7fffef [23]\n" +
|
||||
" (184) |11111111|11111110|1010 fffea [20]\n" +
|
||||
" (185) |11111111|11111111|100010 3fffe2 [22]\n" +
|
||||
" (186) |11111111|11111111|100011 3fffe3 [22]\n" +
|
||||
" (187) |11111111|11111111|100100 3fffe4 [22]\n" +
|
||||
" (188) |11111111|11111111|1110000 7ffff0 [23]\n" +
|
||||
" (189) |11111111|11111111|100101 3fffe5 [22]\n" +
|
||||
" (190) |11111111|11111111|100110 3fffe6 [22]\n" +
|
||||
" (191) |11111111|11111111|1110001 7ffff1 [23]\n" +
|
||||
" (192) |11111111|11111111|11111000|00 3ffffe0 [26]\n" +
|
||||
" (193) |11111111|11111111|11111000|01 3ffffe1 [26]\n" +
|
||||
" (194) |11111111|11111110|1011 fffeb [20]\n" +
|
||||
" (195) |11111111|11111110|001 7fff1 [19]\n" +
|
||||
" (196) |11111111|11111111|100111 3fffe7 [22]\n" +
|
||||
" (197) |11111111|11111111|1110010 7ffff2 [23]\n" +
|
||||
" (198) |11111111|11111111|101000 3fffe8 [22]\n" +
|
||||
" (199) |11111111|11111111|11110110|0 1ffffec [25]\n" +
|
||||
" (200) |11111111|11111111|11111000|10 3ffffe2 [26]\n" +
|
||||
" (201) |11111111|11111111|11111000|11 3ffffe3 [26]\n" +
|
||||
" (202) |11111111|11111111|11111001|00 3ffffe4 [26]\n" +
|
||||
" (203) |11111111|11111111|11111011|110 7ffffde [27]\n" +
|
||||
" (204) |11111111|11111111|11111011|111 7ffffdf [27]\n" +
|
||||
" (205) |11111111|11111111|11111001|01 3ffffe5 [26]\n" +
|
||||
" (206) |11111111|11111111|11110001 fffff1 [24]\n" +
|
||||
" (207) |11111111|11111111|11110110|1 1ffffed [25]\n" +
|
||||
" (208) |11111111|11111110|010 7fff2 [19]\n" +
|
||||
" (209) |11111111|11111111|00011 1fffe3 [21]\n" +
|
||||
" (210) |11111111|11111111|11111001|10 3ffffe6 [26]\n" +
|
||||
" (211) |11111111|11111111|11111100|000 7ffffe0 [27]\n" +
|
||||
" (212) |11111111|11111111|11111100|001 7ffffe1 [27]\n" +
|
||||
" (213) |11111111|11111111|11111001|11 3ffffe7 [26]\n" +
|
||||
" (214) |11111111|11111111|11111100|010 7ffffe2 [27]\n" +
|
||||
" (215) |11111111|11111111|11110010 fffff2 [24]\n" +
|
||||
" (216) |11111111|11111111|00100 1fffe4 [21]\n" +
|
||||
" (217) |11111111|11111111|00101 1fffe5 [21]\n" +
|
||||
" (218) |11111111|11111111|11111010|00 3ffffe8 [26]\n" +
|
||||
" (219) |11111111|11111111|11111010|01 3ffffe9 [26]\n" +
|
||||
" (220) |11111111|11111111|11111111|1101 ffffffd [28]\n" +
|
||||
" (221) |11111111|11111111|11111100|011 7ffffe3 [27]\n" +
|
||||
" (222) |11111111|11111111|11111100|100 7ffffe4 [27]\n" +
|
||||
" (223) |11111111|11111111|11111100|101 7ffffe5 [27]\n" +
|
||||
" (224) |11111111|11111110|1100 fffec [20]\n" +
|
||||
" (225) |11111111|11111111|11110011 fffff3 [24]\n" +
|
||||
" (226) |11111111|11111110|1101 fffed [20]\n" +
|
||||
" (227) |11111111|11111111|00110 1fffe6 [21]\n" +
|
||||
" (228) |11111111|11111111|101001 3fffe9 [22]\n" +
|
||||
" (229) |11111111|11111111|00111 1fffe7 [21]\n" +
|
||||
" (230) |11111111|11111111|01000 1fffe8 [21]\n" +
|
||||
" (231) |11111111|11111111|1110011 7ffff3 [23]\n" +
|
||||
" (232) |11111111|11111111|101010 3fffea [22]\n" +
|
||||
" (233) |11111111|11111111|101011 3fffeb [22]\n" +
|
||||
" (234) |11111111|11111111|11110111|0 1ffffee [25]\n" +
|
||||
" (235) |11111111|11111111|11110111|1 1ffffef [25]\n" +
|
||||
" (236) |11111111|11111111|11110100 fffff4 [24]\n" +
|
||||
" (237) |11111111|11111111|11110101 fffff5 [24]\n" +
|
||||
" (238) |11111111|11111111|11111010|10 3ffffea [26]\n" +
|
||||
" (239) |11111111|11111111|1110100 7ffff4 [23]\n" +
|
||||
" (240) |11111111|11111111|11111010|11 3ffffeb [26]\n" +
|
||||
" (241) |11111111|11111111|11111100|110 7ffffe6 [27]\n" +
|
||||
" (242) |11111111|11111111|11111011|00 3ffffec [26]\n" +
|
||||
" (243) |11111111|11111111|11111011|01 3ffffed [26]\n" +
|
||||
" (244) |11111111|11111111|11111100|111 7ffffe7 [27]\n" +
|
||||
" (245) |11111111|11111111|11111101|000 7ffffe8 [27]\n" +
|
||||
" (246) |11111111|11111111|11111101|001 7ffffe9 [27]\n" +
|
||||
" (247) |11111111|11111111|11111101|010 7ffffea [27]\n" +
|
||||
" (248) |11111111|11111111|11111101|011 7ffffeb [27]\n" +
|
||||
" (249) |11111111|11111111|11111111|1110 ffffffe [28]\n" +
|
||||
" (250) |11111111|11111111|11111101|100 7ffffec [27]\n" +
|
||||
" (251) |11111111|11111111|11111101|101 7ffffed [27]\n" +
|
||||
" (252) |11111111|11111111|11111101|110 7ffffee [27]\n" +
|
||||
" (253) |11111111|11111111|11111101|111 7ffffef [27]\n" +
|
||||
" (254) |11111111|11111111|11111110|000 7fffff0 [27]\n" +
|
||||
" (255) |11111111|11111111|11111011|10 3ffffee [26]\n" +
|
||||
" EOS (256) |11111111|11111111|11111111|111111 3fffffff [30]";
|
||||
// @formatter:on
|
||||
|
||||
private static final Code EOS = new Code((char) 256, 0x3fffffff, 30);
|
||||
private final SortedMap<Character, Code> CODES = readSpecification();
|
||||
|
||||
private static final class Code {
|
||||
|
||||
final char sym;
|
||||
final int hex;
|
||||
final int len;
|
||||
|
||||
public Code(char sym, int hex, int len) {
|
||||
this.sym = sym;
|
||||
this.hex = hex;
|
||||
this.len = len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Code code = (Code) o;
|
||||
return sym == code.sym &&
|
||||
hex == code.hex &&
|
||||
len == code.len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(sym, hex, len);
|
||||
}
|
||||
}
|
||||
|
||||
private SortedMap<Character, Code> readSpecification() {
|
||||
Pattern line = Pattern.compile(
|
||||
"\\(\\s*(?<sym>\\d+)\\s*\\)\\s*(?<bits>(\\|([01])+)+)\\s*" +
|
||||
"(?<hex>[0-9a-zA-Z]+)\\s*\\[\\s*(?<len>\\d+)\\s*\\]");
|
||||
Matcher m = line.matcher(SPECIFICATION);
|
||||
SortedMap<Character, Code> map = new TreeMap<>();
|
||||
while (m.find()) {
|
||||
String symString = m.group("sym");
|
||||
String binaryString = m.group("bits").replaceAll("\\|", "");
|
||||
String hexString = m.group("hex");
|
||||
String lenString = m.group("len");
|
||||
// several sanity checks for the data read from the table, just to
|
||||
// make sure what we read makes sense:
|
||||
int sym = Integer.parseInt(symString);
|
||||
if (sym < 0 || sym > 65535) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
int binary = Integer.parseInt(binaryString, 2);
|
||||
int len = Integer.parseInt(lenString);
|
||||
if (binaryString.length() != len) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
int hex = Integer.parseInt(hexString, 16);
|
||||
if (hex != binary) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
if (map.put((char) sym, new Code((char) sym, hex, len)) != null) {
|
||||
// a mapping for sym already exists
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
if (map.size() != 257) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/*
|
||||
* Encodes manually each symbol (character) from the specification and
|
||||
* checks that Huffman.Reader decodes the result back to the initial
|
||||
* character. This verifies that Huffman.Reader is implemented according to
|
||||
* RFC 7541.
|
||||
*/
|
||||
@Test
|
||||
public void decodingConsistentWithSpecification() throws IOException {
|
||||
Reader reader = READER.get();
|
||||
for (Code code : CODES.values()) {
|
||||
if (code.equals(EOS)) {
|
||||
continue; // skip EOS
|
||||
}
|
||||
ByteBuffer input = encode(code);
|
||||
StringBuilder output = new StringBuilder(1);
|
||||
reader.read(input, output, true);
|
||||
reader.reset();
|
||||
|
||||
// compare chars using their decimal representation (as some chars
|
||||
// might not be printable/visible)
|
||||
int expected = code.sym;
|
||||
int actual = (int) output.charAt(0);
|
||||
assertEquals(1, output.length()); // exactly 1 character
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeEOS1() {
|
||||
Reader reader = READER.get();
|
||||
TestHelper.assertVoidThrows(
|
||||
IOException.class,
|
||||
() -> reader.read(encode(EOS), new StringBuilder(), true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeEOS2() {
|
||||
Reader reader = READER.get();
|
||||
TestHelper.assertVoidThrows(
|
||||
IOException.class,
|
||||
() -> reader.read(encode(EOS), new StringBuilder(), false));
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.4.1
|
||||
//
|
||||
@Test
|
||||
public void read01() {
|
||||
readExhaustively("f1e3 c2e5 f23a 6ba0 ab90 f4ff", "www.example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void write01() {
|
||||
writeExhaustively("www.example.com", "f1e3 c2e5 f23a 6ba0 ab90 f4ff");
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.4.2
|
||||
//
|
||||
@Test
|
||||
public void read02() {
|
||||
readExhaustively("a8eb 1064 9cbf", "no-cache");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void write02() {
|
||||
writeExhaustively("no-cache", "a8eb 1064 9cbf");
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.4.3
|
||||
//
|
||||
@Test
|
||||
public void read03() {
|
||||
readExhaustively("25a8 49e9 5ba9 7d7f", "custom-key");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void write03() {
|
||||
writeExhaustively("custom-key", "25a8 49e9 5ba9 7d7f");
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.4.3
|
||||
//
|
||||
@Test
|
||||
public void read04() {
|
||||
readExhaustively("25a8 49e9 5bb8 e8b4 bf", "custom-value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void write04() {
|
||||
writeExhaustively("custom-value", "25a8 49e9 5bb8 e8b4 bf");
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.6.1
|
||||
//
|
||||
@Test
|
||||
public void read05() {
|
||||
readExhaustively("6402", "302");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void write05() {
|
||||
writeExhaustively("302", "6402");
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.6.1
|
||||
//
|
||||
@Test
|
||||
public void read06() {
|
||||
readExhaustively("aec3 771a 4b", "private");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void write06() {
|
||||
writeExhaustively("private", "aec3 771a 4b");
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.6.1
|
||||
//
|
||||
@Test
|
||||
public void read07() {
|
||||
readExhaustively(
|
||||
"d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff",
|
||||
"Mon, 21 Oct 2013 20:13:21 GMT");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void write07() {
|
||||
writeExhaustively(
|
||||
"Mon, 21 Oct 2013 20:13:21 GMT",
|
||||
"d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff");
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.6.1
|
||||
//
|
||||
@Test
|
||||
public void read08() {
|
||||
readExhaustively("9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3",
|
||||
"https://www.example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void write08() {
|
||||
writeExhaustively("https://www.example.com",
|
||||
"9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3");
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.6.2
|
||||
//
|
||||
@Test
|
||||
public void read09() {
|
||||
readExhaustively("640e ff", "307");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void write09() {
|
||||
writeExhaustively("307", "640e ff");
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.6.3
|
||||
//
|
||||
@Test
|
||||
public void read10() {
|
||||
readExhaustively(
|
||||
"d07a be94 1054 d444 a820 0595 040b 8166 e084 a62d 1bff",
|
||||
"Mon, 21 Oct 2013 20:13:22 GMT");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void write10() {
|
||||
writeExhaustively(
|
||||
"Mon, 21 Oct 2013 20:13:22 GMT",
|
||||
"d07a be94 1054 d444 a820 0595 040b 8166 e084 a62d 1bff");
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.6.3
|
||||
//
|
||||
@Test
|
||||
public void read11() {
|
||||
readExhaustively("9bd9 ab", "gzip");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void write11() {
|
||||
writeExhaustively("gzip", "9bd9 ab");
|
||||
}
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-C.6.3
|
||||
//
|
||||
@Test
|
||||
public void read12() {
|
||||
// The number of possibilities here grow as 2^(n-1). There are 45 bytes
|
||||
// in this input. So it would require 2^44 decoding operations. If we
|
||||
// spend 1 microsecond per operation, it would take approximately
|
||||
//
|
||||
// ((10^15 * 10^(-6)) / 86400) / 365, or about 32 years
|
||||
//
|
||||
// Conclusion: too big to be read exhaustively
|
||||
read("94e7 821d d7f2 e6c7 b335 dfdf cd5b 3960 "
|
||||
+ "d5af 2708 7f36 72c1 ab27 0fb5 291f 9587 "
|
||||
+ "3160 65c0 03ed 4ee5 b106 3d50 07",
|
||||
"foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void write12() {
|
||||
write("foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1",
|
||||
"94e7 821d d7f2 e6c7 b335 dfdf cd5b 3960 "
|
||||
+ "d5af 2708 7f36 72c1 ab27 0fb5 291f 9587 "
|
||||
+ "3160 65c0 03ed 4ee5 b106 3d50 07");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void read13() {
|
||||
readExhaustively("6274 a6b4 0989 4de4 b27f 80",
|
||||
"/https2/fixed?0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTrip() throws IOException {
|
||||
|
||||
class Helper {
|
||||
// Maps code's length to a character that is encoded with a code of
|
||||
// that length. Which of the characters with the same code's length
|
||||
// is picked is undefined.
|
||||
private Map<Integer, Character> chars = new HashMap<>();
|
||||
{
|
||||
for (Map.Entry<Character, Code> e : CODES.entrySet()) {
|
||||
chars.putIfAbsent(e.getValue().len, e.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
private CharSequence charsOfLength(int... lengths) {
|
||||
StringBuilder b = new StringBuilder(lengths.length);
|
||||
for (int length : lengths) {
|
||||
Character c = chars.get(length);
|
||||
if (c == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"No code has length " + length);
|
||||
}
|
||||
b.append(c);
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
private void identity(CharSequence str) throws IOException {
|
||||
Writer w = WRITER.get();
|
||||
StringBuilder b = new StringBuilder(str.length());
|
||||
int size = w.lengthOf(str);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||
w.from(str, 0, str.length()).write(buffer);
|
||||
Reader r = READER.get();
|
||||
r.read(buffer.flip(), b, true);
|
||||
assertEquals(str, b.toString());
|
||||
}
|
||||
|
||||
private void roundTrip(int... lengths) throws IOException {
|
||||
identity(charsOfLength(lengths));
|
||||
}
|
||||
}
|
||||
|
||||
// The idea is to build a number of input strings that are encoded
|
||||
// without the need for padding. The sizes of the encoded forms,
|
||||
// therefore, must be 8, 16, 24, 32, 48, 56, 64 and 72 bits. Then check
|
||||
// that they are encoded and then decoded into the same strings.
|
||||
|
||||
Helper h = new Helper();
|
||||
|
||||
// -- 8 bit code --
|
||||
|
||||
h.roundTrip( 8);
|
||||
|
||||
// -- 16 bit code --
|
||||
|
||||
h.roundTrip( 5, 11);
|
||||
h.roundTrip( 5, 5, 6);
|
||||
|
||||
// -- 24 bit code --
|
||||
|
||||
h.roundTrip(24);
|
||||
h.roundTrip( 5, 19);
|
||||
h.roundTrip( 5, 5, 14);
|
||||
h.roundTrip( 5, 5, 6, 8);
|
||||
|
||||
// -- 32 bit code --
|
||||
|
||||
h.roundTrip( 5, 27);
|
||||
h.roundTrip( 5, 5, 22);
|
||||
h.roundTrip( 5, 5, 7, 15);
|
||||
h.roundTrip( 5, 5, 5, 5, 12);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 7);
|
||||
|
||||
// -- 48 bit code --
|
||||
|
||||
h.roundTrip(20, 28);
|
||||
h.roundTrip( 5, 13, 30);
|
||||
h.roundTrip( 5, 5, 8, 30);
|
||||
h.roundTrip( 5, 5, 5, 5, 28);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 23);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 8, 15);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 13);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 8);
|
||||
|
||||
// -- 56 bit code --
|
||||
|
||||
h.roundTrip(26, 30);
|
||||
h.roundTrip( 5, 21, 30);
|
||||
h.roundTrip( 5, 5, 19, 27);
|
||||
h.roundTrip( 5, 5, 5, 11, 30);
|
||||
h.roundTrip( 5, 5, 5, 5, 6, 30);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 26);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 21);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 6, 15);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 11);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6);
|
||||
|
||||
// -- 64 bit code --
|
||||
|
||||
h.roundTrip( 6, 28, 30);
|
||||
h.roundTrip( 5, 5, 24, 30);
|
||||
h.roundTrip( 5, 5, 5, 19, 30);
|
||||
h.roundTrip( 5, 5, 5, 5, 14, 30);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 11, 28);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 6, 28);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 24);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 19);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 14);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 8);
|
||||
|
||||
// -- 72 bit code --
|
||||
|
||||
h.roundTrip(12, 30, 30);
|
||||
h.roundTrip( 5, 7, 30, 30);
|
||||
h.roundTrip( 5, 5, 5, 27, 30);
|
||||
h.roundTrip( 5, 5, 5, 5, 22, 30);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 19, 28);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 12, 30);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 7, 30);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 27);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 22);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 7, 15);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 12);
|
||||
h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 7);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cannotEncodeOutsideByte() {
|
||||
TestHelper.Block<Object> coding =
|
||||
() -> WRITER.get()
|
||||
.from(String.valueOf((char) 256), 0, 1)
|
||||
.write(ByteBuffer.allocate(1));
|
||||
RuntimeException e =
|
||||
TestHelper.assertVoidThrows(RuntimeException.class, coding);
|
||||
TestHelper.assertExceptionMessageContains(e, "char");
|
||||
}
|
||||
|
||||
private static void read(String hexdump, String decoded) {
|
||||
ByteBuffer source = SpecHelper.toBytes(hexdump);
|
||||
Appendable actual = new StringBuilder();
|
||||
Reader reader = READER.get();
|
||||
try {
|
||||
reader.read(source, actual, true);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
assertEquals(decoded, actual.toString());
|
||||
}
|
||||
|
||||
private static void readExhaustively(String hexdump, String decoded) {
|
||||
ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
|
||||
Reader reader = READER.get();
|
||||
ByteBuffer source = SpecHelper.toBytes(hexdump);
|
||||
StringBuilder actual = new StringBuilder();
|
||||
BuffersTestingKit.forEachSplit(source, buffers -> {
|
||||
try {
|
||||
for (ByteBuffer b : buffers) {
|
||||
reader.read(b, actual, false);
|
||||
}
|
||||
reader.read(EMPTY_BUFFER, actual, true);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
assertEquals(decoded, actual.toString());
|
||||
reader.reset();
|
||||
actual.setLength(0);
|
||||
});
|
||||
}
|
||||
|
||||
private static void write(String decoded, String hexdump) {
|
||||
Writer writer = WRITER.get();
|
||||
int n = writer.lengthOf(decoded);
|
||||
ByteBuffer destination = ByteBuffer.allocateDirect(n);
|
||||
writer.from(decoded, 0, decoded.length());
|
||||
boolean written = writer.write(destination);
|
||||
assertTrue(written);
|
||||
String actual = SpecHelper.toHexdump(destination.flip());
|
||||
assertEquals(hexdump, actual);
|
||||
writer.reset();
|
||||
}
|
||||
|
||||
private static void writeExhaustively(String decoded, String hexdump) {
|
||||
Writer writer = WRITER.get();
|
||||
int n = writer.lengthOf(decoded);
|
||||
ByteBuffer destination = ByteBuffer.allocate(n);
|
||||
BuffersTestingKit.forEachSplit(destination, byteBuffers -> {
|
||||
writer.from(decoded, 0, decoded.length());
|
||||
boolean written = false;
|
||||
for (ByteBuffer b : byteBuffers) {
|
||||
int pos = b.position();
|
||||
written = writer.write(b);
|
||||
b.position(pos); // "flip" to the saved position, for reading
|
||||
}
|
||||
assertTrue(written);
|
||||
ByteBuffer concated = BuffersTestingKit.concat(byteBuffers);
|
||||
String actual = SpecHelper.toHexdump(concated);
|
||||
assertEquals(hexdump, actual);
|
||||
writer.reset();
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Encodes a single character. This representation is padded, thus ready to
|
||||
* be decoded.
|
||||
*/
|
||||
private static ByteBuffer encode(Code code) {
|
||||
int EOS_MSB = EOS.hex << (32 - EOS.len);
|
||||
int padding = EOS_MSB >>> code.len;
|
||||
int hexMSB = code.hex << (32 - code.len);
|
||||
int c = hexMSB | padding;
|
||||
int n = bytesForBits(code.len);
|
||||
byte[] result = new byte[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
result[i] = (byte) (c >> (32 - 8 * (i + 1)));
|
||||
}
|
||||
return ByteBuffer.wrap(result);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,332 @@
|
|||
/*
|
||||
* Copyright (c) 2014, 2025, 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.
|
||||
*/
|
||||
package jdk.internal.net.http.hpack;
|
||||
|
||||
import jdk.internal.net.http.hpack.SimpleHeaderTable.HeaderField;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static jdk.internal.net.http.hpack.TestHelper.assertExceptionMessageContains;
|
||||
import static jdk.internal.net.http.hpack.TestHelper.assertThrows;
|
||||
import static jdk.internal.net.http.hpack.TestHelper.assertVoidThrows;
|
||||
import static jdk.internal.net.http.hpack.TestHelper.newRandom;
|
||||
import static java.lang.String.format;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class SimpleHeaderTableTest {
|
||||
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7541#appendix-A
|
||||
//
|
||||
// @formatter:off
|
||||
private static final String SPECIFICATION =
|
||||
" | 1 | :authority | |\n" +
|
||||
" | 2 | :method | GET |\n" +
|
||||
" | 3 | :method | POST |\n" +
|
||||
" | 4 | :path | / |\n" +
|
||||
" | 5 | :path | /index.html |\n" +
|
||||
" | 6 | :scheme | http |\n" +
|
||||
" | 7 | :scheme | https |\n" +
|
||||
" | 8 | :status | 200 |\n" +
|
||||
" | 9 | :status | 204 |\n" +
|
||||
" | 10 | :status | 206 |\n" +
|
||||
" | 11 | :status | 304 |\n" +
|
||||
" | 12 | :status | 400 |\n" +
|
||||
" | 13 | :status | 404 |\n" +
|
||||
" | 14 | :status | 500 |\n" +
|
||||
" | 15 | accept-charset | |\n" +
|
||||
" | 16 | accept-encoding | gzip, deflate |\n" +
|
||||
" | 17 | accept-language | |\n" +
|
||||
" | 18 | accept-ranges | |\n" +
|
||||
" | 19 | accept | |\n" +
|
||||
" | 20 | access-control-allow-origin | |\n" +
|
||||
" | 21 | age | |\n" +
|
||||
" | 22 | allow | |\n" +
|
||||
" | 23 | authorization | |\n" +
|
||||
" | 24 | cache-control | |\n" +
|
||||
" | 25 | content-disposition | |\n" +
|
||||
" | 26 | content-encoding | |\n" +
|
||||
" | 27 | content-language | |\n" +
|
||||
" | 28 | content-length | |\n" +
|
||||
" | 29 | content-location | |\n" +
|
||||
" | 30 | content-range | |\n" +
|
||||
" | 31 | content-type | |\n" +
|
||||
" | 32 | cookie | |\n" +
|
||||
" | 33 | date | |\n" +
|
||||
" | 34 | etag | |\n" +
|
||||
" | 35 | expect | |\n" +
|
||||
" | 36 | expires | |\n" +
|
||||
" | 37 | from | |\n" +
|
||||
" | 38 | host | |\n" +
|
||||
" | 39 | if-match | |\n" +
|
||||
" | 40 | if-modified-since | |\n" +
|
||||
" | 41 | if-none-match | |\n" +
|
||||
" | 42 | if-range | |\n" +
|
||||
" | 43 | if-unmodified-since | |\n" +
|
||||
" | 44 | last-modified | |\n" +
|
||||
" | 45 | link | |\n" +
|
||||
" | 46 | location | |\n" +
|
||||
" | 47 | max-forwards | |\n" +
|
||||
" | 48 | proxy-authenticate | |\n" +
|
||||
" | 49 | proxy-authorization | |\n" +
|
||||
" | 50 | range | |\n" +
|
||||
" | 51 | referer | |\n" +
|
||||
" | 52 | refresh | |\n" +
|
||||
" | 53 | retry-after | |\n" +
|
||||
" | 54 | server | |\n" +
|
||||
" | 55 | set-cookie | |\n" +
|
||||
" | 56 | strict-transport-security | |\n" +
|
||||
" | 57 | transfer-encoding | |\n" +
|
||||
" | 58 | user-agent | |\n" +
|
||||
" | 59 | vary | |\n" +
|
||||
" | 60 | via | |\n" +
|
||||
" | 61 | www-authenticate | |\n";
|
||||
// @formatter:on
|
||||
|
||||
static final int STATIC_TABLE_LENGTH = createStaticEntries().size();
|
||||
final Random rnd = newRandom();
|
||||
|
||||
/** Creates a header table under test. Override in subclass. */
|
||||
protected SimpleHeaderTable createHeaderTable(int maxSize) {
|
||||
return new SimpleHeaderTable(maxSize, HPACK.getLogger());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staticData0() {
|
||||
SimpleHeaderTable table = createHeaderTable(0);
|
||||
Map<Integer, HeaderField> staticHeaderFields = createStaticEntries();
|
||||
staticHeaderFields.forEach((index, expectedHeaderField) -> {
|
||||
SimpleHeaderTable.HeaderField actualHeaderField = table.get(index);
|
||||
assertEquals(expectedHeaderField.name, actualHeaderField.name);
|
||||
assertEquals(expectedHeaderField.value, actualHeaderField.value);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorSetsMaxSize() {
|
||||
int size = rnd.nextInt(64);
|
||||
SimpleHeaderTable table = createHeaderTable(size);
|
||||
assertEquals(0, table.size());
|
||||
assertEquals(size, table.maxSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void negativeMaximumSize() {
|
||||
int maxSize = -(rnd.nextInt(100) + 1); // [-100, -1]
|
||||
SimpleHeaderTable table = createHeaderTable(0);
|
||||
IllegalArgumentException e =
|
||||
assertVoidThrows(IllegalArgumentException.class,
|
||||
() -> table.setMaxSize(maxSize));
|
||||
assertExceptionMessageContains(e, "maxSize");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroMaximumSize() {
|
||||
SimpleHeaderTable table = createHeaderTable(0);
|
||||
table.setMaxSize(0);
|
||||
assertEquals(0, table.maxSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void negativeIndex() {
|
||||
int idx = -(rnd.nextInt(256) + 1); // [-256, -1]
|
||||
SimpleHeaderTable table = createHeaderTable(0);
|
||||
IndexOutOfBoundsException e =
|
||||
assertVoidThrows(IndexOutOfBoundsException.class,
|
||||
() -> table.get(idx));
|
||||
assertExceptionMessageContains(e, "index");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroIndex() {
|
||||
SimpleHeaderTable table = createHeaderTable(0);
|
||||
IndexOutOfBoundsException e =
|
||||
assertThrows(IndexOutOfBoundsException.class,
|
||||
() -> table.get(0));
|
||||
assertExceptionMessageContains(e, "index");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void length() {
|
||||
SimpleHeaderTable table = createHeaderTable(0);
|
||||
assertEquals(STATIC_TABLE_LENGTH, table.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void indexOutsideStaticRange() {
|
||||
SimpleHeaderTable table = createHeaderTable(0);
|
||||
int idx = table.length() + (rnd.nextInt(256) + 1);
|
||||
IndexOutOfBoundsException e =
|
||||
assertThrows(IndexOutOfBoundsException.class,
|
||||
() -> table.get(idx));
|
||||
assertExceptionMessageContains(e, "index");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void entryPutAfterStaticArea() {
|
||||
SimpleHeaderTable table = createHeaderTable(256);
|
||||
int idx = table.length() + 1;
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> table.get(idx));
|
||||
|
||||
byte[] bytes = new byte[32];
|
||||
rnd.nextBytes(bytes);
|
||||
String name = new String(bytes, StandardCharsets.ISO_8859_1);
|
||||
String value = "custom-value";
|
||||
|
||||
table.put(name, value);
|
||||
SimpleHeaderTable.HeaderField f = table.get(idx);
|
||||
assertEquals(name, f.name);
|
||||
assertEquals(value, f.value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staticTableHasZeroSize() {
|
||||
SimpleHeaderTable table = createHeaderTable(0);
|
||||
assertEquals(0, table.size());
|
||||
}
|
||||
|
||||
// TODO: negative indexes check
|
||||
// TODO: ensure full table clearance when adding huge header field
|
||||
// TODO: ensure eviction deletes minimum needed entries, not more
|
||||
|
||||
@Test
|
||||
public void fifo() {
|
||||
// Let's add a series of header fields
|
||||
int NUM_HEADERS = 32;
|
||||
SimpleHeaderTable table =
|
||||
createHeaderTable((32 + 4) * NUM_HEADERS);
|
||||
// ^ ^
|
||||
// entry overhead symbols per entry (max 2x2 digits)
|
||||
for (int i = 1; i <= NUM_HEADERS; i++) {
|
||||
String s = String.valueOf(i);
|
||||
table.put(s, s);
|
||||
}
|
||||
// They MUST appear in a FIFO order:
|
||||
// newer entries are at lower indexes
|
||||
// older entries are at higher indexes
|
||||
for (int j = 1; j <= NUM_HEADERS; j++) {
|
||||
SimpleHeaderTable.HeaderField f = table.get(STATIC_TABLE_LENGTH + j);
|
||||
int actualName = Integer.parseInt(f.name);
|
||||
int expectedName = NUM_HEADERS - j + 1;
|
||||
assertEquals(expectedName, actualName);
|
||||
}
|
||||
// Entries MUST be evicted in the order they were added:
|
||||
// the newer the entry the later it is evicted
|
||||
for (int k = 1; k <= NUM_HEADERS; k++) {
|
||||
SimpleHeaderTable.HeaderField f = table.evictEntry();
|
||||
assertEquals(String.valueOf(k), f.name);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToString() {
|
||||
testToString0();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToStringDifferentLocale() {
|
||||
Locale locale = Locale.getDefault();
|
||||
Locale.setDefault(Locale.FRENCH);
|
||||
try {
|
||||
String s = format("%.1f", 3.1);
|
||||
assertEquals("3,1", s); // assumption of the test, otherwise the test is useless
|
||||
testToString0();
|
||||
} finally {
|
||||
Locale.setDefault(locale);
|
||||
}
|
||||
}
|
||||
|
||||
private void testToString0() {
|
||||
SimpleHeaderTable table = createHeaderTable(0);
|
||||
{
|
||||
int maxSize = 2048;
|
||||
table.setMaxSize(maxSize);
|
||||
String expected = format(
|
||||
"dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
|
||||
0, STATIC_TABLE_LENGTH, 0, maxSize, 0.0);
|
||||
assertEquals(expected, table.toString());
|
||||
}
|
||||
|
||||
{
|
||||
String name = "custom-name";
|
||||
String value = "custom-value";
|
||||
int size = 512;
|
||||
|
||||
table.setMaxSize(size);
|
||||
table.put(name, value);
|
||||
String s = table.toString();
|
||||
|
||||
int used = name.length() + value.length() + 32;
|
||||
double ratio = used * 100.0 / size;
|
||||
|
||||
String expected = format(
|
||||
"dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
|
||||
1, STATIC_TABLE_LENGTH + 1, used, size, ratio);
|
||||
assertEquals(expected, s);
|
||||
}
|
||||
|
||||
{
|
||||
table.setMaxSize(78);
|
||||
table.put(":method", "");
|
||||
table.put(":status", "");
|
||||
String s = table.toString();
|
||||
String expected =
|
||||
format("dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
|
||||
2, STATIC_TABLE_LENGTH + 2, 78, 78, 100.0);
|
||||
assertEquals(expected, s);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stateString() {
|
||||
SimpleHeaderTable table = createHeaderTable(256);
|
||||
table.put("custom-key", "custom-header");
|
||||
// @formatter:off
|
||||
assertEquals("[ 1] (s = 55) custom-key: custom-header\n" +
|
||||
" Table size: 55", table.getStateString());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
static Map<Integer, HeaderField> createStaticEntries() {
|
||||
Pattern line = Pattern.compile(
|
||||
"\\|\\s*(?<index>\\d+?)\\s*\\|\\s*(?<name>.+?)\\s*\\|\\s*(?<value>.*?)\\s*\\|");
|
||||
Matcher m = line.matcher(SPECIFICATION);
|
||||
Map<Integer, HeaderField> result = new HashMap<>();
|
||||
while (m.find()) {
|
||||
int index = Integer.parseInt(m.group("index"));
|
||||
String name = m.group("name");
|
||||
String value = m.group("value");
|
||||
HeaderField f = new HeaderField(name, value);
|
||||
result.put(index, f);
|
||||
}
|
||||
return Collections.unmodifiableMap(result); // lol
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2014, 2018, 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.
|
||||
*/
|
||||
package jdk.internal.net.http.hpack;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
//
|
||||
// THIS IS NOT A TEST
|
||||
//
|
||||
public final class SpecHelper {
|
||||
|
||||
private SpecHelper() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public static ByteBuffer toBytes(String hexdump) {
|
||||
Pattern hexByte = Pattern.compile("[0-9a-fA-F]{2}");
|
||||
List<String> bytes = new ArrayList<>();
|
||||
Matcher matcher = hexByte.matcher(hexdump);
|
||||
while (matcher.find()) {
|
||||
bytes.add(matcher.group(0));
|
||||
}
|
||||
ByteBuffer result = ByteBuffer.allocate(bytes.size());
|
||||
for (String f : bytes) {
|
||||
result.put((byte) Integer.parseInt(f, 16));
|
||||
}
|
||||
result.flip();
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String toHexdump(ByteBuffer bb) {
|
||||
List<String> words = new ArrayList<>();
|
||||
int i = 0;
|
||||
while (bb.hasRemaining()) {
|
||||
if (i % 2 == 0) {
|
||||
words.add("");
|
||||
}
|
||||
byte b = bb.get();
|
||||
String hex = Integer.toHexString(256 + Byte.toUnsignedInt(b)).substring(1);
|
||||
words.set(i / 2, words.get(i / 2) + hex);
|
||||
i++;
|
||||
}
|
||||
return words.stream().collect(Collectors.joining(" "));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* Copyright (c) 2014, 2025, 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.
|
||||
*/
|
||||
package jdk.internal.net.http.hpack;
|
||||
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public final class TestHelper {
|
||||
|
||||
public static Random newRandom() {
|
||||
long seed = Long.getLong("jdk.test.lib.random.seed", System.currentTimeMillis());
|
||||
System.out.println("new java.util.Random(" + seed + ")");
|
||||
return new Random(seed);
|
||||
}
|
||||
|
||||
public static <T extends Throwable> T assertVoidThrows(Class<T> clazz, Block<?> code) {
|
||||
return assertThrows(clazz, () -> {
|
||||
code.run();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static <T extends Throwable> T assertThrows(Class<T> clazz, ReturningBlock<?> code) {
|
||||
Objects.requireNonNull(clazz, "clazz == null");
|
||||
Objects.requireNonNull(code, "code == null");
|
||||
try {
|
||||
code.run();
|
||||
} catch (Throwable t) {
|
||||
if (clazz.isInstance(t)) {
|
||||
return clazz.cast(t);
|
||||
}
|
||||
throw new AssertionError("Expected to catch exception of type "
|
||||
+ clazz.getCanonicalName() + ", instead caught "
|
||||
+ t.getClass().getCanonicalName(), t);
|
||||
|
||||
}
|
||||
throw new AssertionError(
|
||||
"Expected to catch exception of type " + clazz.getCanonicalName()
|
||||
+ ", but caught nothing");
|
||||
}
|
||||
|
||||
public static <T> T assertDoesNotThrow(ReturningBlock<T> code) {
|
||||
Objects.requireNonNull(code, "code == null");
|
||||
try {
|
||||
return code.run();
|
||||
} catch (Throwable t) {
|
||||
throw new AssertionError(
|
||||
"Expected code block to exit normally, instead " +
|
||||
"caught " + t.getClass().getCanonicalName(), t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertVoidDoesNotThrow(Block<?> code) {
|
||||
Objects.requireNonNull(code, "code == null");
|
||||
try {
|
||||
code.run();
|
||||
} catch (Throwable t) {
|
||||
throw new AssertionError(
|
||||
"Expected code block to exit normally, instead " +
|
||||
"caught " + t.getClass().getCanonicalName(), t);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void assertExceptionMessageContains(Throwable t,
|
||||
CharSequence firstSubsequence,
|
||||
CharSequence... others) {
|
||||
assertCharSequenceContains(t.getMessage(), firstSubsequence, others);
|
||||
}
|
||||
|
||||
public static void assertCharSequenceContains(CharSequence s,
|
||||
CharSequence firstSubsequence,
|
||||
CharSequence... others) {
|
||||
if (s == null) {
|
||||
throw new NullPointerException("Exception message is null");
|
||||
}
|
||||
String str = s.toString();
|
||||
String missing = null;
|
||||
if (!str.contains(firstSubsequence.toString())) {
|
||||
missing = firstSubsequence.toString();
|
||||
} else {
|
||||
for (CharSequence o : others) {
|
||||
if (!str.contains(o.toString())) {
|
||||
missing = o.toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (missing != null) {
|
||||
throw new AssertionError("CharSequence '" + s + "'" + " does not "
|
||||
+ "contain subsequence '" + missing + "'");
|
||||
}
|
||||
}
|
||||
|
||||
public interface ReturningBlock<T> {
|
||||
T run() throws Throwable;
|
||||
}
|
||||
|
||||
public interface Block<T> {
|
||||
void run() throws Throwable;
|
||||
}
|
||||
|
||||
// tests
|
||||
|
||||
@Test
|
||||
public void assertThrows() {
|
||||
assertThrows(NullPointerException.class, () -> ((Object) null).toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void assertThrowsWrongType() {
|
||||
try {
|
||||
assertThrows(IllegalArgumentException.class, () -> ((Object) null).toString());
|
||||
} catch (AssertionError e) {
|
||||
Throwable cause = e.getCause();
|
||||
String message = e.getMessage();
|
||||
if (cause != null
|
||||
&& cause instanceof NullPointerException
|
||||
&& message != null
|
||||
&& message.contains("instead caught")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void assertThrowsNoneCaught() {
|
||||
try {
|
||||
assertThrows(IllegalArgumentException.class, () -> null);
|
||||
} catch (AssertionError e) {
|
||||
Throwable cause = e.getCause();
|
||||
String message = e.getMessage();
|
||||
if (cause == null
|
||||
&& message != null
|
||||
&& message.contains("but caught nothing")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
BIN
test/jdk/java/net/httpclient/http2/keystore.p12
Normal file
BIN
test/jdk/java/net/httpclient/http2/keystore.p12
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue