Packet reassembly would fail if the server sent packets out of order. Now, it is resilient to this.
This commit is contained in:
@@ -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);
|
||||
|
||||
+41
-52
@@ -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();
|
||||
}
|
||||
}
|
||||
-2
@@ -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")) {
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
+36
-20
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user