/
githubmirror
/
panama-vector
Обзор
Документация
Войти
/
githubmirror
/
panama-vector
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java
473 строки
20 KB
Daniel Fuchs
8378569: Refactor test/jdk/java/net/httpclient/http3/H3ErrorHandlingTest.java to use JUnit
09 мар 2026, 13:25
09 мар 2026, 13:25
23adf6a
Код
Авторство
О чём код?
/* * 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. */ import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.quic.QuicStandaloneServer; import jdk.internal.net.http.common.Logger; import jdk.internal.net.http.common.Utils; import jdk.internal.net.quic.QuicVersion; 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.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.OutputStream; import java.net.ProtocolException; import java.net.http.HttpClient; import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.util.HexFormat; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BooleanSupplier; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpOption.H3_DISCOVERY; import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; /* * @test * @bug 8369595 * @summary Verifies that the HTTP/3 malformed responses are correctly handled * @library /test/jdk/java/net/httpclient/lib * /test/lib * @run junit ${test.main.class} */ /// Verifies that the HTTP/3 malformed responses are correctly handled. /// /// ### HTTP/3 `HEADERS` frame & QPACK Field Section encoding crash course /// /// Consider an HTTP/3 `HEADERS` frame that carries: /// /// ``` /// :status: 200 /// content-length: 2 /// ``` /// /// This will be encoded as the following byte sequence: /// /// ``` /// 01 06 00 00 D9 54 01 32 /// ``` /// /// Let's start with decoding the HTTP/3 frame: /// /// - `01`: Frame Type (`01` denotes `HEADERS``) /// /// - `06`: Payload length (6 bytes) /// /// Figured this is a `HEADERS` frame containing 6 bytes: `00 00 D9 54 01 32`. /// Let's decode the QPACK Field Section /// /// - `00`: Required Insert Count (0) /// /// - `00`: Base (0) /// /// - `D9`: /// QPACK has a static table (indexed from 0) and `:status: 200` is at the /// static-table index 25. /// /// ``` /// D9 = <1101 1001> /// = <1> (Indexed Field Line) /// + <1> (Static Table) /// + <01 1001> (entry index = 25) /// ``` /// /// - `54 01 32`: /// `content-length: 2` can be encoded as a *literal field line with name /// reference* using the static name `content-length` (static index 4) and /// the literal value `2`. /// /// ``` /// 54 01 32 = <0101 0100 0000 0001 0011 0010> /// = <0> (Literal Field Line) /// + <1> (Name Reference) /// + <0100> (entry index = 4) /// + <0000 0001> (value length = 1 byte) /// + <0011 0010> (value = ASCII "2" = 0x32 = 50) /// ``` /// /// Note that the `value length` field (i.e., `0000 0001`) follows a variable /// coding scheme: /// /// | Prefix | Total size | Payload size | Max value | /// | --------------------- | ---------- | ------------ | ------------- | /// | `00xx xxxx` | 1 byte | 6 bits | 63 | /// | `01xx xxxx xxxx xxxx` | 2 bytes | 14 bits | 16,383 | /// | `10xx xxx…` | 4 bytes | 30 bits | 1,073,741,823 | /// | `11xx xxx…` | 8 bytes | 62 bits | 4.61e18 | class H3MalformedResponseTest { private static final String CLASS_NAME = H3MalformedResponseTest.class.getSimpleName(); private static final Logger LOGGER = Utils.getDebugLogger(CLASS_NAME::toString, Utils.DEBUG); private static final SSLContext SSL_CONTEXT = SimpleSSLContext.findSSLContext(); private static QuicStandaloneServer SERVER; private static HttpRequest REQUEST; @BeforeAll static void setUp() throws Exception { // Create and start the server SERVER = QuicStandaloneServer.newBuilder() .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) .sslContext(SSL_CONTEXT) .alpn("h3") .build(); SERVER.start(); LOGGER.log("Server is started at {}", SERVER.getAddress()); // Create the request var requestURI = URIBuilder.newBuilder() .scheme("https") .loopback() .port(SERVER.getAddress().getPort()) .path("/" + CLASS_NAME) .build(); REQUEST = HttpRequest.newBuilder(requestURI) .version(Version.HTTP_3) .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) .build(); } @AfterAll static void tearDown() { close("server", SERVER); } private static void close(String name, AutoCloseable closeable) { if (closeable != null) { LOGGER.log("Closing {}", name); try { closeable.close(); } catch (Exception e) { LOGGER.log("Could not close " + name, e); } } } /// Malformed responses that should not be accepted by the client, but /// should neither cause the connection to get closed. static Object[][] malformedResponsesPreservingConnection() { return new Object[][]{ {"empty", IOException.class, parseHex("")}, {"non-final response", IOException.class, parseHex( "01040000", // headers, length 4, section prefix "ff00" // :status:100 )}, {"uppercase header name", ProtocolException.class, parseHex( "01090000", // headers, length 9, section prefix "d9", // :status:200 "234147450130", // AGE:0 "000100" // data, 1 byte )}, {"content too long", IOException.class, parseHex( "01040000", // headers, length 4, section prefix "d9", // :status:200 "c4", // content-length:0 "000100" // data, 1 byte )}, {"content too short", IOException.class, parseHex( "01060000", // headers, length 6, section prefix "d9", // :status:200 "540132", // content-length:2 "000100" // data, 1 byte )}, {"text in content-length", ProtocolException.class, parseHex( "01060000" + // headers, length 6, section prefix "d9" + // :status:200 "540161" + // content-length:a "000100" // data, 1 byte )}, {"connection: close", ProtocolException.class, parseHex( "01150000", // headers, length 21, section prefix "d9", // :status:200 "2703636F6E6E656374696F6E05636C6F7365" + // connection:close "000100" // data, 1 byte )}, // request pseudo-headers in response {":method in response", ProtocolException.class, parseHex( "01040000", // headers, length 4, section prefix "d9", // :status:200 "d1", // :method:get "000100" // data, 1 byte )}, {":authority in response", ProtocolException.class, parseHex( "01100000", // headers, length 16, section prefix "d9", // :status:200 "508b089d5c0b8170dc702fbce7", // :authority "000100" // data, 1 byte )}, {":path in response", ProtocolException.class, parseHex( "010a0000", // headers, length 10, section prefix "d9", // :status:200 "51856272d141ff", // :path "000100" // data, 1 byte )}, {":scheme in response", ProtocolException.class, parseHex( "01040000", // headers, length 4, section prefix "d9", // :status:200 "d7", // :scheme:https "000100" // data, 1 byte )}, {"undefined pseudo-header", ProtocolException.class, parseHex( "01080000", // headers, length 8, section prefix "d9", // :status:200 "223A6D0130", // :m:0 "000100" // data, 1 byte )}, {"pseudo-header after regular", ProtocolException.class, parseHex( "011a0000", // headers, length 26, section prefix "5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747", //user-agent "d9", // :status:200 "000100" // data, 1 byte )}, {"trailer", IOException.class, parseHex( "01020000" // headers, length 2, section prefix )}, {"trailer+data", IOException.class, parseHex( "01020000", // headers, length 2, section prefix "000100" // data, 1 byte )}, // valid characters include \t, 0x20-0x7e, 0x80-0xff (RFC 9110, section 5.5) {"invalid character in field value 00", ProtocolException.class, parseHex( "01060000", // headers, length 6, section prefix "d9", // :status:200 "570100", // etag:\0 "000100" // data, 1 byte )}, {"invalid character in field value 0a", ProtocolException.class, parseHex( "01060000", // headers, length 6, section prefix "d9", // :status:200 "57010a", // etag:\n "000100" // data, 1 byte )}, {"invalid character in field value 0d", ProtocolException.class, parseHex( "01060000", // headers, length 6, section prefix "d9", // :status:200 "57010d", // etag:\r "000100" // data, 1 byte )}, {"invalid character in field value 7f", ProtocolException.class, parseHex( "01060000", // headers, length 6, section prefix "d9", // :status:200 "57017f", // etag: 0x7f "000100" // data, 1 byte )}, }; } /// Malformed responses that should not be accepted by the client. /// They might or might not cause the connection to get closed (`H3_FRAME_UNEXPECTED`). static Object[][] malformedResponses() { // data before headers is covered by H3ErrorHandlingTest return new Object[][]{ {"100+data", IOException.class, parseHex( "01040000", // headers, length 4, section prefix "ff00", // :status:100 "000100" // data, 1 byte )}, {"100+data+200", IOException.class, parseHex( "01040000", // headers, length 4, section prefix "ff00", // :status:100 "000100", // data, 1 byte "01030000", // headers, length 3, section prefix "d9" // :status:200 )}, {"200+data+200", IOException.class, parseHex( "01030000", // headers, length 3, section prefix "d9", // :status:200 "000100", // data, 1 byte "01030000", // headers, length 3, section prefix "d9" // :status:200 )}, {"200+data+100", IOException.class, parseHex( "01030000", // headers, length 3, section prefix "d9", // :status:200 "000100", // data, 1 byte "01040000", // headers, length 4, section prefix "ff00" // :status:100 )}, {"200+data+trailers+data", ProtocolException.class, parseHex( "01030000", // headers, length 3, section prefix "d9", // :status:200 "000100", // data, 1 byte "01020000", // trailers, length 2, section prefix "000100" // data, 1 byte )}, {"200+trailers+data", IOException.class, parseHex( "01030000", // headers, length 3, section prefix "d9", // :status:200 "01020000", // trailers, length 2, section prefix "000100" // data, 1 byte )}, {"200+200", IOException.class, parseHex( "01030000", // headers, length 3, section prefix "d9", // :status:200 "01030000", // headers, length 3, section prefix "d9" // :status:200 )}, {"200+100", IOException.class, parseHex( "01030000", // headers, length 3, section prefix "d9", // :status:200 "01040000", // headers, length 4, section prefix "ff00" // :status:100 )}, }; } /// Well-formed responses that should be accepted by the client. static Object[][] wellFormedResponses() { return new Object[][]{ {"100+200+data+reserved", parseHex( "01040000", // headers, length 4, section prefix "ff00", // :status:100 "01030000", // headers, length 3, section prefix "d9", // :status:200 "000100", // data, 1 byte "210100" // reserved, 1 byte )}, {"200+data+reserved", parseHex( "01030000", // headers, length 3, section prefix "d9", // :status:200 "000100", // data, 1 byte "210100" // reserved, 1 byte )}, {"200+data", parseHex( "01030000", // headers, length 3, section prefix "d9", // :status:200 "000100" // data, 1 byte )}, {"200+user-agent+data", parseHex( "011a0000", // headers, length 26, section prefix "d9", // :status:200 "5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747", //user-agent "000100" // data, 1 byte )}, {"200", parseHex( "01030000", // headers, length 3, section prefix "d9" // :status:200 )}, {"200+data+data", parseHex( "01030000", // headers, length 3, section prefix "d9", // :status:200 "000100", // data, 1 byte "000100" // data, 1 byte )}, {"200+data+trailers", parseHex( "01030000", // headers, length 3, section prefix "d9", // :status:200 "000100", // data, 1 byte "01020000" // trailers, length 2, section prefix )}, {"200+trailers", parseHex( "01030000", // headers, length 3, section prefix "d9", // :status:200 "01020000" // trailers, length 2, section prefix )}, }; } private static byte[] parseHex(String... strings) { var buffer = new StringBuilder(); for (String string : strings) { buffer.append(string); } return HexFormat.of().parseHex(buffer.toString()); } @ParameterizedTest @MethodSource("wellFormedResponses") void testWellFormedResponse(String desc, byte[] serverResponseBytes) throws Exception { var connectionTerminated = configureServerResponse(serverResponseBytes); try (var client = createClient()) { final HttpResponse<Void> response = client.send(REQUEST, BodyHandlers.discarding()); assertEquals(200, response.statusCode()); assertFalse(connectionTerminated.getAsBoolean(), "Expected the connection to be open"); } } @ParameterizedTest @MethodSource("malformedResponsesPreservingConnection") void testMalformedResponsePreservingConnection( String desc, Class<? extends Exception> exceptionClass, byte[] serverResponseBytes) { var connectionTerminated = configureServerResponse(serverResponseBytes); try (var client = createClient()) { var exception = assertThrows(exceptionClass, () -> client.send(REQUEST, BodyHandlers.discarding())); LOGGER.log("Got expected exception for: " + desc, exception); assertFalse(connectionTerminated.getAsBoolean(), "Expected the connection to be open"); } } @ParameterizedTest @MethodSource("malformedResponses") void testMalformedResponse( String desc, Class<? extends Exception> exceptionClass, byte[] serverResponseBytes) { configureServerResponse(serverResponseBytes); try (var client = createClient()) { var exception = assertThrows(exceptionClass, () -> client.send(REQUEST, BodyHandlers.discarding())); LOGGER.log("Got expected exception for: " + desc, exception); } } private static HttpClient createClient() { return HttpServerAdapters.createClientBuilderForH3() .proxy(NO_PROXY) .version(Version.HTTP_3) .sslContext(SSL_CONTEXT) .build(); } private static BooleanSupplier configureServerResponse(byte[] serverResponseBytes) { var connectionTerminated = new AtomicBoolean(); SERVER.setHandler((c, s)-> { try (OutputStream outputStream = s.outputStream()) { outputStream.write(serverResponseBytes); } c.futureTerminationCause().handle((_, _) -> { connectionTerminated.set(true); return true; }); }); return connectionTerminated::get; } }