Packet reassembly would fail if the server sent packets out of order. Now, it is resilient to this.

This commit is contained in:
Manevolent
2018-08-13 01:03:44 -04:00
parent 7d195439b8
commit aad790c146
9 changed files with 112 additions and 120 deletions
@@ -4,7 +4,6 @@ import com.github.manevolent.ts3j.protocol.Packet;
import com.github.manevolent.ts3j.protocol.header.HeaderFlag;
import com.github.manevolent.ts3j.protocol.packet.PacketBodyCompressed;
import com.github.manevolent.ts3j.util.QuickLZ;
import com.github.manevolent.ts3j.util.Ts3Debugging;
import java.nio.ByteBuffer;
import java.util.Collections;
@@ -39,8 +38,17 @@ public final class Fragments {
if (packet.getHeader().getType().isCompressible()) {
byte[] compressed = QuickLZ.compress(outputBuffer.array(), 1);
packet.getHeader().setPacketFlag(HeaderFlag.COMPRESSED, true);
packet.setBody(new PacketBodyCompressed(packet.getHeader().getType(), packet.getRole(), compressed));
Packet compressedPacket = new Packet(packet.getRole());
compressedPacket.setHeader(packet.getHeader().clone());
compressedPacket.getHeader().setPacketFlag(HeaderFlag.COMPRESSED, true);
compressedPacket.setBody(
new PacketBodyCompressed(
packet.getHeader().getType(), packet.getRole(),
compressed
)
);
packet = compressedPacket;
if (packet.getSize() <= MAXIMUM_PACKET_SIZE)
return Collections.singletonList(packet);
@@ -11,36 +11,58 @@ import java.util.stream.Collectors;
public class PacketReassembly {
private final Map<Integer, Packet> queue = new LinkedHashMap<>();
private boolean state = false;
private int currentPacketId = 0;
public Packet reassemble(Packet lastFragment) throws IOException {
public void setPacketId(int currentPacketId) {
this.currentPacketId = currentPacketId;
}
public boolean put(Packet packet) throws IOException {
queue.put(packet.getHeader().getPacketId(), packet);
return currentPacketId == packet.getHeader().getPacketId();
}
public Packet next() throws IOException {
List<Packet> reassemblyList = new ArrayList<>();
try {
// Pull out all other packets in the queue before this one which would also be fragmented
List<Integer> packetIds = new ArrayList<>();
for (int packetId = lastFragment.getHeader().getPacketId(); ; packetId--) {
if (packetId < 0) packetId = 65536 + packetId;
boolean state = false;
Packet olderPacket = queue.get(packetId);
for (int packetId = currentPacketId; ; packetId++) {
if (packetId >= 65536) packetId = 0;
if (olderPacket == null)
Packet packet = queue.get(packetId);
if (packet == null)
break; // Chain breaks here
else if (olderPacket.getHeader().getType() != lastFragment.getHeader().getType())
continue; // skip???
else {
packetIds.add(packetId);
reassemblyList.add(olderPacket);
reassemblyList.add(packet);
if (lastFragment.getHeader().getPacketId() != packetId &&
olderPacket.getHeader().getPacketFlag(HeaderFlag.FRAGMENTED))
break;
if (packet.getHeader().getPacketFlag(HeaderFlag.FRAGMENTED)) {
state = !state;
if (!state) {
currentPacketId += packetIds.size();
break; // fragmentation stopped, we found the last packet
} else
continue; // keep collecting fragments
} else if (!state) {
currentPacketId ++;
break; // don't reassemble this packet, it's not a fragment
} else if (state) {
// keep collecting fragments
continue;
}
}
}
// Reverse the collection
Collections.reverse(reassemblyList);
if (state) return null; // cannot reassemble yet because we don't have all fragments
else if (reassemblyList.size() <= 0) return null; // cannot reassemble because we miss the first packet
// Rebuild a master packet from the contents of all previous packet fragments
int totalLength =
@@ -53,7 +75,7 @@ public class PacketReassembly {
Packet firstPacket = reassemblyList.get(0);
Packet reassembledPacket = new Packet(firstPacket.getRole());
reassembledPacket.setHeader(firstPacket.getHeader());
reassembledPacket.setHeader(firstPacket.getHeader().clone());
ByteBuffer reassemblyBuffer = ByteBuffer.allocate(totalLength);
@@ -62,6 +84,8 @@ public class PacketReassembly {
for (Packet old : reassemblyList)
old.writeBody(reassemblyBuffer);
reassemblyBuffer.position(0);
if (firstPacket.getHeader().getPacketFlag(HeaderFlag.COMPRESSED))
reassemblyBuffer = ByteBuffer.wrap(QuickLZ.decompress(reassemblyBuffer.array()));
@@ -70,7 +94,7 @@ public class PacketReassembly {
// Remove read packets
packetIds.forEach(queue::remove);
return reassembledPacket;
return reassembledPacket;
} catch (Exception ex) {
throw new IOException("Problem reassembling " + reassemblyList.size() + " packet(s): [" +
String.join(",",
@@ -81,43 +105,8 @@ public class PacketReassembly {
}
}
public Packet put(Packet packet) throws IOException {
Packet reassembled;
if (packet.getHeader().getPacketFlag(HeaderFlag.FRAGMENTED)) {
boolean oldState = state;
state = !state;
if (!(packet.getBody() instanceof PacketBodyFragment))
throw new IllegalArgumentException("packet fragment object is not representative of a fragment");
queue.put(packet.getHeader().getPacketId(), packet);
if (oldState) {
reassembled = reassemble(packet);
} else {
reassembled = null;
}
} else {
if (state) {
queue.put(packet.getHeader().getPacketId(), packet);
reassembled = null;
} else {
reassembled = packet;
}
}
return reassembled;
}
public boolean isReassembling() {
return state;
}
public void reset() {
state = false;
currentPacketId = 0;
queue.clear();
}
}
@@ -141,8 +141,6 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
}
} else if (packet.getBody() instanceof PacketBody2Command) {
Ts3Debugging.debug(((PacketBody2Command) packet.getBody()).getText());
SingleCommand command = ((PacketBody2Command) packet.getBody()).parse().simplifyOne();
if (command.getName().equalsIgnoreCase("initivexpand2")) {
@@ -26,7 +26,7 @@ public abstract class LocalClientHandlerFull extends LocalClientHandler {
public void handlePacket(Packet packet) throws IOException, TimeoutException {
switch (packet.getBody().getType()) {
case COMMAND:
Ts3Debugging.debug(((PacketBody2Command) packet.getBody()).getText());
Ts3Debugging.debug("[COMMAND] " + ((PacketBody2Command) packet.getBody()).getText());
try {
handleCommand(MultiCommand.parse(
@@ -39,7 +39,7 @@ public abstract class LocalClientHandlerFull extends LocalClientHandler {
break;
case COMMAND_LOW:
Ts3Debugging.debug("[LOW] " + ((PacketBody2Command) packet.getBody()).getText());
Ts3Debugging.debug("[COMMAND] [LOW] " + ((PacketBody2Command) packet.getBody()).getText());
try {
handleCommand(MultiCommand.parse(
@@ -95,6 +95,8 @@ public abstract class AbstractTeamspeakClientSocket
private Map<PacketKind, PacketStatistics> packetStatistics = new LinkedHashMap<>();
//
private final List<Packet> handlerBacklog = new LinkedList<>();
private final NetworkReader networkReader = new NetworkReader();
private Thread networkThread = null;
@@ -207,6 +209,8 @@ public abstract class AbstractTeamspeakClientSocket
readQueue.clear();
handlerBacklog.clear();
for (PacketBodyType bodyType : PacketBodyType.values())
if (bodyType.isSplittable())
reassemblyQueue.get(bodyType).reset();
@@ -500,9 +504,7 @@ public abstract class AbstractTeamspeakClientSocket
boolean fragment = networkPacket.getHeader().getPacketFlag(HeaderFlag.FRAGMENTED);
if (networkPacket.getHeader().getType().isSplittable()) {
fragment = fragment || reassemblyQueue.get(networkPacket.getHeader().getType()).isReassembling();
} else if (fragment)
if (fragment && !networkPacket.getHeader().getType().isSplittable())
throw new IllegalArgumentException(
networkPacket.getHeader().getType() +
"packet is fragment, but not splittable"
@@ -538,15 +540,15 @@ public abstract class AbstractTeamspeakClientSocket
}
// Fragment handling
if (fragment) {
Ts3Debugging.debug("[PROTOCOL] READ " + networkPacket.getHeader().getType().name() + " fragment");
if (networkPacket.getHeader().getType().isSplittable()) {
Ts3Debugging.debug("[PROTOCOL] READ " + networkPacket.getHeader().getType().name());
packet.setBody(new PacketBodyFragment(networkPacket.getHeader().getType(), getRole().getIn()));
} else {
Ts3Debugging.debug("[PROTOCOL] READ " + networkPacket.getHeader().getType().name());
if (packet.getHeader().getPacketFlag(HeaderFlag.COMPRESSED) && packet.getHeader().getType().isCompressible())
packet.setBody(new PacketBodyCompressed(networkPacket.getHeader().getType(), getRole().getIn()));
Ts3Debugging.debug("[PROTOCOL] READ " + networkPacket.getHeader().getType().name());
}
// Read raw packet body
@@ -586,6 +588,12 @@ public abstract class AbstractTeamspeakClientSocket
long timeout;
while (isReading()) {
// De-spool spooled-up packets
if (handlerBacklog.size() > 0) {
Packet packet = handlerBacklog.remove(0);
return packet;
}
try {
timeout = getState() == ClientConnectionState.CONNECTED ?
Math.min(1000L, 1000L - (System.currentTimeMillis() - lastPing)) :
@@ -663,12 +671,6 @@ public abstract class AbstractTeamspeakClientSocket
}
}
// Find if the packet must be reassembled
if (packet.getHeader().getType().isSplittable()) {
packet = reassemblyQueue.get(packet.getHeader().getType()).put(packet);
if (packet == null) continue;
}
// Find if the packet is itself an acknowledgement (ACK or ACK_LOW)
boolean handle;
final PacketResponse response;
@@ -684,8 +686,6 @@ public abstract class AbstractTeamspeakClientSocket
handle = false;
break;
case ACK_LOW:
lastResponse = System.currentTimeMillis();
response = sendQueueLow.get(((PacketBody7AckLow)packet.getBody()).getPacketId());
if (response == null) {
Ts3Debugging.debug("Unrecognized ACK_LOW: " + ((PacketBody6Ack) packet.getBody()).getPacketId());
@@ -696,7 +696,6 @@ public abstract class AbstractTeamspeakClientSocket
handle = false;
break;
case PONG:
lastResponse = System.currentTimeMillis();
response = pingQueue.get(((PacketBody5Pong)packet.getBody()).getPacketId());
if (response == null) {
Ts3Debugging.debug("Unrecognized PONG: " + ((PacketBody5Pong) packet.getBody()).getPacketId());
@@ -733,6 +732,9 @@ public abstract class AbstractTeamspeakClientSocket
currentRto = smoothedRtt + Math.max(0D, 4 * smoothedRttVar);
}
if (response != null)
response.acknowledge(packet);
// This is pulled out of the following IF block so that packet generation can increase correctly
boolean placed = counter != null && counter.put(packet.getHeader().getPacketId());
@@ -742,10 +744,24 @@ public abstract class AbstractTeamspeakClientSocket
handle = handle & placed;
}
if (response != null) {
response.acknowledge(packet);
if (handle) {
if (packet.getHeader().getType().isSplittable()) {
if (!placed) continue; // reset
// Place in the reassembly queue
PacketReassembly reassembly = reassemblyQueue.get(packet.getHeader().getType());
reassembly.put(packet);
Packet reassembled;
while ((reassembled = reassembly.next()) != null) {
Ts3Debugging.debug("[PROTOCOL] REASSEMBLE " + reassembled.getHeader().getType().name() +
" id=" + reassembled.getHeader().getPacketId() + " len=" + reassembled.getSize());
handlerBacklog.add(reassembled);
}
} else {
handlerBacklog.add(packet);
}
}
if (handle) return packet;
}
throw new IllegalStateException("no longer reading");
@@ -874,7 +890,7 @@ public abstract class AbstractTeamspeakClientSocket
} catch (Throwable e) {
if (e instanceof InterruptedException) continue;
Ts3Debugging.debug("Problem handling packet", e);
getExceptionHandler().accept(e);
}
}
}
@@ -141,8 +141,6 @@ public class LocalTeamspeakClientSocket
response.add(new CommandSingleParameter("connection_server2client_packetloss_total", Integer.toString(0)));
Ts3Debugging.debug(response.build());
try {
writePacket(new PacketBody2Command(getRole().getOut(), response));
} catch (Exception e) {