/
githubmirror
/
panama-vector
Обзор
Документация
Войти
/
githubmirror
/
panama-vector
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
test/jdk/java/net/httpclient/http3/H3ErrorHandlingTest.java
1 072 строки
46 KB
Daisuke Yamazaki
8372325: Refactor tests under jdk/java/net/httpclient to use ${test.main.class}
09 апр 2026, 13:51
09 апр 2026, 13:51
261011a
Код
Авторство
О чём код?
/* * 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.QuicServerConnection; import jdk.httpclient.test.lib.quic.QuicStandaloneServer; import jdk.internal.net.http.common.SequentialScheduler; import jdk.internal.net.http.http3.Http3Error; import jdk.internal.net.http.quic.QuicConnectionId; import jdk.internal.net.http.quic.TerminationCause; import jdk.internal.net.http.quic.streams.QuicSenderStream; import jdk.internal.net.quic.QuicTransportErrors; import jdk.internal.net.quic.QuicTransportException; import jdk.internal.net.quic.QuicVersion; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.net.URIBuilder; import jdk.test.lib.Utils; import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.OutputStream; import java.net.ProtocolException; import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.time.Duration; import java.util.Arrays; import java.util.HexFormat; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import static java.net.http.HttpClient.Version.HTTP_3; import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; import static java.net.http.HttpOption.H3_DISCOVERY; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.*; /* * @test * @bug 8373409 8377181 * @key intermittent * @comment testResetControlStream may fail if the client doesn't read the stream type * before the stream is reset, * testConnectionCloseXXX may fail because connection_close frame is not retransmitted * @summary Verifies that the HTTP client responds with the right error codes and types * @library /test/lib /test/jdk/java/net/httpclient/lib * @library ../access * @build jdk.test.lib.net.SimpleSSLContext * jdk.httpclient.test.lib.common.HttpServerAdapters * @build java.net.http/jdk.internal.net.http.Http3ConnectionAccess * @run junit/othervm * -Djdk.internal.httpclient.debug=true * -Djdk.httpclient.HttpClient.log=requests,responses,errors ${test.main.class} */ public class H3ErrorHandlingTest implements HttpServerAdapters { private static final SSLContext sslContext = SimpleSSLContext.findSSLContext(); private static QuicStandaloneServer server; private static String requestURIBase; public static Object[][] controlStreams() { // control / encoder / decoder return new Object[][] {{(byte)0}, {(byte)2}, {(byte)3}}; } static final byte[] data = new byte[]{(byte)0,(byte)0}; static final byte[] headers = new byte[]{(byte)1,(byte)0}; static final byte[] reserved1 = new byte[]{(byte)2,(byte)0}; static final byte[] cancel_push = new byte[]{(byte)3,(byte)1,(byte)0}; static final byte[] settings = new byte[]{(byte)4,(byte)0}; static final byte[] push_promise = new byte[]{(byte)5,(byte)1,(byte)0}; // 48 bytes, ID 0, 47 byte headers static final byte[] valid_push_promise = HexFormat.of().parseHex( "0530000000"+ // push promise, length 48, id 0, section prefix "508b089d5c0b8170dc702fbce7"+ // :authority "d1"+ // :method:get "51856272d141ff"+ // :path "d7"+ // :scheme:https "5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747"); //user-agent static final byte[] reserved2 = new byte[]{(byte)6,(byte)0}; static final byte[] goaway = new byte[]{(byte)7,(byte)1,(byte)4}; static final byte[] reserved3 = new byte[]{(byte)8,(byte)0}; static final byte[] reserved4 = new byte[]{(byte)9,(byte)0}; static final byte[] max_push_id = new byte[]{(byte)13,(byte)1,(byte)0}; static final byte[] huge_id_push_promise = new byte[]{(byte)5,(byte)10, (byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255, (byte)0, (byte)0}; /* Truncates or expands the frame to the specified length */ private static Object[][] chopFrame(byte[] frame, int... lengths) { var result = new Object[lengths.length][]; for (int i = 0; i< lengths.length; i++) { int length = lengths[i]; byte[] choppedFrame = Arrays.copyOf(frame, length + 2); choppedFrame[1] = (byte)length; result[i] = new Object[] {choppedFrame, lengths[i]}; } return result; } /* Truncates or expands the byte array to the specified length */ private static Object[][] chopBytes(byte[] bytes, int... lengths) { var result = new Object[lengths.length][]; for (int i = 0; i< lengths.length; i++) { int length = lengths[i]; byte[] choppedBytes = Arrays.copyOf(bytes, length); result[i] = new Object[] {choppedBytes, lengths[i]}; } return result; } public static Object[][] malformedSettingsFrames() { // 2-byte ID, 2-byte value byte[] settingsFrame = new byte[]{(byte)4,(byte)4,(byte)0x40, (byte)6, (byte)0x40, (byte)6}; return chopFrame(settingsFrame, 1, 2, 3); } public static Object[][] malformedCancelPushFrames() { byte[] cancelPush = new byte[]{(byte)3,(byte)2, (byte)0x40, (byte)0}; return chopFrame(cancelPush, 0, 1, 3, 9); } public static Object[][] malformedGoawayFrames() { byte[] goaway = new byte[]{(byte)7,(byte)2, (byte)0x40, (byte)0}; return chopFrame(goaway, 0, 1, 3, 9); } public static Object[][] malformedResponseHeadersFrames() { byte[] responseHeaders = HexFormat.of().parseHex( "011a0000"+ // headers, length 26, section prefix "d9"+ // :status:200 "5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747"); //user-agent return chopFrame(responseHeaders, 0, 1, 4, 5, 6, 7); } public static Object[][] truncatedResponseFrames() { byte[] response = HexFormat.of().parseHex( "01030000"+ // headers, length 3, section prefix "d9"+ // :status:200 "000100"+ // data, 1 byte "210100" // reserved, 1 byte ); return chopBytes(response, 1, 2, 3, 4, 6, 7, 9, 10); } public static Object[][] truncatedControlFrames() { byte[] response = HexFormat.of().parseHex( "00"+ // stream type: control "04022100"+ //settings, reserved "070104"+ //goaway, 4 "210100" // reserved, 1 byte ); return chopBytes(response, 2, 3, 4, 6, 7, 9, 10); } public static Object[][] malformedPushPromiseFrames() { return chopFrame(valid_push_promise, 0, 1, 2, 4, 5, 6); } public static Object[][] invalidControlFrames() { // frames not valid on the server control stream (after settings) // all except cancel_push / goaway (max_push_id is client-only) return new Object[][] {{data}, {headers}, {settings}, {push_promise}, {max_push_id}, {reserved1}, {reserved2}, {reserved3}, {reserved4}}; } public static Object[][] invalidResponseFrames() { // frames not valid on the response stream // all except headers / push_promise // data is not valid as the first frame return new Object[][] {{data}, {cancel_push}, {settings}, {goaway}, {max_push_id}, {reserved1}, {reserved2}, {reserved3}, {reserved4}}; } public static Object[][] invalidPushFrames() { // frames not valid on the push promise stream // all except headers // data is not valid as the first frame return new Object[][] {{data}, {cancel_push}, {settings}, {push_promise}, {goaway}, {max_push_id}, {reserved1}, {reserved2}, {reserved3}, {reserved4}}; } @BeforeAll public static void beforeClass() throws Exception { server = QuicStandaloneServer.newBuilder() .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) .sslContext(sslContext) .alpn("h3") .build(); server.start(); System.out.println("Server started at " + server.getAddress()); requestURIBase = URIBuilder.newBuilder().scheme("https").loopback() .port(server.getAddress().getPort()).build().toString(); } @AfterAll public static void afterClass() throws Exception { if (server != null) { System.out.println("Stopping server " + server.getAddress()); server.close(); } } /** * Server sends a non-settings frame on the control stream */ @Test public void testNonSettingsFrame() throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream controlStream; controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); var writer = controlStream.connectWriter(scheduler); // control stream, reserved frame, length 0 byte[] bytesToWrite = new byte[] { 0, 0x21, 0 }; writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_MISSING_SETTINGS); } /** * Server opens 2 control streams */ @ParameterizedTest @MethodSource("controlStreams") public void testTwoControlStreams(byte type) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream controlStream, controlStream2; controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); controlStream2 = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); var writer = controlStream.connectWriter(scheduler); var writer2 = controlStream2.connectWriter(scheduler); // control stream byte[] bytesToWrite = new byte[] { type }; writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); writer2.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_STREAM_CREATION_ERROR); } /** * Server closes control stream */ @ParameterizedTest @MethodSource("controlStreams") public void testCloseControlStream(byte type) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream controlStream; controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var controlscheduler = SequentialScheduler.lockingScheduler(() -> {}); var writer = controlStream.connectWriter(controlscheduler); byte[] bytesToWrite = new byte[] { type }; writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), true); completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_CLOSED_CRITICAL_STREAM); } /** * Server resets control stream */ @ParameterizedTest @MethodSource("controlStreams") public void testResetControlStream(byte type) throws Exception { try { System.out.printf("testResetControlStream(%s) - first attempt%n", type); System.err.printf("%ntestResetControlStream(%s) - first attempt%n", type); testResetControlStreamImpl(type); System.out.printf("testResetControlStream(%s) - first attempt succeeded!%n", type); System.err.printf("%ntestResetControlStream(%s) - first attempt succeeded!%n", type); } catch (Throwable t) { System.out.printf("testResetControlStream(%s) - first attempt failed: %s%n", type, t); System.err.printf("%ntestResetControlStream(%s) - first attempt failed: %s%n", type); System.out.printf("testResetControlStream(%s) - retrying...%n", type); System.err.printf("%ntestResetControlStream(%s) - retrying...%n", type); testResetControlStreamImpl(type); System.out.printf("testResetControlStream(%s) - retry succeeded!%n", type); System.err.printf("%ntestResetControlStream(%s) - retry succeeded!%n", type); } } private void testResetControlStreamImpl(byte type) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream controlStream; controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var controlscheduler = SequentialScheduler.lockingScheduler(() -> {}); var writer = controlStream.connectWriter(controlscheduler); byte[] bytesToWrite = new byte[] { type }; writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); // wait for the stream data to be sent before resetting System.out.println("Server: sending first ping"); c.requestSendPing().join(); // sometimes the first ping succeeds before the stream frame is delivered. // Send another one just in case. System.out.println("Server: sending second ping"); c.requestSendPing().join(); System.out.println("Server: resetting control stream " + writer.stream().streamId()); // the test may fail if the stream type byte is not processed by HTTP3 // before the reset is received. writer.reset(0); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_CLOSED_CRITICAL_STREAM); } /** * Server sends unexpected frame on control stream */ @ParameterizedTest @MethodSource("invalidControlFrames") public void testUnexpectedControlFrame(byte[] frame) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream controlStream; controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); var writer = controlStream.connectWriter(scheduler); // control stream, settings frame, length 0 byte[] bytesToWrite = new byte[] { 0, 4, 0 }; ByteBuffer buf = ByteBuffer.allocate(3 + frame.length); buf.put(bytesToWrite); buf.put(frame); buf.flip(); writer.scheduleForWriting(buf, false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_FRAME_UNEXPECTED); } /** * Server sends malformed settings frame */ @ParameterizedTest @MethodSource("malformedSettingsFrames") public void testMalformedSettingsFrame(byte[] frame, int bytes) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream controlStream; controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); var writer = controlStream.connectWriter(scheduler); // control stream byte[] bytesToWrite = new byte[] { 0 }; ByteBuffer buf = ByteBuffer.allocate(3 + frame.length); buf.put(bytesToWrite); buf.put(frame); buf.flip(); writer.scheduleForWriting(buf, false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_FRAME_ERROR); } /** * Server sends malformed goaway frame */ @ParameterizedTest @MethodSource("malformedGoawayFrames") public void testMalformedGoawayFrame(byte[] frame, int bytes) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream controlStream; controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); var writer = controlStream.connectWriter(scheduler); // control stream, settings frame, length 0 byte[] bytesToWrite = new byte[] { 0, 4, 0 }; ByteBuffer buf = ByteBuffer.allocate(3 + frame.length); buf.put(bytesToWrite); buf.put(frame); buf.flip(); writer.scheduleForWriting(buf, false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_FRAME_ERROR); } /** * Server sends malformed cancel push frame */ @ParameterizedTest @MethodSource("malformedCancelPushFrames") public void testMalformedCancelPushFrame(byte[] frame, int bytes) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream controlStream; controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); var writer = controlStream.connectWriter(scheduler); // control stream, settings frame, length 0 byte[] bytesToWrite = new byte[] { 0, 4, 0 }; ByteBuffer buf = ByteBuffer.allocate(3 + frame.length); buf.put(bytesToWrite); buf.put(frame); buf.flip(); writer.scheduleForWriting(buf, false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerPushError(errorCF, Http3Error.H3_FRAME_ERROR); } /** * Server sends invalid GOAWAY frame sequence */ @Test public void testInvalidGoAwaySequence() throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream controlStream; controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); var writer = controlStream.connectWriter(scheduler); // control stream, settings frame, length 0, GOAWAY, id = 4, GOAWAY, id = 8 byte[] bytesToWrite = new byte[] { 0, 4, 0, 7, 1, 4, 7, 1, 8}; ByteBuffer buf = ByteBuffer.wrap(bytesToWrite); writer.scheduleForWriting(buf, false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_ID_ERROR); } /** * Server sends invalid GOAWAY stream ID */ @Test public void testInvalidGoAwayId() throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream controlStream; controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); var writer = controlStream.connectWriter(scheduler); // control stream, settings frame, length 0, GOAWAY, id = 7 byte[] bytesToWrite = new byte[] { 0, 4, 0, 7, 1, 7}; ByteBuffer buf = ByteBuffer.wrap(bytesToWrite); writer.scheduleForWriting(buf, false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_ID_ERROR); } /** * Server sends invalid CANCEL_PUSH stream ID */ @Test public void testInvalidCancelPushId() throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream controlStream; controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); var writer = controlStream.connectWriter(scheduler); // control stream, settings frame, length 0, CANCEL_PUSH, id = MAX_VL_INTEGER byte[] bytesToWrite = new byte[] { 0, 4, 0, 3, 8, (byte)255, (byte)255, (byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255}; ByteBuffer buf = ByteBuffer.wrap(bytesToWrite); writer.scheduleForWriting(buf, false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_ID_ERROR); } /** * Server sends unexpected frame on push stream */ @ParameterizedTest @MethodSource("invalidPushFrames") public void testUnexpectedPushFrame(byte[] frame) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream pushStream; pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); // write PUSH_PROMISE frame s.outputStream().write(valid_push_promise); var writer = pushStream.connectWriter(scheduler); // push stream, id 0 byte[] bytesToWrite = new byte[] { 1, 0 }; ByteBuffer buf = ByteBuffer.allocate(2 + frame.length); buf.put(bytesToWrite); buf.put(frame); buf.flip(); writer.scheduleForWriting(buf, false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerPushError(errorCF, Http3Error.H3_FRAME_UNEXPECTED); } /** * Server sends malformed frame on push stream */ @ParameterizedTest @MethodSource("malformedResponseHeadersFrames") public void testMalformedPushStreamFrame(byte[] frame, int bytes) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream pushStream; pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); // write PUSH_PROMISE frame s.outputStream().write(valid_push_promise); var writer = pushStream.connectWriter(scheduler); // push stream, id 0 byte[] bytesToWrite = new byte[] { 1, 0 }; ByteBuffer buf = ByteBuffer.allocate(2 + frame.length); buf.put(bytesToWrite); buf.put(frame); buf.flip(); writer.scheduleForWriting(buf, false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerPushError(errorCF, frame.length == 2 ? Http3Error.H3_FRAME_ERROR : Http3Error.QPACK_DECOMPRESSION_FAILED); } /** * Server sends malformed frame on push stream */ @ParameterizedTest @MethodSource("malformedPushPromiseFrames") public void testMalformedPushPromiseFrame(byte[] frame, int bytes) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { // write PUSH_PROMISE frame s.outputStream().write(frame); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerPushError(errorCF, frame.length <= 3 ? Http3Error.H3_FRAME_ERROR : Http3Error.QPACK_DECOMPRESSION_FAILED); } /** * Server reuses push stream ID */ @Test public void testDuplicatePushStream() throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream pushStream, pushStream2; pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); pushStream2 = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); // write PUSH_PROMISE frame s.outputStream().write(valid_push_promise); var writer = pushStream.connectWriter(scheduler); // push stream, id 0 byte[] bytesToWrite = new byte[] { 1, 0 }; writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); writer = pushStream2.connectWriter(scheduler); // push stream, id 0 writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerPushError(errorCF, Http3Error.H3_ID_ERROR); } /** * Server sends push promise with ID > MAX_PUSH_ID */ @Test public void testInvalidPushPromiseId() throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { // write PUSH_PROMISE frame s.outputStream().write(huge_id_push_promise); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_ID_ERROR); } /** * Server opens a push stream ID > MAX_PUSH_ID */ @Test public void testInvalidPushStreamId() throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream pushStream; pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); var writer = pushStream.connectWriter(scheduler); // push stream, id MAX_VL_INTEGER byte[] bytesToWrite = new byte[] { 1, (byte)255, (byte)255, (byte)255, (byte)255, (byte)255, (byte)255, (byte)255, (byte)255 }; ByteBuffer buf = ByteBuffer.wrap(bytesToWrite); writer.scheduleForWriting(buf, false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_ID_ERROR); } /** * Server sends unexpected frame on response stream */ @ParameterizedTest @MethodSource("invalidResponseFrames") public void testUnexpectedResponseFrame(byte[] frame) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { s.outputStream().write(frame); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_FRAME_UNEXPECTED); } /** * Server sends malformed headers frame on response stream */ @ParameterizedTest @MethodSource("malformedResponseHeadersFrames") public void testMalformedResponseFrame(byte[] frame, int bytes) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { s.outputStream().write(frame); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, frame.length == 2 ? Http3Error.H3_FRAME_ERROR : Http3Error.QPACK_DECOMPRESSION_FAILED); } /** * Server truncates a frame on the response stream */ @ParameterizedTest @MethodSource("truncatedResponseFrames") public void testTruncatedResponseFrame(byte[] frame, int bytes) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { try (OutputStream outputStream = s.outputStream()) { outputStream.write(frame); } // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_FRAME_ERROR); } /** * Server truncates a frame on the control stream */ @ParameterizedTest @MethodSource("truncatedControlFrames") public void testTruncatedControlFrame(byte[] frame, int bytes) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream controlStream; controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var controlscheduler = SequentialScheduler.lockingScheduler(() -> {}); var writer = controlStream.connectWriter(controlscheduler); writer.scheduleForWriting(ByteBuffer.wrap(frame), true); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); // H3_CLOSED_CRITICAL_STREAM is also acceptable here triggerError(errorCF, Http3Error.H3_FRAME_ERROR, Http3Error.H3_CLOSED_CRITICAL_STREAM); } /** * Server truncates a frame on the push stream */ @ParameterizedTest @MethodSource("truncatedResponseFrames") public void testTruncatedPushStreamFrame(byte[] frame, int bytes) throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream pushStream; pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); // write PUSH_PROMISE frame s.outputStream().write(valid_push_promise); var writer = pushStream.connectWriter(scheduler); // push stream, id 0 byte[] bytesToWrite = new byte[] { 1, 0 }; ByteBuffer buf = ByteBuffer.allocate(2 + frame.length); buf.put(bytesToWrite); buf.put(frame); buf.flip(); writer.scheduleForWriting(buf, true); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerPushError(errorCF, Http3Error.H3_FRAME_ERROR); } /** * Server sends a settings frame with reserved HTTP2 settings */ @Test public void testReservedSettingsFrames() throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream controlStream; controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); var writer = controlStream.connectWriter(scheduler); // control stream, settings frame, length 2, setting 4 = 0 byte[] bytesToWrite = new byte[] { 0, 4, 2, 4, 0 }; writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_SETTINGS_ERROR); } /** * Server sends a stateless reset */ @Test public void testStatelessReset() throws Exception { server.setHandler((c, s)-> { // stateless reset QuicConnectionId localConnId = c.localConnectionId(); ByteBuffer resetDatagram = c.endpoint().idFactory().statelessReset(localConnId.asReadOnlyBuffer(), 43); ((DatagramChannel)c.channel()).send(resetDatagram, c.peerAddress()); // ignore the request stream; we're expecting the client to close the connection. // The server won't receive any notification from the client here. // The connection will leak. }); HttpClient client = getHttpClient(); try { HttpRequest request = getRequest(); final HttpResponse<Void> response = client.sendAsync( request, BodyHandlers.discarding()) .get(Utils.adjustTimeout(10), TimeUnit.SECONDS); fail("Expected the request to fail, got " + response); } catch (Exception e) { final String expectedMsg = "stateless reset from peer"; if (e.getMessage() != null && e.getMessage().contains(expectedMsg)) { // got the expected exception return; } // unexpected exception, throw it back throw e; } finally { client.shutdownNow(); } } /** * Server opens a bidi stream */ @Test @Disabled("BiDi streams are rejected by H3 client at QUIC level") public void testBidiStream() throws Exception { CompletableFuture<TerminationCause> errorCF = new CompletableFuture<>(); server.setHandler((c, s)-> { QuicSenderStream bidiStream; bidiStream = c.openNewLocalBidiStream(Duration.ZERO).resultNow(); var scheduler = SequentialScheduler.lockingScheduler(() -> { }); var writer = bidiStream.connectWriter(scheduler); // some data byte[] bytesToWrite = new byte[] { 0, 4, 2, 4, 0 }; writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); // ignore the request stream; we're expecting the client to close the connection completeUponTermination(c, errorCF); }); triggerError(errorCF, Http3Error.H3_STREAM_CREATION_ERROR); } /** * Server closes the connection with a known QUIC error */ @Test public void testConnectionCloseQUIC() throws Exception { server.setHandler((c, s)-> { TerminationCause tc = TerminationCause.forException( new QuicTransportException("ignored", null, 0, QuicTransportErrors.INTERNAL_ERROR) ); tc.peerVisibleReason("testtest"); c.connectionTerminator().terminate(tc); }); triggerClose("INTERNAL_ERROR", "testtest"); } /** * Server closes the connection with a known crypto error */ @Test public void testConnectionCloseCryptoQUIC() throws Exception { server.setHandler((c, s)-> { TerminationCause tc = TerminationCause.forException( new QuicTransportException("ignored", null, 0, QuicTransportErrors.CRYPTO_ERROR.from() + 80 /*Alert.INTERNAL_ERROR.id*/, null) ); tc.peerVisibleReason("testtest"); c.connectionTerminator().terminate(tc); }); triggerClose("CRYPTO_ERROR", "internal_error", "testtest"); } /** * Server closes the connection with an unknown crypto error */ @Test public void testConnectionCloseUnknownCryptoQUIC() throws Exception { server.setHandler((c, s)-> { TerminationCause tc = TerminationCause.forException( new QuicTransportException("ignored", null, 0, QuicTransportErrors.CRYPTO_ERROR.from() + 5, null) ); tc.peerVisibleReason("testtest"); c.connectionTerminator().terminate(tc); }); triggerClose("CRYPTO_ERROR", "5", "testtest"); } /** * Server closes the connection with an unknown QUIC error */ @Test public void testConnectionCloseUnknownQUIC() throws Exception { server.setHandler((c, s)-> { TerminationCause tc = TerminationCause.forException( new QuicTransportException("ignored", null, 0, QuicTransportErrors.CRYPTO_ERROR.to() + 1 /*0x200*/, null) ); tc.peerVisibleReason("testtest"); c.connectionTerminator().terminate(tc); }); triggerClose("200", "testtest"); } /** * Server closes the connection with a known H3 error */ @Test public void testConnectionCloseH3() throws Exception { server.setHandler((c, s)-> { TerminationCause tc = TerminationCause.appLayerClose(Http3Error.H3_EXCESSIVE_LOAD.code()); tc.peerVisibleReason("testtest"); c.connectionTerminator().terminate(tc); }); triggerClose("H3_EXCESSIVE_LOAD", "testtest"); } /** * Server closes the connection with an unknown H3 error */ @Test public void testConnectionCloseH3Unknown() throws Exception { server.setHandler((c, s)-> { TerminationCause tc = TerminationCause.appLayerClose(0x1f21); tc.peerVisibleReason("testtest"); c.connectionTerminator().terminate(tc); }); triggerClose("1F21", "testtest"); } private void triggerClose(String... reasons) throws Exception { HttpClient client = getHttpClient(); try { HttpRequest request = getRequest(); final HttpResponse<Void> response = client.sendAsync( request, BodyHandlers.discarding()) .get(Utils.adjustTimeout(10), TimeUnit.SECONDS); fail("Expected the request to fail, got " + response); } catch (ExecutionException e) { System.out.println("Client exception [expected]: " + e); var cause = e.getCause(); assertTrue(cause instanceof IOException, "Expected IOException"); for (String reason : reasons) { assertTrue(cause.getMessage().contains(reason), cause.getMessage() + " does not contain " + reason); } } finally { client.shutdownNow(); } } private void triggerError(CompletableFuture<TerminationCause> errorCF, Http3Error expected) throws Exception { HttpClient client = getHttpClient(); try { HttpRequest request = getRequest(); final HttpResponse<Void> response = client.sendAsync( request, BodyHandlers.discarding()) .get(Utils.adjustTimeout(20), TimeUnit.SECONDS); fail("Expected the request to fail, got " + response); } catch (ExecutionException e) { System.out.println("Client exception [expected]: " + e); var cause = e.getCause(); assertTrue(cause instanceof ProtocolException, "Expected ProtocolException"); TerminationCause terminationCause = errorCF.get(Utils.adjustTimeout(10), TimeUnit.SECONDS); System.out.println("Server reason: \"" + terminationCause.getPeerVisibleReason()+'"'); final long actual = terminationCause.getCloseCode(); // expected assertEquals(expected.code(), actual, "Expected " + toHexString(expected) + " got 0x" + Long.toHexString(actual)); } finally { client.shutdownNow(); } } private void triggerError(CompletableFuture<TerminationCause> errorCF, Http3Error... expected) throws Exception { HttpClient client = getHttpClient(); try { HttpRequest request = getRequest(); final HttpResponse<Void> response = client.sendAsync( request, BodyHandlers.discarding()) .get(Utils.adjustTimeout(10), TimeUnit.SECONDS); fail("Expected the request to fail, got " + response); } catch (ExecutionException e) { System.out.println("Client exception [expected]: " + e); var cause = e.getCause(); assertTrue(cause instanceof ProtocolException, "Expected ProtocolException"); TerminationCause terminationCause = errorCF.get(Utils.adjustTimeout(10), TimeUnit.SECONDS); System.out.println("Server reason: \"" + terminationCause.getPeerVisibleReason()+'"'); final long actual = terminationCause.getCloseCode(); // expected Optional<Http3Error> h3Actual = Http3Error.fromCode(actual); assertTrue(h3Actual.isPresent(), "Expected HTTP3 error, got 0x" + Long.toHexString(actual)); Set<Http3Error> expectedErrors = Set.of(expected); assertTrue(expectedErrors.contains(h3Actual.get()), "Expected "+expectedErrors+ ", got: "+h3Actual); } finally { client.shutdownNow(); } } private void triggerPushError(CompletableFuture<TerminationCause> errorCF, Http3Error http3Error) throws Exception { HttpClient client = getHttpClient(); // close might block; use shutdownNow instead try { HttpRequest request = getRequest(); final HttpResponse<Void> response = client.sendAsync( request, BodyHandlers.discarding(), (initiatingRequest, pushPromiseRequest, acceptor) -> acceptor.apply(BodyHandlers.discarding()) ).get(Utils.adjustTimeout(10), TimeUnit.SECONDS); fail("Expected the request to fail, got " + response); } catch (ExecutionException e) { System.out.println("Client exception [expected]: " + e); var cause = e.getCause(); assertTrue(cause instanceof ProtocolException, "Expected ProtocolException"); TerminationCause terminationCause = errorCF.get(Utils.adjustTimeout(10), TimeUnit.SECONDS); System.out.println("Server reason: \"" + terminationCause.getPeerVisibleReason()+'"'); final long actual = terminationCause.getCloseCode(); // expected assertEquals(http3Error.code(), actual, "Expected " + toHexString(http3Error) + " got 0x" + Long.toHexString(actual)); } finally { client.shutdownNow(); } } private HttpRequest getRequest() throws URISyntaxException { final URI reqURI = new URI(requestURIBase + "/hello"); final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) .version(HTTP_3) .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); return reqBuilder.build(); } private HttpClient getHttpClient() { final HttpClient client = newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .version(HTTP_3) .sslContext(sslContext).build(); return client; } private static String toHexString(final Http3Error error) { return error.name() + "(0x" + Long.toHexString(error.code()) + ")"; } private static void completeUponTermination(final QuicServerConnection serverConnection, final CompletableFuture<TerminationCause> cf) { serverConnection.futureTerminationCause().handle( (r,t) -> t != null ? cf.completeExceptionally(t) : cf.complete(r)); } }