Created a new test for fragments. If a packet fragment is received which wraps around the generation, it is lost. Also, packet fragmentation outgoing would cause a desync in the packet IDs.
This commit is contained in:
@@ -56,4 +56,20 @@ public class ClientPacketHeader extends PacketHeader {
|
|||||||
public int getSize() {
|
public int getSize() {
|
||||||
return super.getSize() + 2 + 1;
|
return super.getSize() + 2 + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PacketHeader clone() {
|
||||||
|
ClientPacketHeader header = new ClientPacketHeader();
|
||||||
|
header.setPacketId(getPacketId());
|
||||||
|
header.setType(getType());
|
||||||
|
header.setClientId(getClientId());
|
||||||
|
header.setGeneration(getGeneration());
|
||||||
|
header.setPacketFlags(getPacketFlags());
|
||||||
|
|
||||||
|
byte[] mac = new byte[8];
|
||||||
|
System.arraycopy(getMac(), 0, mac, 0, 8);
|
||||||
|
header.setMac(mac);
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -110,4 +110,6 @@ public abstract class PacketHeader {
|
|||||||
if (packetGeneration < 0) throw new IllegalArgumentException("packetGeneration");
|
if (packetGeneration < 0) throw new IllegalArgumentException("packetGeneration");
|
||||||
this.packetGeneration = packetGeneration;
|
this.packetGeneration = packetGeneration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract PacketHeader clone();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,4 +50,19 @@ public class ServerPacketHeader extends PacketHeader {
|
|||||||
public int getSize() {
|
public int getSize() {
|
||||||
return super.getSize() + 1;
|
return super.getSize() + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PacketHeader clone() {
|
||||||
|
ServerPacketHeader header = new ServerPacketHeader();
|
||||||
|
header.setPacketId(getPacketId());
|
||||||
|
header.setType(getType());
|
||||||
|
header.setGeneration(getGeneration());
|
||||||
|
header.setPacketFlags(getPacketFlags());
|
||||||
|
|
||||||
|
byte[] mac = new byte[8];
|
||||||
|
System.arraycopy(getMac(), 0, mac, 0, 8);
|
||||||
|
header.setMac(mac);
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -53,8 +53,6 @@ public class PacketBody0Voice extends PacketBody {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(ByteBuffer buffer) {
|
public void read(ByteBuffer buffer) {
|
||||||
Ts3Debugging.debug(buffer.array());
|
|
||||||
|
|
||||||
packetId = buffer.getShort() & 0x0000FFFF;
|
packetId = buffer.getShort() & 0x0000FFFF;
|
||||||
|
|
||||||
if (getRole() == ProtocolRole.SERVER)
|
if (getRole() == ProtocolRole.SERVER)
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package com.github.manevolent.ts3j.protocol.packet.fragment;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Packet fragmentation helper. Pulled out of the client logic for use in FragmentTest so we can track the stability
|
||||||
|
* of fragmentation.
|
||||||
|
*/
|
||||||
|
public final class Fragments {
|
||||||
|
/**
|
||||||
|
* Maximum segment size (header + body) allowed to leave the split method
|
||||||
|
*/
|
||||||
|
public static final int MAXIMUM_PACKET_SIZE = 500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a packet into several sub-packets.
|
||||||
|
* @param packet Packet to send.
|
||||||
|
* @return List of packets to send
|
||||||
|
*/
|
||||||
|
public static List<Packet> split(Packet packet) {
|
||||||
|
if (packet.getSize() > MAXIMUM_PACKET_SIZE) { // Check if packet is too large to send
|
||||||
|
|
||||||
|
int size = packet.getBody().getSize();
|
||||||
|
|
||||||
|
ByteBuffer outputBuffer = ByteBuffer.allocate(size);
|
||||||
|
packet.getBody().write(outputBuffer);
|
||||||
|
|
||||||
|
byte[] outputArray;
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
if (packet.getSize() <= MAXIMUM_PACKET_SIZE)
|
||||||
|
return Collections.singletonList(packet);
|
||||||
|
|
||||||
|
outputArray = compressed;
|
||||||
|
size = compressed.length;
|
||||||
|
} else
|
||||||
|
outputArray = outputBuffer.array();
|
||||||
|
|
||||||
|
List<Packet> pieces = new LinkedList<>();
|
||||||
|
|
||||||
|
int maxFragmentSize = MAXIMUM_PACKET_SIZE - packet.getHeader().getSize();
|
||||||
|
|
||||||
|
for (int offs = 0; offs < size;) {
|
||||||
|
int flush = Math.min(maxFragmentSize, size - offs);
|
||||||
|
|
||||||
|
boolean first = offs == 0;
|
||||||
|
boolean last = flush < maxFragmentSize;
|
||||||
|
|
||||||
|
Packet piece = new Packet(packet.getRole());
|
||||||
|
piece.setHeader(packet.getHeader().clone());
|
||||||
|
|
||||||
|
if (!first) // Only first packet has flags
|
||||||
|
piece.getHeader().setPacketFlags(HeaderFlag.NONE.getIndex());
|
||||||
|
|
||||||
|
// First and last packet get FRAGMENTED flag
|
||||||
|
piece.getHeader().setPacketFlag(HeaderFlag.FRAGMENTED, first || last);
|
||||||
|
|
||||||
|
byte[] pieceBytes = new byte[flush];
|
||||||
|
System.arraycopy(outputArray, offs, pieceBytes, 0, flush);
|
||||||
|
|
||||||
|
piece.setBody(new PacketBodyFragment(
|
||||||
|
packet.getHeader().getType(),
|
||||||
|
packet.getHeader().getRole(),
|
||||||
|
pieceBytes
|
||||||
|
));
|
||||||
|
|
||||||
|
pieces.add(piece);
|
||||||
|
|
||||||
|
offs += flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pieces;
|
||||||
|
} else {
|
||||||
|
// No splitting or compression
|
||||||
|
return Collections.singletonList(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
package com.github.manevolent.ts3j.protocol.packet;
|
package com.github.manevolent.ts3j.protocol.packet.fragment;
|
||||||
|
|
||||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
||||||
|
import com.github.manevolent.ts3j.protocol.packet.PacketBody;
|
||||||
|
import com.github.manevolent.ts3j.protocol.packet.PacketBodyType;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
package com.github.manevolent.ts3j.protocol.packet.fragment;
|
||||||
|
|
||||||
|
import com.github.manevolent.ts3j.protocol.Packet;
|
||||||
|
import com.github.manevolent.ts3j.protocol.header.HeaderFlag;
|
||||||
|
import com.github.manevolent.ts3j.util.QuickLZ;
|
||||||
|
import com.github.manevolent.ts3j.util.Ts3Debugging;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class PacketReassembly {
|
||||||
|
private final Map<Integer, Packet> queue = new LinkedHashMap<>();
|
||||||
|
private boolean state = false;
|
||||||
|
|
||||||
|
public Packet reassemble(Packet lastFragment) throws IOException {
|
||||||
|
// Pull out all other packets in the queue before this one which would also be fragmented
|
||||||
|
List<Packet> reassemblyList = new ArrayList<>();
|
||||||
|
for (int packetId = lastFragment.getHeader().getPacketId();; packetId--) {
|
||||||
|
if (packetId < 0) packetId = 65536 + packetId;
|
||||||
|
|
||||||
|
Packet olderPacket = queue.get(packetId);
|
||||||
|
|
||||||
|
if (olderPacket == null)
|
||||||
|
break; // Chain breaks here
|
||||||
|
else if (olderPacket.getHeader().getType() != lastFragment.getHeader().getType())
|
||||||
|
continue; // skip
|
||||||
|
else {
|
||||||
|
reassemblyList.add(olderPacket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse the collection
|
||||||
|
Collections.reverse(reassemblyList);
|
||||||
|
|
||||||
|
// Rebuild a master packet from the contents of all previous packet fragments
|
||||||
|
int totalLength =
|
||||||
|
reassemblyList
|
||||||
|
.stream()
|
||||||
|
.mapToInt(x -> x.getBody().getSize())
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
if (totalLength < 0) throw new IllegalArgumentException("reassembly too small: " + totalLength);
|
||||||
|
|
||||||
|
Packet firstPacket = reassemblyList.get(0);
|
||||||
|
Packet reassembledPacket = new Packet(firstPacket.getRole());
|
||||||
|
reassembledPacket.setHeader(firstPacket.getHeader());
|
||||||
|
|
||||||
|
ByteBuffer reassemblyBuffer = ByteBuffer.allocate(totalLength);
|
||||||
|
|
||||||
|
reassembledPacket.getHeader().setPacketFlag(HeaderFlag.FRAGMENTED, false);
|
||||||
|
|
||||||
|
for (Packet old : reassemblyList)
|
||||||
|
old.writeBody(reassemblyBuffer);
|
||||||
|
|
||||||
|
if (firstPacket.getHeader().getPacketFlag(HeaderFlag.COMPRESSED))
|
||||||
|
reassemblyBuffer = ByteBuffer.wrap(QuickLZ.decompress(reassemblyBuffer.array()));
|
||||||
|
|
||||||
|
reassembledPacket.readBody(reassemblyBuffer);
|
||||||
|
|
||||||
|
return reassembledPacket;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
queue.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -27,6 +27,7 @@ public abstract class LocalClientHandlerFull extends LocalClientHandler {
|
|||||||
switch (packet.getBody().getType()) {
|
switch (packet.getBody().getType()) {
|
||||||
case COMMAND:
|
case COMMAND:
|
||||||
Ts3Debugging.debug(((PacketBody2Command) packet.getBody()).getText());
|
Ts3Debugging.debug(((PacketBody2Command) packet.getBody()).getText());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
handleCommand(MultiCommand.parse(
|
handleCommand(MultiCommand.parse(
|
||||||
packet.getRole(),
|
packet.getRole(),
|
||||||
@@ -35,8 +36,11 @@ public abstract class LocalClientHandlerFull extends LocalClientHandler {
|
|||||||
} catch (CommandProcessException e) {
|
} catch (CommandProcessException e) {
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case COMMAND_LOW:
|
case COMMAND_LOW:
|
||||||
|
Ts3Debugging.debug("[LOW] " + ((PacketBody2Command) packet.getBody()).getText());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
handleCommand(MultiCommand.parse(
|
handleCommand(MultiCommand.parse(
|
||||||
packet.getRole(),
|
packet.getRole(),
|
||||||
@@ -45,6 +49,7 @@ public abstract class LocalClientHandlerFull extends LocalClientHandler {
|
|||||||
} catch (CommandProcessException e) {
|
} catch (CommandProcessException e) {
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-176
@@ -11,6 +11,9 @@ import com.github.manevolent.ts3j.protocol.header.ClientPacketHeader;
|
|||||||
import com.github.manevolent.ts3j.protocol.header.HeaderFlag;
|
import com.github.manevolent.ts3j.protocol.header.HeaderFlag;
|
||||||
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
|
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
|
||||||
import com.github.manevolent.ts3j.protocol.packet.*;
|
import com.github.manevolent.ts3j.protocol.packet.*;
|
||||||
|
import com.github.manevolent.ts3j.protocol.packet.fragment.Fragments;
|
||||||
|
import com.github.manevolent.ts3j.protocol.packet.fragment.PacketBodyFragment;
|
||||||
|
import com.github.manevolent.ts3j.protocol.packet.fragment.PacketReassembly;
|
||||||
import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler;
|
import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler;
|
||||||
import com.github.manevolent.ts3j.protocol.packet.handler.client.LocalClientHandler;
|
import com.github.manevolent.ts3j.protocol.packet.handler.client.LocalClientHandler;
|
||||||
import com.github.manevolent.ts3j.protocol.packet.statistics.PacketStatistics;
|
import com.github.manevolent.ts3j.protocol.packet.statistics.PacketStatistics;
|
||||||
@@ -52,7 +55,7 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
|
|
||||||
private final LinkedBlockingDeque<Packet> readQueue = new LinkedBlockingDeque<>(65536);
|
private final LinkedBlockingDeque<Packet> readQueue = new LinkedBlockingDeque<>(65536);
|
||||||
|
|
||||||
private final Map<PacketBodyType, Reassembly> reassemblyQueue = new ConcurrentHashMap<>();
|
private final Map<PacketBodyType, PacketReassembly> reassemblyQueue = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private final Map<Integer, PacketResponse> pingQueue = new ConcurrentHashMap<>();
|
private final Map<Integer, PacketResponse> pingQueue = new ConcurrentHashMap<>();
|
||||||
private final Map<Integer, PacketResponse> sendQueue = new ConcurrentHashMap<>();
|
private final Map<Integer, PacketResponse> sendQueue = new ConcurrentHashMap<>();
|
||||||
@@ -61,6 +64,8 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
private final Map<PacketBodyType, LocalCounter> localSendCounter = new ConcurrentHashMap<>();
|
private final Map<PacketBodyType, LocalCounter> localSendCounter = new ConcurrentHashMap<>();
|
||||||
private final Map<PacketBodyType, RemoteCounter> remoteSendCounter = new ConcurrentHashMap<>();
|
private final Map<PacketBodyType, RemoteCounter> remoteSendCounter = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final Map<PacketBodyType, Object> sendLocks = new HashMap<>();
|
||||||
|
|
||||||
private CommandProcessor commandProcessor;
|
private CommandProcessor commandProcessor;
|
||||||
|
|
||||||
private Consumer<Throwable> exceptionHandler = throwable -> {
|
private Consumer<Throwable> exceptionHandler = throwable -> {
|
||||||
@@ -104,7 +109,7 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
// Initialize reassembly queue
|
// Initialize reassembly queue
|
||||||
for (PacketBodyType bodyType : PacketBodyType.values())
|
for (PacketBodyType bodyType : PacketBodyType.values())
|
||||||
if (bodyType.isSplittable())
|
if (bodyType.isSplittable())
|
||||||
reassemblyQueue.put(bodyType, new Reassembly());
|
reassemblyQueue.put(bodyType, new PacketReassembly());
|
||||||
|
|
||||||
// Initialize counters
|
// Initialize counters
|
||||||
for (PacketBodyType bodyType : PacketBodyType.values()) {
|
for (PacketBodyType bodyType : PacketBodyType.values()) {
|
||||||
@@ -115,6 +120,8 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
localSendCounter.put(bodyType, new LocalCounterFull(65536, true));
|
localSendCounter.put(bodyType, new LocalCounterFull(65536, true));
|
||||||
remoteSendCounter.put(bodyType, new RemoteCounterFull(65536, 100));
|
remoteSendCounter.put(bodyType, new RemoteCounterFull(65536, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendLocks.put(bodyType, new Object());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize statistics
|
// Initialize statistics
|
||||||
@@ -254,11 +261,13 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writePacket(PacketBody body) throws IOException, TimeoutException {
|
public void writePacket(PacketBody body) throws IOException, TimeoutException {
|
||||||
// Construct a network packet
|
// Construct a packet object
|
||||||
Packet packet = new Packet(body.getRole());
|
Packet packet = new Packet(body.getRole());
|
||||||
|
|
||||||
|
// Set body
|
||||||
packet.setBody(body);
|
packet.setBody(body);
|
||||||
|
|
||||||
// Generate header
|
// Generate new header (values get filled in as we go, and primarily in writePacketIntl)
|
||||||
PacketHeader header = newOutgoingHeader();
|
PacketHeader header = newOutgoingHeader();
|
||||||
|
|
||||||
// Ensure type matches
|
// Ensure type matches
|
||||||
@@ -270,7 +279,75 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
if (header instanceof ClientPacketHeader)
|
if (header instanceof ClientPacketHeader)
|
||||||
((ClientPacketHeader) header).setClientId(clientId);
|
((ClientPacketHeader) header).setClientId(clientId);
|
||||||
|
|
||||||
|
// Static header assignments (protocol expectations based on type)
|
||||||
|
switch (header.getType()) {
|
||||||
|
case INIT1:
|
||||||
|
case PONG:
|
||||||
|
case PING:
|
||||||
|
// setHeaderValues will do this, too
|
||||||
|
header.setPacketFlag(HeaderFlag.UNENCRYPTED, true);
|
||||||
|
break;
|
||||||
|
case COMMAND:
|
||||||
|
case COMMAND_LOW:
|
||||||
|
header.setPacketFlag(HeaderFlag.NEW_PROTOCOL, true);
|
||||||
|
break;
|
||||||
|
case VOICE:
|
||||||
|
// > X is a ushort in H2N order of an own audio packet counter
|
||||||
|
// it seems it can be the same as the packet counter so we will let the packethandler do it.
|
||||||
|
((PacketBody0Voice)body).setPacketId(header.getPacketId());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign header pointer
|
||||||
|
packet.setHeader(header);
|
||||||
|
|
||||||
|
// Lower-level write; helper is done
|
||||||
|
writePacket(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writePacket(Packet packet) throws IOException, TimeoutException {
|
||||||
|
// Sanity checks. People are insane!
|
||||||
|
if (packet.getHeader() == null)
|
||||||
|
throw new NullPointerException("header");
|
||||||
|
else if (packet.getHeader().getType() == null)
|
||||||
|
throw new NullPointerException("header type");
|
||||||
|
else if (packet.getHeader().getRole() != getRole().getOut())
|
||||||
|
throw new IllegalArgumentException("packet role mismatch: " +
|
||||||
|
packet.getHeader().getRole().name() + " != " +
|
||||||
|
getRole().getOut().name());
|
||||||
|
|
||||||
|
if (packet.getHeader().getType().isSplittable()) {
|
||||||
|
// Split up the outgoing packet into valid pieces
|
||||||
|
// it's better to do this all at once instead of sending them as they are split,
|
||||||
|
// because we might get halfway through splitting them then throw an exception and
|
||||||
|
// we can't write the rest.
|
||||||
|
List<Packet> packets = Fragments.split(packet);
|
||||||
|
|
||||||
|
// Note, it's very important we send these packets synchronously.
|
||||||
|
// we allow the split to happen multi-thread, but we block until all
|
||||||
|
// chunks are sent synchronously. So, all chunks of packets are synchronous.
|
||||||
|
// this happens because we care that the packet IDs are contiguous.
|
||||||
|
synchronized (sendLocks.get(packet.getHeader().getType())) {
|
||||||
|
for (Packet piece : packets)
|
||||||
|
writePacketIntl(piece); // Write the individual piece, incrementing its packet
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ensure the outbound packet's maximum size isn't too large if it can't be split (which it can't here)
|
||||||
|
if (packet.getSize() > Fragments.MAXIMUM_PACKET_SIZE)
|
||||||
|
throw new IllegalArgumentException("packet too large: " + packet.getSize() + "; " +
|
||||||
|
packet.getHeader().getType().name() + " cannot split");
|
||||||
|
|
||||||
|
// Don't lock here, for the slight performance benefit.
|
||||||
|
writePacketIntl(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writePacketIntl(Packet packet) throws IOException, TimeoutException {
|
||||||
|
PacketHeader header = packet.getHeader();
|
||||||
|
|
||||||
// Count packet
|
// Count packet
|
||||||
|
// depends on the actual packet type coming in
|
||||||
Pair<Integer, Integer> counter;
|
Pair<Integer, Integer> counter;
|
||||||
|
|
||||||
switch (header.getType()) {
|
switch (header.getType()) {
|
||||||
@@ -306,104 +383,20 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use the counters determined above to generate a packet id and generation id
|
||||||
header.setPacketId(counter.getKey());
|
header.setPacketId(counter.getKey());
|
||||||
header.setGeneration(counter.getValue());
|
header.setGeneration(counter.getValue());
|
||||||
|
|
||||||
switch (header.getType()) {
|
|
||||||
case INIT1:
|
|
||||||
case PONG:
|
|
||||||
case PING:
|
|
||||||
// setHeaderValues will do this, too
|
|
||||||
header.setPacketFlag(HeaderFlag.UNENCRYPTED, true);
|
|
||||||
break;
|
|
||||||
case COMMAND:
|
|
||||||
case COMMAND_LOW:
|
|
||||||
header.setPacketFlag(HeaderFlag.NEW_PROTOCOL, true);
|
|
||||||
break;
|
|
||||||
case VOICE:
|
|
||||||
// > X is a ushort in H2N order of an own audio packet counter
|
|
||||||
// it seems it can be the same as the packet counter so we will let the packethandler do it.
|
|
||||||
((PacketBody0Voice)body).setPacketId(header.getPacketId());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
packet.setHeader(header);
|
|
||||||
|
|
||||||
writePacket(packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writePacket(Packet packet) throws IOException, TimeoutException {
|
|
||||||
if (packet.getHeader() == null)
|
|
||||||
throw new NullPointerException("header");
|
|
||||||
else if (packet.getHeader().getRole() != getRole().getOut())
|
|
||||||
throw new IllegalArgumentException("packet role mismatch: " +
|
|
||||||
packet.getHeader().getRole().name() + " != " +
|
|
||||||
getRole().getOut().name());
|
|
||||||
|
|
||||||
if (packet.getSize() > 500) {
|
|
||||||
int totalSize = packet.getBody().getSize();
|
|
||||||
|
|
||||||
ByteBuffer outputBuffer = ByteBuffer.allocate(totalSize);
|
|
||||||
packet.getBody().write(outputBuffer);
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
if (packet.getSize() <= 500) {
|
|
||||||
writePacketIntl(packet);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!packet.getHeader().getType().isSplittable())
|
|
||||||
throw new IllegalArgumentException("packet too large: " + packet.getSize() + " (cannot split)");
|
|
||||||
|
|
||||||
for (int offs = 0; offs < totalSize;) {
|
|
||||||
int flush = Math.min(500 - packet.getHeader().getSize(), totalSize - offs);
|
|
||||||
boolean first = offs == 0;
|
|
||||||
boolean last = flush < 500;
|
|
||||||
|
|
||||||
Packet piece = new Packet(packet.getRole());
|
|
||||||
piece.setHeader(packet.getHeader());
|
|
||||||
|
|
||||||
if (!first) // Only first packet has flags
|
|
||||||
piece.getHeader().setPacketFlags(HeaderFlag.NONE.getIndex());
|
|
||||||
|
|
||||||
// First and last packet get FRAGMENTED flag
|
|
||||||
piece.getHeader().setPacketFlag(HeaderFlag.FRAGMENTED, first || last);
|
|
||||||
|
|
||||||
byte[] pieceBytes = new byte[flush];
|
|
||||||
System.arraycopy(outputBuffer.array(), offs, pieceBytes, 0, flush);
|
|
||||||
|
|
||||||
piece.setBody(new PacketBodyFragment(
|
|
||||||
packet.getHeader().getType(),
|
|
||||||
packet.getHeader().getRole(),
|
|
||||||
pieceBytes
|
|
||||||
));
|
|
||||||
|
|
||||||
writePacketIntl(piece);
|
|
||||||
|
|
||||||
offs += flush;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
writePacketIntl(packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writePacketIntl(Packet packet) throws IOException, TimeoutException {
|
|
||||||
// Flush to a buffer
|
// Flush to a buffer
|
||||||
ByteBuffer outputBuffer;
|
ByteBuffer outputBuffer;
|
||||||
|
|
||||||
if (!packet.getHeader().getPacketFlag(HeaderFlag.UNENCRYPTED)) {
|
if (!packet.getHeader().getPacketFlag(HeaderFlag.UNENCRYPTED)) { // encrypt out
|
||||||
Ts3Debugging.debug("[PROTOCOL] ENCRYPT " +
|
Ts3Debugging.debug("[PROTOCOL] ENCRYPT " +
|
||||||
packet.getHeader().getType().name() + " generation=" +
|
packet.getHeader().getType().name() + " generation=" +
|
||||||
packet.getHeader().getGeneration());
|
packet.getHeader().getGeneration());
|
||||||
|
|
||||||
outputBuffer = getTransformation().encrypt(packet);
|
outputBuffer = getTransformation().encrypt(packet);
|
||||||
} else {
|
} else { // "fake encrypt"
|
||||||
if (packet.getHeader().getType() == PacketBodyType.INIT1) {
|
if (packet.getHeader().getType() == PacketBodyType.INIT1) {
|
||||||
System.arraycopy(INIT1_MAC, 0, packet.getHeader().getMac(), 0, 8);
|
System.arraycopy(INIT1_MAC, 0, packet.getHeader().getMac(), 0, 8);
|
||||||
} else {
|
} else {
|
||||||
@@ -417,6 +410,7 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
packet.writeBody(outputBuffer);
|
packet.writeBody(outputBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construct actual network packet now that the protocol is finished.
|
||||||
NetworkPacket networkPacket = new NetworkPacket(
|
NetworkPacket networkPacket = new NetworkPacket(
|
||||||
new DatagramPacket(outputBuffer.array(), outputBuffer.position()),
|
new DatagramPacket(outputBuffer.array(), outputBuffer.position()),
|
||||||
packet.getHeader(),
|
packet.getHeader(),
|
||||||
@@ -459,8 +453,10 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
|
|
||||||
Ts3Debugging.debug("[PROTOCOL] WRITE " + networkPacket.getHeader().getType().name());
|
Ts3Debugging.debug("[PROTOCOL] WRITE " + networkPacket.getHeader().getType().name());
|
||||||
|
|
||||||
|
// Real network send
|
||||||
writeNetworkPacket(networkPacket);
|
writeNetworkPacket(networkPacket);
|
||||||
|
|
||||||
|
// Now that the packet is sent, we need to track the stats
|
||||||
PacketKind kind = networkPacket.getHeader().getType().getKind();
|
PacketKind kind = networkPacket.getHeader().getType().getKind();
|
||||||
if (kind != null)
|
if (kind != null)
|
||||||
packetStatistics
|
packetStatistics
|
||||||
@@ -656,6 +652,8 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
|
|
||||||
pong.getHeader().setPacketFlag(HeaderFlag.UNENCRYPTED, true);
|
pong.getHeader().setPacketFlag(HeaderFlag.UNENCRYPTED, true);
|
||||||
pong.getHeader().setPacketId(packet.getHeader().getPacketId());
|
pong.getHeader().setPacketId(packet.getHeader().getPacketId());
|
||||||
|
pong.getHeader().setType(PacketBodyType.PONG);
|
||||||
|
|
||||||
pong.setBody(new PacketBody5Pong(getRole().getOut(), packet.getHeader().getPacketId()));
|
pong.setBody(new PacketBody5Pong(getRole().getOut(), packet.getHeader().getPacketId()));
|
||||||
|
|
||||||
writePacket(pong);
|
writePacket(pong);
|
||||||
@@ -1350,89 +1348,4 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Reassembly {
|
|
||||||
private final Map<Integer, Packet> queue = new LinkedHashMap<>();
|
|
||||||
private boolean state = false;
|
|
||||||
|
|
||||||
public Packet reassemble(Packet lastFragment) throws IOException {
|
|
||||||
// Pull out all other packets in the queue before this one which would also be fragmented
|
|
||||||
List<Packet> reassemblyList = new ArrayList<>();
|
|
||||||
for (int packetId = lastFragment.getHeader().getPacketId();; packetId--) {
|
|
||||||
Packet olderPacket = queue.get(packetId);
|
|
||||||
|
|
||||||
if (olderPacket == null)
|
|
||||||
break; // Chain breaks here
|
|
||||||
else if (olderPacket.getHeader().getType() != lastFragment.getHeader().getType())
|
|
||||||
continue; // skip
|
|
||||||
else {
|
|
||||||
reassemblyList.add(olderPacket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reverse the collection
|
|
||||||
Collections.reverse(reassemblyList);
|
|
||||||
|
|
||||||
// Rebuild a master packet from the contents of all previous packet fragments
|
|
||||||
int totalLength = reassemblyList.stream().mapToInt(x -> x.getBody().getSize()).sum() + lastFragment.getBody().getSize();
|
|
||||||
if (totalLength < 0) throw new IllegalArgumentException("reassembly too small: " + totalLength);
|
|
||||||
|
|
||||||
Packet firstPacket = reassemblyList.get(0);
|
|
||||||
Packet reassembledPacket = new Packet(firstPacket.getRole());
|
|
||||||
reassembledPacket.setHeader(firstPacket.getHeader());
|
|
||||||
|
|
||||||
ByteBuffer reassemblyBuffer = ByteBuffer.allocate(totalLength);
|
|
||||||
|
|
||||||
reassembledPacket.getHeader().setPacketFlag(HeaderFlag.FRAGMENTED, false);
|
|
||||||
|
|
||||||
for (Packet old : reassemblyList)
|
|
||||||
old.writeBody(reassemblyBuffer);
|
|
||||||
|
|
||||||
if (firstPacket.getHeader().getPacketFlag(HeaderFlag.COMPRESSED))
|
|
||||||
reassemblyBuffer = ByteBuffer.wrap(QuickLZ.decompress(reassemblyBuffer.array()));
|
|
||||||
|
|
||||||
reassembledPacket.readBody(reassemblyBuffer);
|
|
||||||
|
|
||||||
return reassembledPacket;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
queue.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.github.manevolent.ts3j;
|
|||||||
|
|
||||||
import com.github.manevolent.ts3j.util.QuickLZ;
|
import com.github.manevolent.ts3j.util.QuickLZ;
|
||||||
import junit.framework.TestCase;
|
import junit.framework.TestCase;
|
||||||
|
import org.bouncycastle.util.encoders.Base64;
|
||||||
|
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.github.manevolent.ts3j;
|
||||||
|
|
||||||
|
import com.github.manevolent.ts3j.protocol.Packet;
|
||||||
|
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
||||||
|
import com.github.manevolent.ts3j.protocol.header.ClientPacketHeader;
|
||||||
|
import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command;
|
||||||
|
import com.github.manevolent.ts3j.protocol.packet.PacketBodyType;
|
||||||
|
import com.github.manevolent.ts3j.protocol.packet.fragment.Fragments;
|
||||||
|
import com.github.manevolent.ts3j.protocol.packet.fragment.PacketReassembly;
|
||||||
|
import com.github.manevolent.ts3j.protocol.socket.client.AbstractTeamspeakClientSocket;
|
||||||
|
import com.github.manevolent.ts3j.util.Pair;
|
||||||
|
import com.github.manevolent.ts3j.util.QuickLZ;
|
||||||
|
import com.github.manevolent.ts3j.util.Ts3Debugging;
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
import org.bouncycastle.util.encoders.Base64;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class FragmentTest extends TestCase {
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
new FragmentTest().testParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testParser() throws Exception {
|
||||||
|
Ts3Debugging.setEnabled(true);
|
||||||
|
|
||||||
|
byte[] data = new byte[2000];
|
||||||
|
new Random(0x1234567).nextBytes(data);
|
||||||
|
|
||||||
|
ClientPacketHeader header = new ClientPacketHeader();
|
||||||
|
Packet largePacket = new Packet(ProtocolRole.CLIENT, header);
|
||||||
|
header.setType(PacketBodyType.COMMAND);
|
||||||
|
|
||||||
|
String text = Base64.toBase64String(data);
|
||||||
|
largePacket.setBody(new PacketBody2Command(ProtocolRole.CLIENT, text));
|
||||||
|
|
||||||
|
PacketReassembly reassembly = new PacketReassembly();
|
||||||
|
AbstractTeamspeakClientSocket.LocalCounterFull counter =
|
||||||
|
new AbstractTeamspeakClientSocket.LocalCounterFull(65536, true);
|
||||||
|
|
||||||
|
counter.setPacketId(65534); // Test wrapping around the generation at the same time :)
|
||||||
|
|
||||||
|
List<Packet> pieces = Fragments.split(largePacket);
|
||||||
|
Ts3Debugging.debug("Created " + pieces.size() + " pieces (total=" + data.length + ").");
|
||||||
|
|
||||||
|
for (Packet piece : pieces) {
|
||||||
|
Pair<Integer, Integer> packetId = counter.next();
|
||||||
|
piece.getHeader().setPacketId(packetId.getKey());
|
||||||
|
piece.getHeader().setGeneration(packetId.getValue());
|
||||||
|
|
||||||
|
Packet out = reassembly.put(piece);
|
||||||
|
if (out != null)
|
||||||
|
assertEquals(text, ((PacketBody2Command)out.getBody()).getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -146,6 +146,8 @@ public class ServerConnectionTest {
|
|||||||
|
|
||||||
client.subscribeAll();
|
client.subscribeAll();
|
||||||
|
|
||||||
|
for (int i = 0; i < 1000; i ++) client.getClientInfo(client.getClientId());
|
||||||
|
|
||||||
Thread.sleep(100000000L);
|
Thread.sleep(100000000L);
|
||||||
} catch (Throwable ex) {
|
} catch (Throwable ex) {
|
||||||
ex.printStackTrace();
|
ex.printStackTrace();
|
||||||
|
|||||||
Reference in New Issue
Block a user