/*
* Copyright © 2020, 2021, 2022, 2023 Peter Doornbosch
*
* This file is part of Kwik, an implementation of the QUIC protocol in Java.
*
* Kwik is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* Kwik 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 Lesser General Public License for
* more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
package net.luminis.quic.send;
import net.luminis.quic.*;
import net.luminis.quic.crypto.Aead;
import net.luminis.quic.crypto.ConnectionSecrets;
import net.luminis.quic.frame.CryptoFrame;
import net.luminis.quic.frame.PingFrame;
import net.luminis.quic.frame.StreamFrame;
import net.luminis.quic.log.NullLogger;
import net.luminis.quic.packet.ShortHeaderPacket;
import net.luminis.quic.test.FieldReader;
import net.luminis.quic.test.TestClock;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.mockito.ArgumentCaptor;
import net.luminis.quic.test.FieldSetter;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
class SenderImplTest extends AbstractSenderTest {
private TestClock clock;
private SenderImpl sender;
private GlobalPacketAssembler packetAssembler;
private DatagramSocket socket;
@BeforeEach
void initObjectUnderTest() throws Exception {
clock = new TestClock();
socket = mock(DatagramSocket.class);
InetSocketAddress peerAddress = new InetSocketAddress("example.com", 443);
QuicConnectionImpl connection = mock(QuicConnectionImpl.class);
when(connection.getDestinationConnectionId()).thenReturn(new byte[4]);
when(connection.getSourceConnectionId()).thenReturn(new byte[4]);
when(connection.getIdleTimer()).thenReturn(new IdleTimer(connection, new NullLogger()));
ConnectionSecrets connectionSecrets = mock(ConnectionSecrets.class);
Aead aead = TestUtils.createKeys();
when(connectionSecrets.getOwnAead(any(EncryptionLevel.class))).thenReturn(aead);
sender = new SenderImpl(clock, new VersionHolder(Version.getDefault()), 1200, socket, peerAddress, connection, 100, new NullLogger());
FieldSetter.setField(sender, sender.getClass().getDeclaredField("connectionSecrets"), connectionSecrets);
}
@Test
void whenAckWithDelayIsQueuedSenderIsWakedUpAfterDelay() {
// Given
sender.enableAllLevels();
// When
sender.sendAck(PnSpace.App, 50);
sender.packetProcessed(false);
// Then
long delay = sender.determineMaximumWaitTime();
assertThat(delay).isBetween(49L, 51L);
}
@Test
void senderStatisticsShouldWork() throws Exception {
ShortHeaderPacket packet1 = new ShortHeaderPacket(Version.getDefault(), new byte[4], new StreamFrame(0, new byte[1100], false));
ShortHeaderPacket packet2 = new ShortHeaderPacket(Version.getDefault(), new byte[4], new StreamFrame(0, new byte[11], false));
packet1.setPacketNumber(10);
sender.send(List.of(new SendItem(packet1)));
packet1.setPacketNumber(11);
packet2.setPacketNumber(12);
sender.send(List.of(new SendItem(packet1), new SendItem(packet2)));
assertThat(sender.getStatistics().datagramsSent()).isEqualTo(2);
assertThat(sender.getStatistics().packetsSent()).isEqualTo(3);
assertThat(sender.getStatistics().bytesSent()).isBetween(2200l, 2300l);
}
@Test
void addingProbeToDiscardedSpaceDiscardsIt() throws Exception {
// Given
SendRequestQueue[] senderQueues = (SendRequestQueue[]) new FieldReader(sender, sender.getClass().getDeclaredField("sendRequestQueue")).read();
sender.discard(PnSpace.Initial, "test");
// When
sender.sendProbe(EncryptionLevel.Initial);
// Then
assertThat(senderQueues[EncryptionLevel.Initial.ordinal()].hasProbe()).isFalse();
assertThat(senderQueues[EncryptionLevel.Handshake.ordinal()].hasProbe()).isFalse();
}
@Test
void whenAntiAmplificationLimitNotReachedAssemblerIsCalledWithNoLimit() throws Exception {
// Given
setupMockPacketAssember();
sender.setAntiAmplificationLimit(3 * 1200); // This is how it's initialized when client packet received
// When
sender.sendIfAny();
// Then verify
ArgumentCaptor packetSizeCaptor = ArgumentCaptor.forClass(Integer.class);
verify(packetAssembler, atLeastOnce()).assemble(anyInt(), packetSizeCaptor.capture(), any(byte[].class), any(byte[].class));
assertThat(packetSizeCaptor.getValue()).isLessThanOrEqualTo(3 * 1200);
}
@Test
void whenAntiAmplificationLimitIsReachedNothingIsSentAnymore() throws Exception {
// Given
sender.enableAllLevels();
sender.setAntiAmplificationLimit(3 * 1200); // This is how it's initialized when client packet received
for (int i = 0; i < 9; i++) {
sender.send(new StreamFrame(0, i * 1100, new byte[1100], false), EncryptionLevel.App);
}
sender.flush();
// When
sender.sendIfAny();
// Then (given fixed size of StreamFrames, only three packets will fit in the limit of 3 * 1200)
verify(socket, times(3)).send(any(DatagramPacket.class));
}
private void setupMockPacketAssember() throws NoSuchFieldException {
packetAssembler = mock(GlobalPacketAssembler.class);
when(packetAssembler.assemble(anyInt(), anyInt(), any(byte[].class), any(byte[].class))).thenReturn(List.of(new SendItem(new MockPacket(0, 1200, ""))));
when(packetAssembler.nextDelayedSendTime()).thenReturn(Optional.empty());
FieldSetter.setField(sender, sender.getClass().getDeclaredField("packetAssembler"), packetAssembler);
}
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void whenNothingIsQueuedNothingIsSentWhenPacketProcessedIsCalled() throws Exception {
// Given
// When
sender.packetProcessed(false);
sender.doLoopIteration();
// Then
verify(socket, never()).send(any(DatagramPacket.class));
}
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void whenPacketProcessedIsCalledQueuedFramesAreSent() throws Exception {
// Given
sender.send(new PingFrame(), EncryptionLevel.Handshake);
// When
sender.packetProcessed(false);
sender.doLoopIteration();
// Then
verify(socket).send(any(DatagramPacket.class));
}
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void probeIsSentImmediatelyEvenWhenSenderIsNotFlushed() throws Exception {
// Given
sender.enableAllLevels();
// When
sender.sendProbe(EncryptionLevel.App);
sender.doLoopIteration();
// Then
verify(socket).send(any(DatagramPacket.class));
}
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void probeWithDataIsSentImmediatelyEvenWhenSenderIsNotFlushed() throws Exception {
// Given
sender.enableAllLevels();
// When
sender.sendProbe(List.of(new CryptoFrame(Version.getDefault(), new byte[368])), EncryptionLevel.App);
sender.doLoopIteration();
// Then
verify(socket).send(any(DatagramPacket.class));
}
}