From 44a18ad7a03070aaf9d41de8af349593f2ff547a Mon Sep 17 00:00:00 2001 From: Manevolent Date: Tue, 31 Jul 2018 15:39:03 -0400 Subject: [PATCH] Beginning to work on audio stuff now. Lots of changes. --- .../manevolent/ts3j/audio/AudioEncoding.java | 7 + .../manevolent/ts3j/audio/Microphone.java | 15 + .../event/ClientChannelGroupChangedEvent.java | 57 ++++ .../manevolent/ts3j/event/TS3Listener.java | 3 + .../manevolent/ts3j/protocol/RingQueue.java | 164 ++++++++++ .../ts3j/protocol/packet/PacketBody5Pong.java | 1 + .../client/LocalClientHandlerConnecting.java | 9 +- .../transformation/PacketTransformation.java | 9 +- .../client/AbstractTeamspeakClientSocket.java | 287 +++++++++++++----- .../client/LocalTeamspeakClientSocket.java | 20 +- .../ts3j/util/HighPrecisionRecurrentTask.java | 82 +++++ .../github/manevolent/ts3j/util/QuickLZ.java | 2 +- .../manevolent/ts3j/util/Ts3Debugging.java | 24 +- .../manevolent/ts3j/ServerConnectionTest.java | 4 +- 14 files changed, 592 insertions(+), 92 deletions(-) create mode 100644 src/main/java/com/github/manevolent/ts3j/audio/AudioEncoding.java create mode 100644 src/main/java/com/github/manevolent/ts3j/audio/Microphone.java create mode 100644 src/main/java/com/github/manevolent/ts3j/event/ClientChannelGroupChangedEvent.java create mode 100644 src/main/java/com/github/manevolent/ts3j/protocol/RingQueue.java create mode 100644 src/main/java/com/github/manevolent/ts3j/util/HighPrecisionRecurrentTask.java diff --git a/src/main/java/com/github/manevolent/ts3j/audio/AudioEncoding.java b/src/main/java/com/github/manevolent/ts3j/audio/AudioEncoding.java new file mode 100644 index 0000000..2653023 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/audio/AudioEncoding.java @@ -0,0 +1,7 @@ +package com.github.manevolent.ts3j.audio; + +public enum AudioEncoding { + OPUS, + SPEEX, + PCM +} diff --git a/src/main/java/com/github/manevolent/ts3j/audio/Microphone.java b/src/main/java/com/github/manevolent/ts3j/audio/Microphone.java new file mode 100644 index 0000000..e702718 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/audio/Microphone.java @@ -0,0 +1,15 @@ +package com.github.manevolent.ts3j.audio; + +public interface Microphone { + default boolean isMuted() { + return false; + } + + boolean isReady(); + + default AudioEncoding getEncoding() { + return AudioEncoding.PCM; + } + + byte[] provide(); +} diff --git a/src/main/java/com/github/manevolent/ts3j/event/ClientChannelGroupChangedEvent.java b/src/main/java/com/github/manevolent/ts3j/event/ClientChannelGroupChangedEvent.java new file mode 100644 index 0000000..9f15380 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/event/ClientChannelGroupChangedEvent.java @@ -0,0 +1,57 @@ +package com.github.manevolent.ts3j.event; + +/* + * #%L + * TeamSpeak 3 Java API + * %% + * Copyright (C) 2014 Bert De Geyter + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Map; + +public class ClientChannelGroupChangedEvent extends BaseEvent { + + public ClientChannelGroupChangedEvent(Map map) { + super(map); + } + + public int getClientId() { + return getInt("clid"); + } + + public int getChannelGroupId() { + return getInt("cgid"); + } + + public int getChannelId() { + return getInt("cid"); + } + + public int getInheritedChannelId() { + return getInt("cgi"); + } + + @Override + public void fire(TS3Listener listener) { + listener.onClientChannelGroupChanged(this); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/event/TS3Listener.java b/src/main/java/com/github/manevolent/ts3j/event/TS3Listener.java index 60572e1..41c20c9 100644 --- a/src/main/java/com/github/manevolent/ts3j/event/TS3Listener.java +++ b/src/main/java/com/github/manevolent/ts3j/event/TS3Listener.java @@ -57,4 +57,7 @@ public interface TS3Listener { void onServerGroupList(ServerGroupListEvent e); void onClientNeededPermissions(ClientNeededPermissionsEvent e); + + void onClientChannelGroupChanged(ClientChannelGroupChangedEvent e); + } diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/RingQueue.java b/src/main/java/com/github/manevolent/ts3j/protocol/RingQueue.java new file mode 100644 index 0000000..4b669e4 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/RingQueue.java @@ -0,0 +1,164 @@ +package com.github.manevolent.ts3j.protocol; + +public class RingQueue { + private static final int + NOT_SET = 0x00, + SET = 0x01, + OUT_OF_WINDOW = 0x00, + IN_WINDOW = 0x10; + + private boolean[] ringBufferSet; + + private final int initialBufferSize = 16; + private int currentStart, currentLength; + + private int mappedBaseOffset; + private final int mappedMod; + private int generation; + + private final int maxBufferSize; + + public RingQueue(int maxBufferSize, int mod) { + if (maxBufferSize == -1) + { + this.maxBufferSize = (mod / 2) - 1; + } + else + { + if (maxBufferSize >= mod) + throw new IllegalArgumentException("Modulo must be bigger than buffer size"); + + this.maxBufferSize = maxBufferSize; + } + + int setBufferSize = Math.min(initialBufferSize, maxBufferSize); + ringBufferSet = new boolean[setBufferSize]; + mappedMod = mod; + + clear(); + } + + private void bufferSet(int index) { + bufferExtend(index); + int local = indexToLocal(index); + int newLength = local - currentStart + 1 + (local >= currentStart ? 0 : ringBufferSet.length); + currentLength = Math.max(currentLength, newLength); + ringBufferSet[local] = true; + } + + private boolean stateGet(int index) { + bufferExtend(index); + int local = indexToLocal(index); + return ringBufferSet[local]; + } + + private void bufferPop() { + ringBufferSet[currentStart] = false; + + currentStart = (currentStart + 1) % ringBufferSet.length; + currentLength --; + } + + private void bufferExtend(int index) { + if (index < ringBufferSet.length) + return; + if (index >= maxBufferSize) + throw new IllegalArgumentException("the index does not fit into the maximal buffer size"); + int extendto = index < ringBufferSet.length * 2 + ? Math.min(ringBufferSet.length * 2, maxBufferSize) + : Math.min(index + ringBufferSet.length, maxBufferSize); + + Object[] extRingBuffer = new Object[extendto]; + boolean[] extRingBufferSet = new boolean[extendto]; + + System.arraycopy(ringBufferSet, currentStart, extRingBufferSet, 0, ringBufferSet.length - currentStart); + System.arraycopy(ringBufferSet, 0, extRingBufferSet, ringBufferSet.length - currentStart, currentStart); + + currentStart = 0; + ringBufferSet = extRingBufferSet; + } + + private int indexToLocal(int index) { + return (currentStart + index) % ringBufferSet.length; + } + + public void clear() + { + currentStart = 0; + currentLength = 0; + for (int i = 0; i < ringBufferSet.length; i ++) ringBufferSet[i] = false; + mappedBaseOffset = 0; + generation = 0; + } + + public void set(int mappedValue) { + int index = mappedToIndex(mappedValue); + + if (isSetIndex(index) != ItemSetStatus.IN_WINDOW_NOT_SET) + throw new IllegalStateException("object cannot be set"); + + bufferSet(index); + } + + private int mappedToIndex(int mappedValue) { + if (mappedValue >= mappedMod) + throw new ArrayIndexOutOfBoundsException("mappedValue"); + + if (isNextGen(mappedValue)) { + return (mappedValue + mappedMod) - mappedBaseOffset; + } else { + return mappedValue - mappedBaseOffset; + } + } + + public boolean isNextGen(int mappedValue) { + return mappedBaseOffset > mappedMod - maxBufferSize && mappedValue < maxBufferSize; + } + + public int getGeneration(int mappedValue) { + return generation + (isNextGen(mappedValue) ? 1 : 0); + } + + private ItemSetStatus isSetIndex(int index) { + if (index < 0) + return ItemSetStatus.OUT_OF_WINDOW_SET; + else if (index > currentLength && index < maxBufferSize) + return ItemSetStatus.IN_WINDOW_NOT_SET; + else if (index >= maxBufferSize) + return ItemSetStatus.OUT_OF_WINDOW_NOT_SET; + + return stateGet(index) ? ItemSetStatus.IN_WINDOW_SET : ItemSetStatus.IN_WINDOW_NOT_SET; + } + + public int getCount() { + return currentLength; + } + + public boolean isSet(int packetId) { + return isSetIndex(packetId) == ItemSetStatus.IN_WINDOW_SET; + } + + public enum ItemSetStatus { + OUT_OF_WINDOW_NOT_SET(0x00), + OUT_OF_WINDOW_SET(0x01), + IN_WINDOW_NOT_SET(0x10), + IN_WINDOW_SET(0x11); + + private final int index; + + ItemSetStatus(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static ItemSetStatus fromId(int index) { + for (ItemSetStatus value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody5Pong.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody5Pong.java index 1a75fd0..1f80f38 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody5Pong.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody5Pong.java @@ -5,6 +5,7 @@ import com.github.manevolent.ts3j.protocol.header.HeaderFlag; import com.github.manevolent.ts3j.protocol.header.PacketHeader; import java.nio.ByteBuffer; +import java.nio.ByteOrder; public class PacketBody5Pong extends PacketBody { private int packetId; 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 cd194a8..b99d34f 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 @@ -141,6 +141,8 @@ 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")) { @@ -186,18 +188,19 @@ public class LocalClientHandlerConnecting extends LocalClientHandler { getClient().setSecureParameters(Ts3Crypt.cryptoInit2(license, alphaBytes, beta, keyPair.getValue())); + alphaBytes = null; + getClient().writePacket(new PacketBody2Command( ProtocolRole.CLIENT, new SingleCommand( "clientinit", ProtocolRole.CLIENT, new CommandSingleParameter("client_nickname", getClient().getNickname()), - new CommandSingleParameter("client_version", "3.1.8 [Build: 1516614607]"), + new CommandSingleParameter("client_version", "3.?.? [Build: 5680278000]"), new CommandSingleParameter("client_platform", "Windows"), new CommandSingleParameter( "client_version_sign", - "LJ5q+KWT4KwBX7oR/9j9A12hBrq5ds5ony99" + - "f9kepNmqFskhT7gfB51bAJNgAMOzXVCeaItNmc10F2wUNktqCw==" + "DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg==" ), new CommandSingleParameter("client_input_hardware", "1"), new CommandSingleParameter("client_output_hardware", "1"), 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 66ad968..c0f6161 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 @@ -119,7 +119,8 @@ public class PacketTransformation { return outputBuffer; } - public byte[] decrypt(PacketHeader header, ByteBuffer buffer, int dataLen) { + public byte[] decrypt(PacketHeader header, ByteBuffer buffer, int dataLen) + throws InvalidCipherTextException { Pair parameters = computeParameters(header); byte[] headerWithoutMac = new byte[header.getSize() - MAC_LEN]; @@ -142,11 +143,7 @@ public class PacketTransformation { 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); - } + len += cipher.doFinal(result, len); if (len != dataLen) throw new IllegalArgumentException(len + " != " + dataLen); diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java index 1fba3bb..94387ac 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java @@ -4,6 +4,7 @@ import com.github.manevolent.ts3j.command.*; import com.github.manevolent.ts3j.identity.Identity; import com.github.manevolent.ts3j.protocol.NetworkPacket; import com.github.manevolent.ts3j.protocol.Packet; +import com.github.manevolent.ts3j.protocol.RingQueue; import com.github.manevolent.ts3j.protocol.SocketRole; import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.header.ClientPacketHeader; @@ -19,9 +20,11 @@ import com.github.manevolent.ts3j.util.QuickLZ; import com.github.manevolent.ts3j.util.Ts3Crypt; import com.github.manevolent.ts3j.util.Ts3Debugging; import javafx.util.Pair; +import org.bouncycastle.crypto.InvalidCipherTextException; import java.io.IOException; import java.net.DatagramPacket; +import java.net.SocketTimeoutException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.*; @@ -39,8 +42,10 @@ public abstract class AbstractTeamspeakClientSocket private final Map reassemblyQueue = new ConcurrentHashMap<>(); private final Map clientOptions = new ConcurrentHashMap<>(); private final Map sendQueue = new ConcurrentHashMap<>(); + private final Map sendQueueLow = new ConcurrentHashMap<>(); + private final Map localSendCounter = new ConcurrentHashMap<>(); - private final Map remoteSendCounter = new ConcurrentHashMap<>(); + private final Map remoteQueue = new ConcurrentHashMap<>(); private CommandProcessor commandProcessor; @@ -53,6 +58,8 @@ public abstract class AbstractTeamspeakClientSocket private int clientId = 0; private int packetId = 0; + private long lastPing; + private final NetworkReader networkReader = new NetworkReader(); private final NetworkHandler networkHandler = new NetworkHandler(); private Thread networkThread = null; @@ -75,14 +82,15 @@ public abstract class AbstractTeamspeakClientSocket reassemblyQueue.put(bodyType, new Reassembly()); // Initialize counters - for (PacketBodyType bodyType : PacketBodyType.values()) + for (PacketBodyType bodyType : PacketBodyType.values()) { if (bodyType == PacketBodyType.INIT1) { - localSendCounter.put(PacketBodyType.INIT1, new PacketCounterZero()); - remoteSendCounter.put(PacketBodyType.INIT1, new PacketCounterZero()); + localSendCounter.put(bodyType, new PacketCounterZero()); } else { - localSendCounter.put(bodyType, new PacketCounterFull()); - remoteSendCounter.put(bodyType, new PacketCounterFull(-1)); + localSendCounter.put(bodyType, new PacketCounterFull(true)); } + + remoteQueue.put(bodyType, new RingQueue(100, 65536)); + } } protected void start() { @@ -104,7 +112,7 @@ public abstract class AbstractTeamspeakClientSocket setTransformation(new PacketTransformation(parameters.getIvStruct(), parameters.getFakeSignature())); } - protected abstract NetworkPacket readNetworkPacket() throws IOException; + protected abstract NetworkPacket readNetworkPacket(int timeout) throws IOException; protected abstract void writeNetworkPacket(NetworkPacket packet) throws IOException; @@ -166,21 +174,93 @@ public abstract class AbstractTeamspeakClientSocket return connectionState == ClientConnectionState.CONNECTED; } - @Override - public void writePacket(PacketBody body) throws IOException, TimeoutException { + private PacketHeader newOutgoingHeader() { PacketHeader header; try { - header = body.getRole().getHeaderClass().newInstance(); + header = getRole().getOut().getHeaderClass().newInstance(); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } + return header; + } + + @Override + public void writePacket(PacketBody body) throws IOException, TimeoutException { // Construct a network packet Packet packet = new Packet(body.getRole()); - packet.setHeader(header); packet.setBody(body); + // Generate header + PacketHeader header = newOutgoingHeader(); + + // Ensure type matches + header.setType(packet.getBody().getType()); + + // Set header values + body.setHeaderValues(header); + + if (header instanceof ClientPacketHeader) + ((ClientPacketHeader) header).setClientId(clientId); + + // Count packet + Pair counter; + + switch (header.getType()) { + case ACK: + counter = new Pair<>( + ((PacketBody6Ack)packet.getBody()).getPacketId(), + remoteQueue.get(PacketBodyType.ACK).getGeneration( + ((PacketBody6Ack) packet.getBody()).getPacketId() + ) + ); + break; + case ACK_LOW: + counter = new Pair<>( + ((PacketBody7AckLow)packet.getBody()).getPacketId(), + remoteQueue.get(PacketBodyType.ACK_LOW).getGeneration( + ((PacketBody7AckLow) packet.getBody()).getPacketId() + ) + ); + break; + case PONG: + counter = new Pair<>( + ((PacketBody5Pong)packet.getBody()).getPacketId(), + remoteQueue.get(PacketBodyType.PONG).getGeneration( + ((PacketBody5Pong) packet.getBody()).getPacketId() + ) + ); + break; + /*case PING: + counter = localSendCounter.get(PacketBodyType.PING).current(); + break;*/ + case INIT1: + counter = new Pair<>(101, 0); + break; + default: + counter = localSendCounter.get(header.getType()).next(); + break; + } + + header.setPacketId(counter.getKey()); + 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; + } + + packet.setHeader(header); + writePacket(packet); } @@ -193,30 +273,6 @@ public abstract class AbstractTeamspeakClientSocket packet.getHeader().getRole().name() + " != " + getRole().getOut().name()); - // Ensure type matches - packet.getHeader().setType(packet.getBody().getType()); - - // Set header values - packet.getBody().setHeaderValues(packet.getHeader()); - - Pair counterResult = localSendCounter.get(packet.getHeader().getType()).next(); - if (packet.getHeader() instanceof ClientPacketHeader) - ((ClientPacketHeader) packet.getHeader()).setClientId(clientId); - - packet.getHeader().setPacketId(counterResult.getKey()); - packet.getHeader().setGeneration(counterResult.getValue()); - - switch (packet.getHeader().getType()) { - case INIT1: - packet.getHeader().setPacketFlag(HeaderFlag.UNENCRYPTED, true); - packet.getHeader().setPacketId((short)101); - break; - case COMMAND: - case COMMAND_LOW: - packet.getHeader().setPacketFlag(HeaderFlag.NEW_PROTOCOL, true); - break; - } - if (packet.getSize() > 500) { int totalSize = packet.getBody().getSize(); @@ -274,6 +330,10 @@ public abstract class AbstractTeamspeakClientSocket ByteBuffer outputBuffer; if (!packet.getHeader().getPacketFlag(HeaderFlag.UNENCRYPTED)) { + Ts3Debugging.debug("[PROTOCOL] ENCRYPT " + + packet.getHeader().getType().name() + " generation=" + + packet.getHeader().getGeneration()); + outputBuffer = getTransformation().encrypt(packet); } else { if (packet.getHeader().getType() == PacketBodyType.INIT1) { @@ -315,7 +375,7 @@ public abstract class AbstractTeamspeakClientSocket writeNetworkPacket(networkPacket); // If necessary, fulfill promise for the packet acknowledgement or throw TimeoutException - if (response != null) { + if (response != null && networkPacket.getHeader().getType() != PacketBodyType.PING) { while (response.getRetries() < response.getMaxTries()) { try { if (getState() == ClientConnectionState.DISCONNECTED) @@ -344,6 +404,11 @@ public abstract class AbstractTeamspeakClientSocket private Packet readPacketIntl(NetworkPacket networkPacket) throws IOException { if (networkPacket.getHeader() == null) throw new NullPointerException("header"); + RingQueue ring = remoteQueue.get(networkPacket.getHeader().getType()); + int currentGeneration = ring == null ? 0 : ring.getGeneration(networkPacket.getHeader().getPacketId()); + + networkPacket.getHeader().setGeneration(currentGeneration); + Packet packet = new Packet(networkPacket.getHeader().getRole()); packet.setHeader(networkPacket.getHeader()); @@ -366,18 +431,25 @@ public abstract class AbstractTeamspeakClientSocket " is unencrypted, but must be encrypted" ); - ByteBuffer packetBuffer = - (ByteBuffer) networkPacket.getBuffer().position(networkPacket.getHeader().getSize()); + ByteBuffer packetBuffer = networkPacket.getBuffer(); // Decrypt if (encrypted) { - packetBuffer = ByteBuffer.wrap( - getTransformation().decrypt( - networkPacket.getHeader(), - packetBuffer, - networkPacket.getDatagram().getLength() - networkPacket.getHeader().getSize() - ) - ); + Ts3Debugging.debug("[PROTOCOL] DECRYPT " + networkPacket.getHeader().getType().name() + + " generation=" + currentGeneration); + + try { + packetBuffer = ByteBuffer.wrap( + getTransformation().decrypt( + networkPacket.getHeader(), + packetBuffer, + networkPacket.getDatagram().getLength() - + networkPacket.getHeader().getSize() + ) + ).order(ByteOrder.BIG_ENDIAN); + } catch (InvalidCipherTextException e) { + throw new IOException("failed to decrypt " + networkPacket.getHeader().getType().name(), e); + } } // Fragment handling @@ -399,7 +471,9 @@ public abstract class AbstractTeamspeakClientSocket if (packet.getBody() instanceof PacketBodyCompressed) { byte[] decompressed = QuickLZ.decompress(((PacketBodyCompressed) packet.getBody()).getCompressed()); - packetBuffer = ByteBuffer.wrap(decompressed); + packetBuffer = ByteBuffer + .wrap(decompressed) + .order(ByteOrder.BIG_ENDIAN); packet = new Packet(packet.getRole(), packet.getHeader()); @@ -413,43 +487,89 @@ public abstract class AbstractTeamspeakClientSocket } @Override - public Packet readPacket() throws IOException, TimeoutException { + public Packet readPacket() throws + IOException, + TimeoutException { + NetworkPacket networkPacket; + long timeout; + while (isReading()) { - Packet packet = readPacketIntl(readNetworkPacket()); + try { + timeout = getState() == ClientConnectionState.CONNECTED ? + Math.min(1000L, 1000L - (System.currentTimeMillis() - lastPing)) : + Integer.MAX_VALUE; + + if (timeout <= 0) throw new SocketTimeoutException(); + + networkPacket = readNetworkPacket((int) timeout); + } catch (SocketTimeoutException e) { + if (getState() == ClientConnectionState.CONNECTED) { + writePacket(new PacketBody4Ping(getRole().getOut())); + lastPing = System.currentTimeMillis(); + } + + continue; + } + + Packet packet = readPacketIntl(networkPacket); // Find if the packet must be acknowledged - PacketBodyType ackType = packet.getHeader().getType().getAcknolwedgedBy(); + PacketBodyType ackType = packet + .getHeader() + .getType() + .getAcknolwedgedBy(); + if (ackType != null) { switch (ackType) { case ACK: + Ts3Debugging.debug("[PROTOCOL] Acknowledge " + packet + " " + packet.getHeader().getType().name() + + " id=" + packet.getHeader().getPacketId()); writePacket(new PacketBody6Ack(getRole().getOut(), packet.getHeader().getPacketId())); break; case ACK_LOW: writePacket(new PacketBody7AckLow(getRole().getOut(), packet.getHeader().getPacketId())); break; case PONG: - writePacket(new PacketBody5Pong(getRole().getOut(), packet.getHeader().getPacketId())); + Packet pong = new Packet( + getRole().getOut(), + newOutgoingHeader() + ); + + if (pong.getHeader() instanceof ClientPacketHeader) + ((ClientPacketHeader) pong.getHeader()).setClientId(getClientId()); + + pong.getHeader().setPacketFlag(HeaderFlag.UNENCRYPTED, true); + pong.getHeader().setPacketId(packet.getHeader().getPacketId()); + pong.setBody(new PacketBody5Pong(getRole().getOut(), packet.getHeader().getPacketId())); + + writePacket(pong); + continue; // Don't pass pings to the parent } } + if (packet.getHeader().getType().canResend() && + packet.getHeader().getType() != PacketBodyType.INIT1) { + // Find if we already acknowledged this packet + RingQueue ring = remoteQueue.get(packet.getHeader().getType()); + if (ring.isSet(packet.getHeader().getPacketId())) { + Ts3Debugging.debug("[PROTOCOL] Ignoring handling resent packet: " + + packet + " " + + packet.getHeader().getType().name() + " id=" + + packet.getHeader().getPacketId()); + + continue; // Drop the packet + } else { + ring.set(packet.getHeader().getPacketId()); + } + } + // Find if the packet must be reassembled if (packet.getHeader().getType().isSplittable()) { packet = reassemblyQueue.get(packet.getHeader().getType()).put(packet); if (packet == null) continue; } - if (packet.getHeader().getType().canResend() && packet.getHeader().getType() != PacketBodyType.INIT1) - // Find if we already acknowledged this packet - if (!remoteSendCounter - .get(packet.getHeader().getType()) - .setPacketId(packet.getHeader().getPacketId())) { - Ts3Debugging.debug("[PROTOCOL] Ignoring handling resent packet: " + - packet.getHeader().getType().name() + " id=" + - packet.getHeader().getPacketId()); - continue; // Drop the packet - } - // Find if the packet is itself an acknowledgement boolean handle; PacketResponse response; @@ -459,7 +579,11 @@ public abstract class AbstractTeamspeakClientSocket handle = false; break; case ACK_LOW: - response = sendQueue.get(((PacketBody7AckLow)packet.getBody()).getPacketId()); + response = sendQueueLow.get(((PacketBody7AckLow)packet.getBody()).getPacketId()); + handle = false; + break; + case PONG: + response = null; handle = false; break; default: @@ -680,6 +804,11 @@ public abstract class AbstractTeamspeakClientSocket int getPacketId(); boolean setPacketId(int packetId); int getGenerationId(); + void setGenerationId(int i); + + default Pair current() { + return new Pair<>(getPacketId(), getGenerationId()); + } } private class PacketCounterZero implements PacketCounter { @@ -702,18 +831,26 @@ public abstract class AbstractTeamspeakClientSocket public int getGenerationId() { return 0; } + + @Override + public void setGenerationId(int i) { + throw new UnsupportedOperationException(); + } } private class PacketCounterFull implements PacketCounter { private final Object sendLock = new Object(); + private final boolean counting; private int packetId = 0; private int generationId = 0; - public PacketCounterFull() { - + public PacketCounterFull(boolean counting) { + this.counting = counting; } - public PacketCounterFull(int start) { + public PacketCounterFull(int start, boolean counting) { + this.counting = counting; + setPacketId(start); } @@ -727,8 +864,8 @@ public abstract class AbstractTeamspeakClientSocket if (this.packetId == packetId) return false; if (packetId >= 65536) { - packetId = 0; - generationId++; + this.packetId = 0; // wrap around + if (isCounting()) setGenerationId(getGenerationId() + 1); } else { this.packetId = packetId; } @@ -741,13 +878,25 @@ public abstract class AbstractTeamspeakClientSocket return generationId; } + @Override + public void setGenerationId(int i) { + synchronized (sendLock) { + Ts3Debugging.debug("setGenerationId: " + i); + this.generationId = i; + } + } + @Override public Pair next() { synchronized (sendLock) { - setPacketId(packetId + 1); - return new Pair<>(packetId, generationId); + setPacketId(getPacketId() + 1); + return current(); } } + + public boolean isCounting() { + return counting; + } } private class Reassembly { diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java index d601b36..fed05e7 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java @@ -16,10 +16,7 @@ import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command; import com.github.manevolent.ts3j.util.Ts3Debugging; import java.io.IOException; -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.InetSocketAddress; -import java.net.SocketException; +import java.net.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.*; @@ -67,10 +64,12 @@ public class LocalTeamspeakClientSocket } @Override - protected NetworkPacket readNetworkPacket() throws IOException { - socket.receive(packet); + protected NetworkPacket readNetworkPacket(int timeout) throws + IOException, + SocketTimeoutException { - Ts3Debugging.debug("[NETWORK] READ len=" + packet.getLength() + " from " + packet.getSocketAddress()); + socket.setSoTimeout(timeout); + socket.receive(packet); PacketHeader header; @@ -86,6 +85,11 @@ public class LocalTeamspeakClientSocket header.read(buffer); + Ts3Debugging.debug("[NETWORK] READ" + + " id=" + header.getPacketId() + + " len=" + packet.getLength() + + " from " + packet.getSocketAddress()); + return new NetworkPacket( packet, header, @@ -325,6 +329,8 @@ public class LocalTeamspeakClientSocket public void process(AbstractTeamspeakClientSocket client, SingleCommand singleCommand) throws CommandProcessException { try { + setClientId(Integer.parseInt(singleCommand.get("aclid").getValue())); + setState(ClientConnectionState.CONNECTED); } catch (Exception e) { throw new CommandProcessException(e); diff --git a/src/main/java/com/github/manevolent/ts3j/util/HighPrecisionRecurrentTask.java b/src/main/java/com/github/manevolent/ts3j/util/HighPrecisionRecurrentTask.java new file mode 100644 index 0000000..6b3114c --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/util/HighPrecisionRecurrentTask.java @@ -0,0 +1,82 @@ +/* + * This file is part of Discord4J. + * + * Discord4J is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Discord4J is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Discord4J. If not, see . + */ +package com.github.manevolent.ts3j.util; + +import java.util.concurrent.locks.LockSupport; + +/** + * Custom Thread that executes the passed task every specified period. + * + * Time of execution is held in account when calculating the time to sleep until the next execution. + */ +public class HighPrecisionRecurrentTask extends Thread { + + private final int periodInNanos; + private final int spinningTimeNanos; + private final Runnable target; + private volatile boolean stop; + + /** + * @param periodInMilis The period in milliseconds of the task execution. + * @param percentageOfSleepSpinning The percentage of total sleeping that should be done spinning to increase accuracy. + * @param target The task to run. + */ + public HighPrecisionRecurrentTask(int periodInMilis, float percentageOfSleepSpinning, Runnable target) { + super("TS3J AudioThread"); + if (percentageOfSleepSpinning < 0 || percentageOfSleepSpinning > 1) + throw new IllegalArgumentException("percentageOfSleepSpinning < 0 | percentageOfSleepSpinning > 1"); + this.periodInNanos = periodInMilis * 1_000_000; + this.spinningTimeNanos = (int) (periodInNanos * percentageOfSleepSpinning); + this.target = target; + } + + public boolean isStop() { + return stop; + } + + public void setStop(boolean stop) { + this.stop = stop; + } + + @Override + @SuppressWarnings(value = "empty-statement") + public void run() { + long nextTarget = 0; + while (!stop) { + long now = System.nanoTime(); + target.run(); + long total = System.nanoTime() - now; + nextTarget = now + periodInNanos - total; + sleepFor(periodInNanos - total - spinningTimeNanos); + while (nextTarget > System.nanoTime()) { + ; //consume cycles + } + } + } + + private void sleepFor(long nanos) { + if (nanos > 0) { + long elapsed = 0; + while (elapsed < nanos) { + long t0 = System.nanoTime(); + LockSupport.parkNanos(nanos - elapsed); + elapsed += System.nanoTime() - t0; + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/github/manevolent/ts3j/util/QuickLZ.java b/src/main/java/com/github/manevolent/ts3j/util/QuickLZ.java index f5d0c03..10e4d25 100644 --- a/src/main/java/com/github/manevolent/ts3j/util/QuickLZ.java +++ b/src/main/java/com/github/manevolent/ts3j/util/QuickLZ.java @@ -324,7 +324,7 @@ public final class QuickLZ int level = (source[0] >>> 2) & 0x3; if (level != 1 && level != 3) - throw new RuntimeException("Java version only supports level 1 and 3"); + return source; if ((source[0] & 1) != 1) { diff --git a/src/main/java/com/github/manevolent/ts3j/util/Ts3Debugging.java b/src/main/java/com/github/manevolent/ts3j/util/Ts3Debugging.java index dde9d68..ca528cc 100644 --- a/src/main/java/com/github/manevolent/ts3j/util/Ts3Debugging.java +++ b/src/main/java/com/github/manevolent/ts3j/util/Ts3Debugging.java @@ -1,6 +1,16 @@ package com.github.manevolent.ts3j.util; public final class Ts3Debugging { + private static boolean enabled; + + public static boolean isEnabled() { + return enabled; + } + + public static void setEnabled(boolean enabled) { + Ts3Debugging.enabled = enabled; + } + public static byte[] hexStringToByteArray(String s) { int len = s.length(); byte[] data = new byte[len / 2]; @@ -25,19 +35,23 @@ public final class Ts3Debugging { } public static void info(String message) { - System.err.println("[INFO] " + message); + if (isEnabled()) { + System.err.println("[INFO] " + message); + } } public static void debug(Object message) { - System.err.println("[DEBUG] " + message.toString()); + if (isEnabled()) System.err.println("[DEBUG] " + message.toString()); } public static void debug(byte[] message) { - debug(getHex(message)); + if (isEnabled()) debug(getHex(message)); } public static void debug(String message, Throwable ex) { - System.err.println("[DEBUG] " + message); - ex.printStackTrace(); + if (isEnabled()) { + System.err.println("[DEBUG] " + message); + ex.printStackTrace(); + } } } diff --git a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java index 414c2fd..5b2563b 100644 --- a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java +++ b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java @@ -39,7 +39,9 @@ public class ServerConnectionTest { assertEquals(client.getState(), ClientConnectionState.CONNECTED); - client.getChannels().complete(); + while (true) { + Thread.sleep(1000L); + } } catch (Throwable ex) { ex.printStackTrace(); }