diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/LocalEndpoint.java b/src/main/java/com/github/manevolent/ts3j/protocol/LocalEndpoint.java index 2643884..b3a020b 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/LocalEndpoint.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/LocalEndpoint.java @@ -2,8 +2,12 @@ package com.github.manevolent.ts3j.protocol; import com.github.manevolent.ts3j.identity.LocalIdentity; import com.github.manevolent.ts3j.protocol.header.HeaderFlag; +import com.github.manevolent.ts3j.protocol.header.PacketHeader; +import com.github.manevolent.ts3j.protocol.packet.PacketType; +import com.github.manevolent.ts3j.protocol.packet.channel.PacketChannel; import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler; import com.github.manevolent.ts3j.protocol.socket.LocalTeamspeakSocket; +import com.github.manevolent.ts3j.protocol.packet.channel.ReassemblyQueue; import com.github.manevolent.ts3j.util.Ts3Logging; import java.io.IOException; @@ -13,10 +17,10 @@ import java.net.SocketException; public abstract class LocalEndpoint implements Endpoint { private final LocalTeamspeakSocket socket; - - private PacketHandler handler = null; private LocalIdentity localIdentity = null; + private final ReassemblyQueue reassemblyQueue = new ReassemblyQueue(); + private final Object runLock = new Object(); private Thread receiverThread; private boolean running = false; @@ -41,6 +45,9 @@ public abstract class LocalEndpoint implements Endpoint { return socket; } + protected abstract void acknowledge(NetworkPacket packet, PacketType ackType) throws IOException; + protected abstract PacketChannel getChannel(NetworkPacket packet); + public void setRunning(boolean running) { if (this.running != running) { this.running = running; @@ -69,22 +76,6 @@ public abstract class LocalEndpoint implements Endpoint { return socket.receive(); } - public void setHandler(PacketHandler handler) throws IOException { - if (this.handler != handler) { - if (this.handler != null) this.handler.onUnassigning(); - this.handler = handler; - if (handler != null) handler.onAssigned(); - } - } - - protected void handlePacket(NetworkPacket packet) throws IOException { - if (packet.getHeader().getPacketFlag(HeaderFlag.FRAGMENTED)) { - - } else { - if (this.handler != null) this.handler.handlePacket(packet); - } - } - public LocalIdentity getLocalIdentity() { return localIdentity; } @@ -100,7 +91,21 @@ public abstract class LocalEndpoint implements Endpoint { while (running) { try { - handlePacket(receive()); + NetworkPacket packet = receive(); + PacketChannel channel = getChannel(packet); + if (channel == null) continue; + + channel.insert(packet); + + if (packet.getHeader().getType().getAcknolwedgedBy() != null) + acknowledge(packet, packet.getHeader().getType().getAcknolwedgedBy()); + + PacketHandler handler = channel.getHandler(); + if (handler != null) { + while ((packet = channel.next()) != null) { + handler.handlePacket(packet); + } + } } catch (Exception e) { if (e instanceof InterruptedException) continue; diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/NetworkPacket.java b/src/main/java/com/github/manevolent/ts3j/protocol/NetworkPacket.java index 9736f63..113ff48 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/NetworkPacket.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/NetworkPacket.java @@ -49,35 +49,43 @@ public final class NetworkPacket { * Writes the existing data out to the specified byte buffer * @param buffer Buffer to write to */ - public void write(ByteBuffer buffer) { + public ByteBuffer write(ByteBuffer buffer) { writeHeader(buffer); writeBody(buffer); + + return buffer; } - public void writeHeader(ByteBuffer buffer) { + public ByteBuffer writeHeader(ByteBuffer buffer) { if (getHeader() == null) throw new NullPointerException("header"); getHeader().setType(getPacket().getType()); getHeader().write(buffer); + + return buffer; } - public void writeBody(ByteBuffer buffer) { + public ByteBuffer writeBody(ByteBuffer buffer) { if (getPacket() == null) throw new NullPointerException("packet"); getPacket().write(buffer); + + return buffer; } /** * Reads data from the specified byte buffer, filling in the packet body as needed. * @param buffer Buffer to read from. */ - public void read(ByteBuffer buffer) { + public ByteBuffer read(ByteBuffer buffer) { readHeader(buffer); readBody(buffer); + + return buffer; } - public void readHeader(ByteBuffer buffer) { + public ByteBuffer readHeader(ByteBuffer buffer) { if (getHeader() == null) { try { setHeader(getRole().getHeaderClass().newInstance()); @@ -87,9 +95,11 @@ public final class NetworkPacket { } getHeader().read(buffer); + + return buffer; } - public void readBody(ByteBuffer buffer) { + public ByteBuffer readBody(ByteBuffer buffer) { if (getPacket() == null) { try { setPacket( @@ -103,6 +113,8 @@ public final class NetworkPacket { } getPacket().read(buffer); + + return buffer; } /** diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/client/LocalTeamspeakClient.java b/src/main/java/com/github/manevolent/ts3j/protocol/client/LocalTeamspeakClient.java index b3213b5..dd2da4a 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/client/LocalTeamspeakClient.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/client/LocalTeamspeakClient.java @@ -1,12 +1,15 @@ package com.github.manevolent.ts3j.protocol.client; import com.github.manevolent.ts3j.command.Command; -import com.github.manevolent.ts3j.command.part.CommandSingleParameter; import com.github.manevolent.ts3j.command.response.CommandResponse; -import com.github.manevolent.ts3j.identity.LocalIdentity; import com.github.manevolent.ts3j.protocol.LocalEndpoint; +import com.github.manevolent.ts3j.protocol.NetworkPacket; +import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.SocketRole; import com.github.manevolent.ts3j.protocol.header.ClientPacketHeader; +import com.github.manevolent.ts3j.protocol.packet.Packet6Ack; +import com.github.manevolent.ts3j.protocol.packet.PacketType; +import com.github.manevolent.ts3j.protocol.packet.channel.PacketChannel; import com.github.manevolent.ts3j.protocol.packet.handler.client.LocalClientHandler; import com.github.manevolent.ts3j.protocol.socket.LocalTeamspeakSocket; import com.github.manevolent.ts3j.protocol.packet.Packet; @@ -18,16 +21,16 @@ import java.net.SocketException; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.TimeoutException; -import java.util.function.Function; public class LocalTeamspeakClient extends LocalEndpoint implements TeamspeakClient { private final Object connectionStateLock = new Object(); - private final Map clientOptions = new LinkedHashMap<>(); + private final PacketChannel channel = new PacketChannel(); - private LocalClientHandler handler; - private ClientConnectionState clientConnectionState = null; + private ClientConnectionState connectionState = null; + private int packetId = 0; + private int clientId = 0; private int returnCodeIndex = 0; public LocalTeamspeakClient(LocalTeamspeakSocket socket) { @@ -36,9 +39,45 @@ public class LocalTeamspeakClient extends LocalEndpoint implements TeamspeakClie if (socket.getSocketRole() != SocketRole.CLIENT) throw new IllegalArgumentException("invalid socket role: " + socket.getSocketRole().name()); - setClientConnectionState(ClientConnectionState.DISCONNECTED); + setConnectionState(ClientConnectionState.DISCONNECTED); } + @Override + public SocketRole getRole() { + return SocketRole.CLIENT; + } + + @Override + protected void acknowledge(NetworkPacket packet, PacketType ackType) throws IOException { + switch (ackType) { + case ACK: + Ts3Logging.debug("ACK " + packet.getHeader().getType().name()); + + Packet6Ack ack = new Packet6Ack(ProtocolRole.CLIENT); + ack.setPacketId(packet.getHeader().getPacketId()); + send(ack); + + break; + } + } + + // Just return the client's single channel (to the server) here + @Override + protected PacketChannel getChannel(NetworkPacket packet) { + return channel; + } + + @Override + public Map getOptions() { + return clientOptions; + } + + /** + * Sends a command to the remote server. + * @param command Command to send. + * @return Command response object to track command response. + * @throws IOException + */ public CommandResponse sendCommand(Command command) throws IOException { if (command.willExpectResponse() && !isConnected()) throw new IOException("not connected"); @@ -64,48 +103,49 @@ public class LocalTeamspeakClient extends LocalEndpoint implements TeamspeakClie this(new LocalTeamspeakSocket(SocketRole.CLIENT)); } - @Override - public SocketRole getRole() { - return SocketRole.CLIENT; - } - + /** + * Sends a packet to the server + * @param packet Packet to send + * @throws IOException + */ public void send(Packet packet) throws IOException { // Construct header ClientPacketHeader header = new ClientPacketHeader(); + header.setClientId(clientId); + header.setPacketId(packetId); + packet.setHeaderValues(header); getSocket().send(header, packet); } - protected void setClientConnectionState(ClientConnectionState state) { + private void setConnectionState(ClientConnectionState state) { synchronized (connectionStateLock) { - if (state != this.clientConnectionState) { + if (state != this.connectionState) { Ts3Logging.debug("State changing: " + state.name()); - boolean wasDisconnected = this.clientConnectionState == ClientConnectionState.DISCONNECTED; + boolean wasDisconnected = this.connectionState == ClientConnectionState.DISCONNECTED; - this.clientConnectionState = state; + this.connectionState = state; if (wasDisconnected) setRunning(true); - if (handler != null) - handler.handleConnectionStateChanging(state); + if (channel.getHandler() != null) + ((LocalClientHandler)channel.getHandler()).handleConnectionStateChanging(state); - if (handler == null || handler.getClass() != state.getHandlerClass()) { + if (channel.getHandler() == null || channel.getHandler().getClass() != state.getHandlerClass()) { Ts3Logging.debug("Assigning " + state.getHandlerClass().getName() + " handler..."); - handler = state.createHandler(this); - try { - setHandler(handler); + channel.setHandler(state.createHandler(this)); } catch (IOException e) { throw new IllegalStateException(e); } } - if (this.clientConnectionState == ClientConnectionState.DISCONNECTED) + if (this.connectionState == ClientConnectionState.DISCONNECTED) clientOptions.clear(); connectionStateLock.notifyAll(); @@ -115,17 +155,8 @@ public class LocalTeamspeakClient extends LocalEndpoint implements TeamspeakClie } } - public boolean isConnected() { - return clientConnectionState == ClientConnectionState.CONNECTED; - } - - @Override - public Map getOptions() { - return clientOptions; - } - - public ClientConnectionState getClientConnectionState() { - return clientConnectionState; + public ClientConnectionState getConnectionState() { + return connectionState; } private void joinConnectionState(ClientConnectionState state, long wait) @@ -133,29 +164,40 @@ public class LocalTeamspeakClient extends LocalEndpoint implements TeamspeakClie synchronized (connectionStateLock) { long start = System.currentTimeMillis(); - while (clientConnectionState != state && System.currentTimeMillis() - start < wait) { + while (connectionState != state && System.currentTimeMillis() - start < wait) { connectionStateLock.wait(Math.max(0L, wait - (System.currentTimeMillis() - start))); } - if (clientConnectionState != state) + if (connectionState != state) throw new TimeoutException("timeout waiting for " + state.name() + " state"); } } + public boolean isConnected() { + return connectionState == ClientConnectionState.CONNECTED; + } + + /** + * Initiates a connection to a server + * @param remote remote sever to contact + * @param password server password + * @param timeout timeout, in milliseconds, to complete a connection. + * @throws IOException + */ public void connect(InetSocketAddress remote, String password, long timeout) throws IOException { try { - if (clientConnectionState != ClientConnectionState.DISCONNECTED) - throw new IllegalStateException(clientConnectionState.name()); + if (connectionState != ClientConnectionState.DISCONNECTED) + throw new IllegalStateException(connectionState.name()); setOption("client.hostname", remote.getHostString()); setOption("client.password", password); getSocket().connect(remote); - setClientConnectionState(ClientConnectionState.CONNECTING); + setConnectionState(ClientConnectionState.CONNECTING); joinConnectionState(ClientConnectionState.CONNECTED, timeout); } catch (Throwable e) { - setClientConnectionState(ClientConnectionState.DISCONNECTED); + setConnectionState(ClientConnectionState.DISCONNECTED); throw new IOException(e); } diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet2Command.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet2Command.java index d4c35a8..f05502c 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet2Command.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet2Command.java @@ -1,26 +1,38 @@ package com.github.manevolent.ts3j.protocol.packet; import com.github.manevolent.ts3j.protocol.ProtocolRole; +import com.github.manevolent.ts3j.util.Ts3Logging; import java.nio.ByteBuffer; +import java.nio.charset.Charset; public class Packet2Command extends Packet { + private String text; + public Packet2Command(ProtocolRole role) { super(PacketType.COMMAND, role); } + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + @Override public void read(ByteBuffer buffer) { - + text = Charset.forName("ASCII").decode(buffer).toString(); } @Override public void write(ByteBuffer buffer) { - + buffer.put(text.getBytes(Charset.forName("ASCII"))); } @Override public int getSize() { - return 0; + return text.length(); } } diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet6Ack.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet6Ack.java new file mode 100644 index 0000000..0ac69d7 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet6Ack.java @@ -0,0 +1,37 @@ +package com.github.manevolent.ts3j.protocol.packet; + +import com.github.manevolent.ts3j.protocol.ProtocolRole; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +public class Packet6Ack extends Packet { + private int packetId; + + public Packet6Ack(ProtocolRole role) { + super(PacketType.ACK, role); + } + + public int getPacketId() { + return packetId; + } + + public void setPacketId(int packetId) { + this.packetId = packetId; + } + + @Override + public void read(ByteBuffer buffer) { + packetId = buffer.getShort() & 0x0000FFFF; + } + + @Override + public void write(ByteBuffer buffer) { + buffer.putShort((short) (getPacketId() & 0xFFFFFF)); + } + + @Override + public int getSize() { + return 2; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketFragment.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketFragment.java new file mode 100644 index 0000000..77822b5 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketFragment.java @@ -0,0 +1,38 @@ +package com.github.manevolent.ts3j.protocol.packet; + +import com.github.manevolent.ts3j.protocol.ProtocolRole; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +public class PacketFragment extends Packet { + private byte[] fragment; + + public PacketFragment( PacketType type, ProtocolRole role) { + super(type, role); + } + + public byte[] getFragment() { + return fragment; + } + + public void setFragment(byte[] fragment) { + this.fragment = fragment; + } + + @Override + public void read(ByteBuffer buffer) { + fragment = new byte[buffer.remaining()]; + buffer.get(fragment); + } + + @Override + public void write(ByteBuffer buffer) { + buffer.put(fragment); + } + + @Override + public int getSize() { + return fragment.length; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketType.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketType.java index 3c5b77e..1b268af 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketType.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketType.java @@ -5,12 +5,11 @@ public enum PacketType { VOICE_WHISPER(0x1, null, null, false, false, false, false), PONG(0x5, null, null, false, false, false, false), PING(0x4, null, PONG, false, false, false, false), - ACK(0x6, null, null, true, true, false, false), + ACK(0x6, Packet6Ack.class, null, true, true, false, false), ACK_LOW(0x7, null, null, true, true, false, false), INIT1(0x8, Packet8Init1.class, null, false, true, false, false), COMMAND_LOW(0x3, null, ACK_LOW, true, true, true, true), - COMMAND(0x2, Packet2Command.class, ACK, true, true, true, true) - ; + COMMAND(0x2, Packet2Command.class, ACK, true, true, true, true); private final int index; private final Class packetClass; diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/channel/PacketChannel.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/channel/PacketChannel.java new file mode 100644 index 0000000..849c248 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/channel/PacketChannel.java @@ -0,0 +1,50 @@ +package com.github.manevolent.ts3j.protocol.packet.channel; + +import com.github.manevolent.ts3j.protocol.NetworkPacket; +import com.github.manevolent.ts3j.protocol.header.HeaderFlag; +import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler; + +import java.io.IOException; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +public final class PacketChannel { + private final Queue receive = new ConcurrentLinkedQueue<>(); + private final ReassemblyQueue reassemblyQueue = new ReassemblyQueue(); + + private PacketHandler handler; + + /** + * Inserts a packet into the receive queue. + * @param networkPacket Packet to insert. + */ + public void insert(NetworkPacket networkPacket) { + if ((networkPacket = reassemblyQueue.put(networkPacket)) != null) + receive.add(networkPacket); + } + + /** + * Finds the next packet in the receive queue available for dequeue. + * @return Network packet, null otherwise + */ + public NetworkPacket next() { + try { + return receive.remove(); + } catch (NoSuchElementException ex) { + return null; + } + } + + public PacketHandler getHandler() { + return handler; + } + + public void setHandler(PacketHandler handler) throws IOException { + if (this.handler != handler) { + if (this.handler != null) this.handler.onUnassigning(); + this.handler = handler; + if (handler != null) handler.onAssigned(); + } + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/channel/ReassemblyQueue.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/channel/ReassemblyQueue.java new file mode 100644 index 0000000..066927d --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/channel/ReassemblyQueue.java @@ -0,0 +1,88 @@ +package com.github.manevolent.ts3j.protocol.packet.channel; + +import com.github.manevolent.ts3j.protocol.NetworkPacket; +import com.github.manevolent.ts3j.protocol.header.HeaderFlag; +import com.github.manevolent.ts3j.protocol.packet.PacketFragment; +import com.github.manevolent.ts3j.protocol.packet.PacketType; + +import java.nio.ByteBuffer; +import java.util.*; + +public class ReassemblyQueue { + private final Map queue = new LinkedHashMap<>(); + private final Map fragmentation = new LinkedHashMap<>(); + + public NetworkPacket reassemble(NetworkPacket lastFragment) { + // Pull out all other packets in the queue before this one which would also be fragmented + List reassemblyList = new ArrayList<>(); + for (int packetId = lastFragment.getHeader().getPacketId();; packetId--) { + NetworkPacket 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); + fragmentation.remove(packetId); // Don't reuse this packet + } + } + + // 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.getPacket().getSize()).sum() + + lastFragment.getPacket().getSize(); + + if (totalLength <= 0) throw new IllegalArgumentException("reassembly too small: " + totalLength); + + ByteBuffer reassemblyBuffer = ByteBuffer.allocate(totalLength); + + for (NetworkPacket old : reassemblyList) + old.writeBody(reassemblyBuffer); + + NetworkPacket firstPacket = reassemblyList.get(0); + NetworkPacket reassembledPacket = new NetworkPacket(firstPacket.getRole()); + reassembledPacket.setHeader(firstPacket.getHeader()); + reassembledPacket.getHeader().setPacketFlag(HeaderFlag.FRAGMENTED, false); + reassembledPacket.readBody((ByteBuffer) reassemblyBuffer.position(0)); + + return reassembledPacket; + } + + public NetworkPacket put(NetworkPacket networkPacket) { + Boolean fragmentState = fragmentation.get(networkPacket.getHeader().getType()); + + if (networkPacket.getHeader().getPacketFlag(HeaderFlag.FRAGMENTED)) { + if (!(networkPacket.getPacket() instanceof PacketFragment)) + throw new IllegalArgumentException("packet fragment object is not instance of PacketFragment class"); + + boolean fragmentationEnded = false; + + if (fragmentState == null) + fragmentation.put(networkPacket.getHeader().getType(), fragmentState = true); + else if (fragmentState) { + // Fragmentation end + fragmentationEnded = true; + + fragmentation.put(networkPacket.getHeader().getType(), fragmentState = false); + } + + queue.put(networkPacket.getHeader().getPacketId(), networkPacket); + + if (fragmentationEnded) + return reassemble(networkPacket); + + return null; + } else { + if (fragmentState != null && fragmentState) { + queue.put(networkPacket.getHeader().getPacketId(), networkPacket); + + return null; + } else { + return networkPacket; + } + } + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java index 37bae27..0d4c75b 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java @@ -52,7 +52,7 @@ public class LocalClientHandlerConnecting extends LocalClientHandler { @Override public void handlePacket(NetworkPacket packet) throws IOException { - Ts3Logging.debug("Connecting: handling " + packet.getClass().getSimpleName()); + Ts3Logging.debug("Connecting: handling " + packet.getPacket().getClass().getSimpleName()); if (packet.getPacket() instanceof Packet8Init1) { Packet8Init1 init1 = (Packet8Init1) packet.getPacket(); @@ -136,7 +136,7 @@ public class LocalClientHandlerConnecting extends LocalClientHandler { } } else if (packet.getPacket() instanceof Packet2Command) { - + Ts3Logging.debug(((Packet2Command) packet.getPacket()).getText()); } } } diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/transformation/PacketTransformation.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/transformation/PacketTransformation.java index 0bb7d87..08f4b46 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/transformation/PacketTransformation.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/transformation/PacketTransformation.java @@ -1,5 +1,6 @@ package com.github.manevolent.ts3j.protocol.packet.transformation; +import com.github.manevolent.ts3j.protocol.NetworkPacket; import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.header.PacketHeader; import com.github.manevolent.ts3j.util.Ts3Logging; @@ -17,6 +18,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class PacketTransformation { + private static final int MAC_LEN = 8; private static MessageDigest SHA256; { @@ -70,33 +72,34 @@ public class PacketTransformation { return new Pair<>(key, nonce); } - public byte[] encrypt(PacketHeader header, byte[] body) { - return transform(header, body, true); - } + public ByteBuffer encrypt(NetworkPacket packet) { + Pair parameters = computeParameters(packet.getHeader()); - public byte[] decrypt(PacketHeader header, byte[] body) { - return transform(header, body, false); - } + // Write header (this is temporary) + ByteBuffer headerBuffer = packet.writeHeader(ByteBuffer.allocate(packet.getHeader().getSize())); - private byte[] transform(PacketHeader header, byte[] body, boolean encrypt) { - Pair parameters = computeParameters(header); + // Get the header without a MAC for the associated text field + byte[] headerWithoutMac = new byte[packet.getHeader().getSize() - MAC_LEN]; + System.arraycopy(headerBuffer.array(), MAC_LEN, headerWithoutMac, 0, headerWithoutMac.length); - byte[] associatedText = new byte[8]; - - CipherParameters cipherParameters = new AEADParameters(new KeyParameter( - parameters.getKey()), - 8 * associatedText.length, + CipherParameters cipherParameters = new AEADParameters( + new KeyParameter(parameters.getKey()), + 8 * MAC_LEN, parameters.getValue(), - encrypt ? associatedText : header.getMac() + headerWithoutMac ); + int dataLen = packet.getPacket().getSize(); + ByteBuffer packetBuffer = packet.writeBody(ByteBuffer.allocate(dataLen)); + byte[] result; int len; - synchronized (cipher){ - cipher.init(encrypt, cipherParameters); - result = new byte[cipher.getOutputSize(body.length)]; - len = cipher.processBytes(body, 0, body.length, result, 0); + synchronized (cipher) { + cipher.init(true, cipherParameters); + + result = new byte[cipher.getOutputSize(dataLen)]; + len = cipher.processBytes(packetBuffer.array(), 0, dataLen, result, 0); try { len += cipher.doFinal(result, len); @@ -105,12 +108,56 @@ public class PacketTransformation { } } - byte[] output = new byte[len]; - System.arraycopy(result, 0, output, 0, len); + ByteBuffer outputBuffer = ByteBuffer.allocate(MAC_LEN + headerWithoutMac.length + (len - MAC_LEN)); - if (encrypt) - header.setMac(associatedText); + // MAC + outputBuffer.put(result, len - MAC_LEN, MAC_LEN); - return output; + // Rest of header + outputBuffer.put(headerWithoutMac); + + // Encrypted body + outputBuffer.put(result, 0, len - MAC_LEN); + + return outputBuffer; + } + + public byte[] decrypt(PacketHeader header, ByteBuffer buffer, int dataLen) { + Pair parameters = computeParameters(header); + + byte[] headerWithoutMac = new byte[header.getSize() - MAC_LEN]; + System.arraycopy(buffer.array(), MAC_LEN, headerWithoutMac, 0, headerWithoutMac.length); + + // Get the header without a MAC for the associated text field + CipherParameters cipherParameters = new AEADParameters( + new KeyParameter(parameters.getKey()), + 8 * MAC_LEN, + parameters.getValue(), + headerWithoutMac + ); + + byte[] result; + int len; + + synchronized (cipher) { + + cipher.init(false, cipherParameters); + result = new byte[cipher.getOutputSize(dataLen + MAC_LEN)]; + + len = cipher.processBytes(buffer.array(), header.getSize(), dataLen, result, 0); + len += cipher.processBytes(buffer.array(), 0, MAC_LEN, result, len); + try { + len += cipher.doFinal(result, len); + } catch (InvalidCipherTextException e) { + throw new RuntimeException(e); + } + + if (len != dataLen) + throw new IllegalArgumentException(len + " != " + dataLen); + + } + + + return result; } } diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/socket/LocalTeamspeakSocket.java b/src/main/java/com/github/manevolent/ts3j/protocol/socket/LocalTeamspeakSocket.java index 701666f..5875eb2 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/socket/LocalTeamspeakSocket.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/socket/LocalTeamspeakSocket.java @@ -5,6 +5,8 @@ import com.github.manevolent.ts3j.protocol.SocketRole; import com.github.manevolent.ts3j.protocol.header.HeaderFlag; import com.github.manevolent.ts3j.protocol.header.PacketHeader; import com.github.manevolent.ts3j.protocol.packet.Packet; +import com.github.manevolent.ts3j.protocol.packet.PacketFragment; +import com.github.manevolent.ts3j.protocol.packet.PacketType; import com.github.manevolent.ts3j.util.Ts3Logging; import java.io.IOException; @@ -78,28 +80,25 @@ public class LocalTeamspeakSocket extends AbstractTeamspeakSocket implements Tea header.getRole().name() + " != " + getSocketRole().getOut().name()); + // Ensure type matches + header.setType(packet.getType()); + + // Construct a network packet NetworkPacket networkPacket = new NetworkPacket(header.getRole()); networkPacket.setHeader(header); networkPacket.setPacket(packet); - if (networkPacket.getHeader().getMac() == null) { - // Generate mac? - } - - ByteBuffer outputBuffer = ByteBuffer.allocate(networkPacket.getSize()); - outputBuffer.order(ByteOrder.BIG_ENDIAN); - networkPacket.writeHeader(outputBuffer); + // Flush to a buffer + ByteBuffer outputBuffer; if (networkPacket.getHeader().getType().isEncrypted() && !networkPacket.getHeader().getPacketFlag(HeaderFlag.UNENCRYPTED)) { - ByteBuffer encryptionBuffer = ByteBuffer.allocate(500); - networkPacket.writeBody(encryptionBuffer); - - byte[] unencryptedBody = new byte[encryptionBuffer.position()]; - System.arraycopy(encryptionBuffer.array(), 0, unencryptedBody, 0, unencryptedBody.length); - - outputBuffer.put(getPacketTransformation().encrypt(header, unencryptedBody)); + outputBuffer = getPacketTransformation().encrypt(networkPacket); } else { + outputBuffer = ByteBuffer.allocate(networkPacket.getSize()); + + outputBuffer.order(ByteOrder.BIG_ENDIAN); + networkPacket.writeHeader(outputBuffer); networkPacket.writeBody(outputBuffer); } @@ -117,7 +116,7 @@ public class LocalTeamspeakSocket extends AbstractTeamspeakSocket implements Tea datagramSocket.receive(datagramPacket); Ts3Logging.debug( - "[NETWORK] READ Len=" + datagramPacket.getLength() + " to " + datagramPacket.getSocketAddress() + + "[NETWORK] READ Len=" + datagramPacket.getLength() + " from " + datagramPacket.getSocketAddress() + "\n" + Ts3Logging.getHex(datagramPacket.getData(), datagramPacket.getLength()) ); @@ -125,27 +124,51 @@ public class LocalTeamspeakSocket extends AbstractTeamspeakSocket implements Tea } public NetworkPacket receive() throws IOException { - DatagramPacket datagramPacket = receiveNetworkPacket(); + while (true) { + DatagramPacket datagramPacket = receiveNetworkPacket(); - NetworkPacket networkPacket = new NetworkPacket(getSocketRole().getIn()); + NetworkPacket networkPacket = new NetworkPacket(getSocketRole().getIn()); - ByteBuffer buffer = ByteBuffer.wrap(datagramPacket.getData()); - networkPacket.readHeader(buffer); + ByteBuffer buffer = ByteBuffer.wrap(datagramPacket.getData()); + networkPacket.readHeader(buffer); - Ts3Logging.debug("[PROTOCOL] READ " + networkPacket.getHeader().getType().name()); + boolean fragment = networkPacket.getHeader().getPacketFlag(HeaderFlag.FRAGMENTED); + if (fragment && !networkPacket.getHeader().getType().isSplittable()) continue; // drop packet - if (networkPacket.getHeader().getType().isEncrypted()) { - byte[] transformBuffer = new byte[buffer.remaining()]; - buffer.get(transformBuffer); + ByteBuffer packetBuffer; - networkPacket.readBody(ByteBuffer.wrap( - getPacketTransformation().decrypt(networkPacket.getHeader(), transformBuffer) - )); - } else { - networkPacket.readBody(buffer); + if (networkPacket.getHeader().getType().isEncrypted()) { + packetBuffer = ByteBuffer.wrap( + getPacketTransformation().decrypt( + networkPacket.getHeader(), + buffer, + datagramPacket.getLength() - networkPacket.getHeader().getSize() + ) + ); + } else { + packetBuffer = buffer; + } + + if (fragment) { + Ts3Logging.debug("[PROTOCOL] READ " + + networkPacket.getHeader().getType().name() + " fragment"); + + networkPacket.setPacket( + new PacketFragment( + networkPacket.getHeader().getType(), + getSocketRole().getIn() + ) + ); + } else { + Ts3Logging.debug("[PROTOCOL] READ " + + networkPacket.getHeader().getType().name()); + } + + + networkPacket.readBody(packetBuffer); + + return networkPacket; } - - return networkPacket; } } diff --git a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java index 459c535..0b094f2 100644 --- a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java +++ b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java @@ -27,7 +27,7 @@ public class ServerConnectionTest extends TestCase { 10000L ); - assertEquals(client.getClientConnectionState(), ClientConnectionState.CONNECTED); + assertEquals(client.getConnectionState(), ClientConnectionState.CONNECTED); } catch (Throwable ex) { ex.printStackTrace(); }