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
730
test/jdk/java/net/httpclient/quic/AckElicitingTest.java
Normal file
730
test/jdk/java/net/httpclient/quic/AckElicitingTest.java
Normal file
|
|
@ -0,0 +1,730 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 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 java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.IntFunction;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jdk.internal.net.http.quic.PeerConnectionId;
|
||||
import jdk.internal.net.quic.QuicKeyUnavailableException;
|
||||
import jdk.internal.net.quic.QuicOneRttContext;
|
||||
import jdk.internal.net.quic.QuicVersion;
|
||||
import jdk.internal.net.http.quic.frames.AckFrame;
|
||||
import jdk.internal.net.http.quic.frames.AckFrame.AckRange;
|
||||
import jdk.internal.net.http.quic.frames.ConnectionCloseFrame;
|
||||
import jdk.internal.net.http.quic.frames.CryptoFrame;
|
||||
import jdk.internal.net.http.quic.frames.DataBlockedFrame;
|
||||
import jdk.internal.net.http.quic.frames.HandshakeDoneFrame;
|
||||
import jdk.internal.net.http.quic.frames.MaxDataFrame;
|
||||
import jdk.internal.net.http.quic.frames.MaxStreamDataFrame;
|
||||
import jdk.internal.net.http.quic.frames.MaxStreamsFrame;
|
||||
import jdk.internal.net.http.quic.frames.NewConnectionIDFrame;
|
||||
import jdk.internal.net.http.quic.frames.NewTokenFrame;
|
||||
import jdk.internal.net.http.quic.frames.PaddingFrame;
|
||||
import jdk.internal.net.http.quic.frames.PathChallengeFrame;
|
||||
import jdk.internal.net.http.quic.frames.PathResponseFrame;
|
||||
import jdk.internal.net.http.quic.frames.PingFrame;
|
||||
import jdk.internal.net.http.quic.frames.QuicFrame;
|
||||
import jdk.internal.net.http.quic.frames.ResetStreamFrame;
|
||||
import jdk.internal.net.http.quic.frames.RetireConnectionIDFrame;
|
||||
import jdk.internal.net.http.quic.frames.StopSendingFrame;
|
||||
import jdk.internal.net.http.quic.frames.StreamDataBlockedFrame;
|
||||
import jdk.internal.net.http.quic.frames.StreamFrame;
|
||||
import jdk.internal.net.http.quic.frames.StreamsBlockedFrame;
|
||||
import jdk.internal.net.http.quic.packets.QuicPacketDecoder;
|
||||
import jdk.internal.net.http.quic.packets.QuicPacketEncoder;
|
||||
import jdk.internal.net.http.quic.CodingContext;
|
||||
import jdk.internal.net.http.quic.QuicConnectionId;
|
||||
import jdk.internal.net.http.quic.packets.QuicPacket;
|
||||
import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace;
|
||||
import jdk.internal.net.http.quic.packets.QuicPacket.PacketType;
|
||||
import jdk.internal.net.quic.QuicTLSEngine;
|
||||
import jdk.internal.net.quic.QuicTransportException;
|
||||
import jdk.internal.net.quic.QuicTransportParametersConsumer;
|
||||
import jdk.test.lib.RandomFactory;
|
||||
|
||||
import javax.crypto.AEADBadTagException;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSession;
|
||||
|
||||
import static jdk.internal.net.http.quic.frames.QuicFrame.*;
|
||||
import static jdk.internal.net.http.quic.frames.ConnectionCloseFrame.CONNECTION_CLOSE_VARIANT;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @summary tests the logic to decide whether a packet or
|
||||
* a frame is ACK-eliciting.
|
||||
* @library /test/lib
|
||||
* @run junit AckElicitingTest
|
||||
* @run junit/othervm -Dseed=-7997973196290088038 AckElicitingTest
|
||||
*/
|
||||
public class AckElicitingTest {
|
||||
|
||||
static final Random RANDOM = RandomFactory.getRandom();
|
||||
|
||||
private static class DummyQuicTLSEngine implements QuicTLSEngine {
|
||||
@Override
|
||||
public HandshakeState getHandshakeState() {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTLSHandshakeComplete() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeySpace getCurrentSendKeySpace() {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
@Override
|
||||
public boolean keysAvailable(KeySpace keySpace) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void discardKeys(KeySpace keySpace) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocalQuicTransportParameters(ByteBuffer params) {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restartHandshake() throws IOException {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRemoteQuicTransportParametersConsumer(QuicTransportParametersConsumer consumer) {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deriveInitialKeys(QuicVersion version, ByteBuffer connectionId) { }
|
||||
|
||||
@Override
|
||||
public int getHeaderProtectionSampleSize(KeySpace keySpace) {
|
||||
return 0;
|
||||
}
|
||||
@Override
|
||||
public ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, boolean incoming, ByteBuffer sample) {
|
||||
return ByteBuffer.allocate(5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAuthTagSize() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encryptPacket(KeySpace keySpace, long packetNumber,
|
||||
IntFunction<ByteBuffer> headerGenerator,
|
||||
ByteBuffer packetPayload, ByteBuffer output)
|
||||
throws QuicKeyUnavailableException, QuicTransportException {
|
||||
// this dummy QUIC TLS engine doesn't do any encryption.
|
||||
// we just copy over the raw packet payload into the output buffer
|
||||
output.put(packetPayload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decryptPacket(KeySpace keySpace, long packetNumber, int keyPhase,
|
||||
ByteBuffer packet, int headerLength, ByteBuffer output) {
|
||||
packet.position(packet.position() + headerLength);
|
||||
output.put(packet);
|
||||
}
|
||||
@Override
|
||||
public void signRetryPacket(QuicVersion quicVersion,
|
||||
ByteBuffer originalConnectionId, ByteBuffer packet, ByteBuffer output) {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
@Override
|
||||
public void verifyRetryPacket(QuicVersion quicVersion,
|
||||
ByteBuffer originalConnectionId, ByteBuffer packet) throws AEADBadTagException {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
@Override
|
||||
public ByteBuffer getHandshakeBytes(KeySpace keySpace) {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
@Override
|
||||
public void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
@Override
|
||||
public Runnable getDelegatedTask() {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
@Override
|
||||
public boolean tryMarkHandshakeDone() {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
@Override
|
||||
public boolean tryReceiveHandshakeDone() {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<QuicVersion> getSupportedQuicVersions() {
|
||||
return Set.of(QuicVersion.QUIC_V1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUseClientMode(boolean mode) {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getUseClientMode() {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLParameters getSSLParameters() {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSSLParameters(SSLParameters sslParameters) {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApplicationProtocol() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLSession getSession() { throw new AssertionError("should not come here!"); }
|
||||
|
||||
@Override
|
||||
public SSLSession getHandshakeSession() { throw new AssertionError("should not come here!"); }
|
||||
|
||||
@Override
|
||||
public void versionNegotiated(QuicVersion quicVersion) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOneRttContext(QuicOneRttContext ctx) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
private static final QuicTLSEngine TLS_ENGINE = new DummyQuicTLSEngine();
|
||||
private static abstract class TestCodingContext implements CodingContext {
|
||||
TestCodingContext() { }
|
||||
@Override
|
||||
public int writePacket(QuicPacket packet, ByteBuffer buffer) {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
@Override
|
||||
public QuicPacket parsePacket(ByteBuffer src) throws IOException {
|
||||
throw new AssertionError("should not come here!");
|
||||
}
|
||||
@Override
|
||||
public boolean verifyToken(QuicConnectionId destinationID, byte[] token) {
|
||||
return true;
|
||||
}
|
||||
@Override
|
||||
public QuicTLSEngine getTLSEngine() {
|
||||
return TLS_ENGINE;
|
||||
}
|
||||
}
|
||||
|
||||
static final int CIDLEN = RANDOM.nextInt(5, QuicConnectionId.MAX_CONNECTION_ID_LENGTH + 1);
|
||||
|
||||
private static final TestCodingContext CONTEXT = new TestCodingContext() {
|
||||
|
||||
@Override
|
||||
public long largestProcessedPN(PacketNumberSpace packetSpace) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long largestAckedPN(PacketNumberSpace packetSpace) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int connectionIdLength() {
|
||||
return CIDLEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QuicConnectionId originalServerConnId() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A record to store all the input of a given test case.
|
||||
* @param type a concrete frame type or packet type
|
||||
* @param describer a function to describe the {@code obj} instance
|
||||
* for tracing/diagnosis purposes
|
||||
* @param ackEliciting the function we want to test. This is either
|
||||
* {@link QuicFrame#isAckEliciting()
|
||||
* QuicFrame::isAckEliciting} or
|
||||
* {@link QuicPacket#isAckEliciting()
|
||||
* QuicPacket::isAckEliciting}
|
||||
* @param obj the instance on which to call the {@code ackEliciting}
|
||||
* function.
|
||||
* @param expected the expected result of calling
|
||||
* {@code obj.ackEliciting()}
|
||||
* @param <T> A concrete subclass of {@link QuicFrame} or {@link QuicPacket}
|
||||
*/
|
||||
record TestCase<T>(Class<? extends T> type,
|
||||
Function<T, String> describer,
|
||||
Predicate<? super T> ackEliciting,
|
||||
T obj,
|
||||
boolean expected) {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// shorter & better toString than the default
|
||||
return "%s(%s)"
|
||||
.formatted(type.getSimpleName(), describer.apply(obj));
|
||||
}
|
||||
|
||||
private static String describeFrame(QuicFrame frame) {
|
||||
long type = frame.getTypeField();
|
||||
return HexFormat.of().toHexDigits((byte)type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of {@code TestCase} for a concrete frame type
|
||||
* @param type the concrete frame class
|
||||
* @param frame the concrete instance
|
||||
* @param expected whether {@link QuicFrame#isAckEliciting()}
|
||||
* should return true for that instance.
|
||||
* @param <T> a concrete subclass of {@code QuicFrame}
|
||||
* @return a new instance of {@code TestCase}
|
||||
*/
|
||||
public static <T extends QuicFrame> TestCase<T>
|
||||
of(Class<? extends T> type, T frame, boolean expected) {
|
||||
return new TestCase<>(type, TestCase::describeFrame,
|
||||
QuicFrame::isAckEliciting, frame, expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of {@code TestCase} for a concrete frame type
|
||||
* @param frame the concrete frame instance
|
||||
* @param expected whether {@link QuicFrame#isAckEliciting()}
|
||||
* should return true for that instance.
|
||||
* @param <T> a concrete subclass of {@code QuicFrame}
|
||||
* @return a new instance of {@code TestCase}
|
||||
*/
|
||||
public static <T extends QuicFrame> TestCase<T> of(T frame, boolean expected) {
|
||||
return new TestCase<>((Class<T>)frame.getClass(),
|
||||
TestCase::describeFrame,
|
||||
QuicFrame::isAckEliciting,
|
||||
frame, expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of {@code TestCase} for a concrete packet type
|
||||
* @param packet the concrete packet instance
|
||||
* @param expected whether {@link QuicPacket#isAckEliciting()}
|
||||
* should return true for that instance.
|
||||
* @param <T> a concrete subclass of {@code QuicPacket}
|
||||
* @return a new instance of {@code TestCase}
|
||||
*/
|
||||
public static <T extends QuicPacket> TestCase<T> of(T packet, boolean expected) {
|
||||
return new TestCase<T>((Class<T>)packet.getClass(),
|
||||
(p) -> p.frames().stream()
|
||||
.map(Object::getClass)
|
||||
.map(Class::getSimpleName)
|
||||
.collect(Collectors.joining(", ")),
|
||||
QuicPacket::isAckEliciting,
|
||||
packet, expected);
|
||||
}
|
||||
}
|
||||
|
||||
// convenient alias to shorten lines in data providers
|
||||
private static <T extends QuicFrame> TestCase<T> of(T frame, boolean ackEliciting) {
|
||||
return TestCase.of(frame, ackEliciting);
|
||||
}
|
||||
|
||||
// convenient alias to shorten lines in data providers
|
||||
private static <T extends QuicPacket> TestCase<T> of(T packet, boolean ackEliciting) {
|
||||
return TestCase.of(packet, ackEliciting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the given frame type, populated with
|
||||
* dummy values.
|
||||
* @param frameClass the frame type
|
||||
* @param <T> a concrete subclass of {@code QuicFrame}
|
||||
* @return a new instance of the given concrete class.
|
||||
*/
|
||||
static <T extends QuicFrame> T newFrame(Class<T> frameClass) {
|
||||
var frameType = QuicFrame.frameTypeOf(frameClass);
|
||||
if (frameType == CONNECTION_CLOSE) {
|
||||
if (RANDOM.nextBoolean()) {
|
||||
frameType = CONNECTION_CLOSE_VARIANT;
|
||||
}
|
||||
}
|
||||
long streamId = 4;
|
||||
long largestAcknowledge = 3;
|
||||
long ackDelay = 20;
|
||||
long gap = 0;
|
||||
long range = largestAcknowledge;
|
||||
long offset = 0;
|
||||
boolean fin = false;
|
||||
int length = 10;
|
||||
long errorCode = 1;
|
||||
long finalSize = 10;
|
||||
int size = 3;
|
||||
long maxData = 10;
|
||||
boolean maxStreamsBidi = true;
|
||||
long maxStreams = 100;
|
||||
long maxStreamData = 10;
|
||||
long sequenceNumber = 4;
|
||||
long retirePriorTo = 3;
|
||||
String reason = "none";
|
||||
long errorFrameType = ACK;
|
||||
int pathChallengeLen = PathChallengeFrame.LENGTH;
|
||||
int pathResponseLen = PathResponseFrame.LENGTH;
|
||||
var frame = switch (frameType) {
|
||||
case ACK -> new AckFrame(largestAcknowledge, ackDelay, List.of(new AckRange(gap, range)));
|
||||
case STREAM -> new StreamFrame(streamId, offset, length, fin, ByteBuffer.allocate(length));
|
||||
case RESET_STREAM -> new ResetStreamFrame(streamId, errorCode, finalSize);
|
||||
case PADDING -> new PaddingFrame(size);
|
||||
case PING -> new PingFrame();
|
||||
case STOP_SENDING -> new StopSendingFrame(streamId, errorCode);
|
||||
case CRYPTO -> new CryptoFrame(offset, length, ByteBuffer.allocate(length));
|
||||
case NEW_TOKEN -> new NewTokenFrame(ByteBuffer.allocate(length));
|
||||
case DATA_BLOCKED -> new DataBlockedFrame(maxData);
|
||||
case MAX_DATA -> new MaxDataFrame(maxData);
|
||||
case MAX_STREAMS -> new MaxStreamsFrame(maxStreamsBidi, maxStreams);
|
||||
case MAX_STREAM_DATA -> new MaxStreamDataFrame(streamId, maxStreamData);
|
||||
case STREAM_DATA_BLOCKED -> new StreamDataBlockedFrame(streamId, maxStreamData);
|
||||
case STREAMS_BLOCKED -> new StreamsBlockedFrame(maxStreamsBidi, maxStreams);
|
||||
case NEW_CONNECTION_ID -> new NewConnectionIDFrame(sequenceNumber, retirePriorTo,
|
||||
ByteBuffer.allocate(length), ByteBuffer.allocate(16));
|
||||
case RETIRE_CONNECTION_ID -> new RetireConnectionIDFrame(sequenceNumber);
|
||||
case PATH_CHALLENGE -> new PathChallengeFrame(ByteBuffer.allocate(pathChallengeLen));
|
||||
case PATH_RESPONSE -> new PathResponseFrame(ByteBuffer.allocate(pathResponseLen));
|
||||
case CONNECTION_CLOSE -> new ConnectionCloseFrame(errorCode, errorFrameType, reason);
|
||||
case CONNECTION_CLOSE_VARIANT -> new ConnectionCloseFrame(errorCode, reason);
|
||||
case HANDSHAKE_DONE -> new HandshakeDoneFrame();
|
||||
default -> throw new IllegalArgumentException("Unrecognised frame");
|
||||
};
|
||||
return frameClass.cast(frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of {@code TestCase} to test all possible concrete
|
||||
* subclasses of {@code QuicFrame}.
|
||||
*
|
||||
* @return a list of {@code TestCase} to test all possible concrete
|
||||
* subclasses of {@code QuicFrame}
|
||||
*/
|
||||
static List<TestCase<? extends QuicFrame>> createFramesTests() {
|
||||
List<TestCase<? extends QuicFrame>> frames = new ArrayList<>();
|
||||
frames.add(of(newFrame(AckFrame.class), false));
|
||||
frames.add(of(newFrame(ConnectionCloseFrame.class), false));
|
||||
frames.add(of(newFrame(PaddingFrame.class), false));
|
||||
|
||||
for (var frameType : QuicFrame.class.getPermittedSubclasses()) {
|
||||
if (frameType == AckFrame.class) continue;
|
||||
if (frameType == ConnectionCloseFrame.class) continue;
|
||||
if (frameType == PaddingFrame.class) continue;
|
||||
Class<? extends QuicFrame> quicFrameClass = (Class<? extends QuicFrame>)frameType;
|
||||
frames.add(of(newFrame(quicFrameClass), true));
|
||||
}
|
||||
|
||||
return List.copyOf(frames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code QuicPacket} containing the given list of frames.
|
||||
* @param frames a list of frames
|
||||
* @return a new instance of {@code QuicPacket}
|
||||
*/
|
||||
static QuicPacket createPacket(List<QuicFrame> frames) {
|
||||
PacketType[] values = PacketType.values();
|
||||
int index = PacketType.NONE.ordinal();
|
||||
while (index == PacketType.NONE.ordinal()) {
|
||||
index = RANDOM.nextInt(0, values.length);
|
||||
}
|
||||
PacketType packetType = values[index];
|
||||
QuicPacketEncoder encoder = QuicPacketEncoder.of(QuicVersion.QUIC_V1);
|
||||
byte[] scid = new byte[CIDLEN];
|
||||
RANDOM.nextBytes(scid);
|
||||
byte[] dcid = new byte[CIDLEN];
|
||||
RANDOM.nextBytes(dcid);
|
||||
QuicConnectionId source = new PeerConnectionId(scid);
|
||||
QuicConnectionId dest = new PeerConnectionId(dcid);
|
||||
long largestAckedPacket = CONTEXT.largestAckedPN(packetType);
|
||||
QuicPacket packet = switch (packetType) {
|
||||
case NONE -> throw new AssertionError("should not come here");
|
||||
// TODO: add more packet types
|
||||
default -> encoder.newInitialPacket(source, dest, null, largestAckedPacket + 1 ,
|
||||
largestAckedPacket, frames, CONTEXT);
|
||||
};
|
||||
return packet;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a random instance of {@code QuicPacket} containing a
|
||||
* pseudo random list of concrete {@link QuicFrame} instances.
|
||||
* @param ackEliciting whether the returned packet should be
|
||||
* ack eliciting.
|
||||
* @return a new QuicPacket
|
||||
*/
|
||||
static QuicPacket createPacket(boolean ackEliciting) {
|
||||
List<QuicFrame> frames = new ArrayList<>();
|
||||
int mincount = ackEliciting ? 1 : 0;
|
||||
int ackCount = RANDOM.nextInt(mincount, 5);
|
||||
int nackCount = RANDOM.nextInt(0, 10);
|
||||
|
||||
// TODO: maybe refactor this to make sure the frame
|
||||
// we use are compatible with the packet type.
|
||||
List<Class<? extends QuicFrame>> noAckFrames = List.of(AckFrame.class,
|
||||
PaddingFrame.class, ConnectionCloseFrame.class);
|
||||
for (int i=0; i < nackCount ; i++) {
|
||||
frames.add(newFrame(noAckFrames.get(i % noAckFrames.size())));
|
||||
}
|
||||
if (ackEliciting) {
|
||||
// TODO: maybe refactor this to make sure the frame
|
||||
// we use are compatible with the packet type.
|
||||
Class<?>[] frameClasses = QuicFrame.class.getPermittedSubclasses();
|
||||
for (int i=0; i < ackCount; i++) {
|
||||
Class<?> selected;
|
||||
do {
|
||||
int fx = RANDOM.nextInt(0, frameClasses.length);
|
||||
selected = frameClasses[fx];
|
||||
} while (noAckFrames.contains(selected));
|
||||
frames.add(newFrame((Class<? extends QuicFrame>) selected));
|
||||
}
|
||||
}
|
||||
if (!ackEliciting || RANDOM.nextBoolean()) {
|
||||
// if !ackEliciting we always shuffle.
|
||||
// Otherwise, we only shuffle half the time.
|
||||
Collections.shuffle(frames, RANDOM);
|
||||
}
|
||||
return createPacket(mergeConsecutivePaddingFrames(frames));
|
||||
}
|
||||
|
||||
private static List<QuicFrame> mergeConsecutivePaddingFrames(List<QuicFrame> frames) {
|
||||
var iterator = frames.listIterator();
|
||||
QuicFrame previous = null;
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
var frame = iterator.next();
|
||||
if (previous instanceof PaddingFrame prevPad
|
||||
&& frame instanceof PaddingFrame nextPad) {
|
||||
int previousIndex = iterator.previousIndex();
|
||||
QuicFrame merged = new PaddingFrame(prevPad.size() + nextPad.size());
|
||||
frames.set(previousIndex, merged);
|
||||
iterator.remove();
|
||||
} else {
|
||||
previous = frame;
|
||||
}
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of {@code TestCase} to test random instances of
|
||||
* {@code QuicPacket} containing random instances of {@link QuicFrame}
|
||||
* @return a list of {@code TestCase} to test random instances of
|
||||
* {@code QuicPacket} containing random instances of {@link QuicFrame}
|
||||
*/
|
||||
static List<TestCase<? extends QuicPacket>> createPacketsTests() {
|
||||
List<TestCase<? extends QuicPacket>> packets = new ArrayList<>();
|
||||
packets.add(of(createPacket(List.of(newFrame(AckFrame.class))), false));
|
||||
packets.add(of(createPacket(List.of(newFrame(ConnectionCloseFrame.class))), false));
|
||||
packets.add(of(createPacket(List.of(newFrame(PaddingFrame.class))), false));
|
||||
var frames = new ArrayList<>(List.of(
|
||||
newFrame(PaddingFrame.class),
|
||||
newFrame(AckFrame.class),
|
||||
newFrame(ConnectionCloseFrame.class)));
|
||||
Collections.shuffle(frames, RANDOM);
|
||||
packets.add(of(createPacket(List.copyOf(frames)), false));
|
||||
|
||||
int maxPackets = RANDOM.nextInt(5, 11);
|
||||
for (int i = 0; i < maxPackets ; i++) {
|
||||
packets.add(of(createPacket(true), true));
|
||||
}
|
||||
return List.copyOf(packets);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A provider of test case to test
|
||||
* {@link QuicFrame#isAckEliciting()}.
|
||||
* @return test case to test
|
||||
* {@link QuicFrame#isAckEliciting()}
|
||||
*/
|
||||
public static Object[][] framesDataProvider() {
|
||||
return createFramesTests().stream()
|
||||
.map(List::of)
|
||||
.map(List::toArray)
|
||||
.toArray(Object[][]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* A provider of test case to test
|
||||
* {@link QuicPacket#isAckEliciting()}.
|
||||
* @return test case to test
|
||||
* {@link QuicPacket#isAckEliciting()}
|
||||
*/
|
||||
public static Object[][] packetsDataProvider() {
|
||||
return createPacketsTests().stream()
|
||||
.map(List::of)
|
||||
.map(List::toArray)
|
||||
.toArray(Object[][]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the behavior of {@link QuicFrame#isAckEliciting()}
|
||||
* with the given test case inputs.
|
||||
* @param test the test inputs
|
||||
* @param <T> a concrete subclass of QuicFrame
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("framesDataProvider")
|
||||
<T extends QuicFrame> void testFrames(TestCase<T> test) {
|
||||
testAckEliciting(test.type(),
|
||||
test.describer(),
|
||||
test.ackEliciting(),
|
||||
test.obj(),
|
||||
test.expected());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the behavior of {@link QuicPacket#isAckEliciting()}
|
||||
* with the given test case inputs.
|
||||
* @param test the test inputs
|
||||
* @param <T> a concrete subclass of QuickPacket
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("packetsDataProvider")
|
||||
<T extends QuicPacket> void testPackets(TestCase<T> test) {
|
||||
testAckEliciting(test.type(),
|
||||
test.describer(),
|
||||
test.ackEliciting(),
|
||||
test.obj(),
|
||||
test.expected());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Asserts that {@code ackEliciting.test(obj) == expected}.
|
||||
* @param type the concrete type of {@code obj}
|
||||
* @param describer a function to describe {@code obj}
|
||||
* @param ackEliciting the function being tested
|
||||
* @param obj the instance on which to call the function being tested
|
||||
* @param expected the expected result of {@code ackEliciting.test(obj)}
|
||||
* @param <T> the concrete class being tested
|
||||
*/
|
||||
private <T> void testAckEliciting(Class<? extends T> type,
|
||||
Function<? super T, String> describer,
|
||||
Predicate<? super T> ackEliciting,
|
||||
T obj,
|
||||
boolean expected) {
|
||||
System.out.printf("%ntestAckEliciting: %s(%s) - expecting %s%n",
|
||||
type.getSimpleName(),
|
||||
describer.apply(obj),
|
||||
expected);
|
||||
assertEquals(expected, ackEliciting.test(obj), describer.apply(obj));
|
||||
if (obj instanceof QuicFrame frame) {
|
||||
checkFrame(frame);
|
||||
} else if (obj instanceof QuicPacket packet) {
|
||||
checkPacket(packet);
|
||||
}
|
||||
}
|
||||
|
||||
// This is not a full-fledged test for frame encoding/decoding.
|
||||
// Just a smoke test to verify that the ACK-eliciting property
|
||||
// survives encoding/decoding
|
||||
private void checkFrame(QuicFrame frame) {
|
||||
int size = frame.size();
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||
System.out.println("Checking frame: " + frame.getClass());
|
||||
try {
|
||||
frame.encode(buffer);
|
||||
buffer.flip();
|
||||
var decoded = QuicFrame.decode(buffer);
|
||||
checkFrame(decoded, frame);
|
||||
} catch (QuicTransportException x) {
|
||||
throw new AssertionError(frame.getClass().getName(), x);
|
||||
}
|
||||
}
|
||||
|
||||
// This is not a full-fledged test for frame equality:
|
||||
// And we still need a proper decoding/encoding test for frames
|
||||
private void checkFrame(QuicFrame decoded, QuicFrame expected) {
|
||||
System.out.printf("Comparing frames: %s with %s%n",
|
||||
decoded.getClass().getSimpleName(),
|
||||
expected.getClass().getSimpleName());
|
||||
assertEquals(expected.getClass(), decoded.getClass());
|
||||
assertEquals(expected.size(), decoded.size());
|
||||
assertEquals(expected.getTypeField(), decoded.getTypeField());
|
||||
assertEquals(expected.isAckEliciting(), decoded.isAckEliciting());
|
||||
}
|
||||
|
||||
// This is not a full-fledged test for packet encoding/decoding.
|
||||
// Just a smoke test to verify that the ACK-eliciting property
|
||||
// survives encoding/decoding
|
||||
private void checkPacket(QuicPacket packet) {
|
||||
int size = packet.size();
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||
System.out.println("Checking packet: " + packet.getClass());
|
||||
try {
|
||||
var encoder = QuicPacketEncoder.of(QuicVersion.QUIC_V1);
|
||||
var decoder = QuicPacketDecoder.of(QuicVersion.QUIC_V1);
|
||||
encoder.encode(packet, buffer, CONTEXT);
|
||||
buffer.flip();
|
||||
var decoded = decoder.decode(buffer, CONTEXT);
|
||||
assertEquals(packet.size(), decoded.size());
|
||||
assertEquals(packet.packetType(), decoded.packetType());
|
||||
assertEquals(packet.payloadSize(), decoded.payloadSize());
|
||||
assertEquals(packet.isAckEliciting(), decoded.isAckEliciting());
|
||||
var frames = packet.frames();
|
||||
var decodedFrames = decoded.frames();
|
||||
assertEquals(frames.size(), decodedFrames.size());
|
||||
} catch (Exception x) {
|
||||
throw new AssertionError(packet.getClass().getName(), x);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue