Beginning to work on audio stuff now. Lots of changes.

This commit is contained in:
Manevolent
2018-07-31 15:39:03 -04:00
parent e93b0dee6f
commit 44a18ad7a0
14 changed files with 592 additions and 92 deletions
@@ -0,0 +1,7 @@
package com.github.manevolent.ts3j.audio;
public enum AudioEncoding {
OPUS,
SPEEX,
PCM
}
@@ -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();
}
@@ -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<String, String> 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);
}
}
@@ -57,4 +57,7 @@ public interface TS3Listener {
void onServerGroupList(ServerGroupListEvent e); void onServerGroupList(ServerGroupListEvent e);
void onClientNeededPermissions(ClientNeededPermissionsEvent e); void onClientNeededPermissions(ClientNeededPermissionsEvent e);
void onClientChannelGroupChanged(ClientChannelGroupChangedEvent e);
} }
@@ -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);
}
}
}
@@ -5,6 +5,7 @@ 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 java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class PacketBody5Pong extends PacketBody { public class PacketBody5Pong extends PacketBody {
private int packetId; private int packetId;
@@ -141,6 +141,8 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
} }
} else if (packet.getBody() instanceof PacketBody2Command) { } else if (packet.getBody() instanceof PacketBody2Command) {
Ts3Debugging.debug(((PacketBody2Command) packet.getBody()).getText());
SingleCommand command = ((PacketBody2Command) packet.getBody()).parse().simplifyOne(); SingleCommand command = ((PacketBody2Command) packet.getBody()).parse().simplifyOne();
if (command.getName().equalsIgnoreCase("initivexpand2")) { if (command.getName().equalsIgnoreCase("initivexpand2")) {
@@ -186,18 +188,19 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
getClient().setSecureParameters(Ts3Crypt.cryptoInit2(license, alphaBytes, beta, keyPair.getValue())); getClient().setSecureParameters(Ts3Crypt.cryptoInit2(license, alphaBytes, beta, keyPair.getValue()));
alphaBytes = null;
getClient().writePacket(new PacketBody2Command( getClient().writePacket(new PacketBody2Command(
ProtocolRole.CLIENT, ProtocolRole.CLIENT,
new SingleCommand( new SingleCommand(
"clientinit", "clientinit",
ProtocolRole.CLIENT, ProtocolRole.CLIENT,
new CommandSingleParameter("client_nickname", getClient().getNickname()), 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_platform", "Windows"),
new CommandSingleParameter( new CommandSingleParameter(
"client_version_sign", "client_version_sign",
"LJ5q+KWT4KwBX7oR/9j9A12hBrq5ds5ony99" + "DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg=="
"f9kepNmqFskhT7gfB51bAJNgAMOzXVCeaItNmc10F2wUNktqCw=="
), ),
new CommandSingleParameter("client_input_hardware", "1"), new CommandSingleParameter("client_input_hardware", "1"),
new CommandSingleParameter("client_output_hardware", "1"), new CommandSingleParameter("client_output_hardware", "1"),
@@ -119,7 +119,8 @@ public class PacketTransformation {
return outputBuffer; return outputBuffer;
} }
public byte[] decrypt(PacketHeader header, ByteBuffer buffer, int dataLen) { public byte[] decrypt(PacketHeader header, ByteBuffer buffer, int dataLen)
throws InvalidCipherTextException {
Pair<byte[], byte[]> parameters = computeParameters(header); Pair<byte[], byte[]> parameters = computeParameters(header);
byte[] headerWithoutMac = new byte[header.getSize() - MAC_LEN]; 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(), header.getSize(), dataLen, result, 0);
len += cipher.processBytes(buffer.array(), 0, MAC_LEN, result, len); len += cipher.processBytes(buffer.array(), 0, MAC_LEN, result, len);
try {
len += cipher.doFinal(result, len); len += cipher.doFinal(result, len);
} catch (InvalidCipherTextException e) {
throw new RuntimeException(e);
}
if (len != dataLen) if (len != dataLen)
throw new IllegalArgumentException(len + " != " + dataLen); throw new IllegalArgumentException(len + " != " + dataLen);
@@ -4,6 +4,7 @@ import com.github.manevolent.ts3j.command.*;
import com.github.manevolent.ts3j.identity.Identity; import com.github.manevolent.ts3j.identity.Identity;
import com.github.manevolent.ts3j.protocol.NetworkPacket; import com.github.manevolent.ts3j.protocol.NetworkPacket;
import com.github.manevolent.ts3j.protocol.Packet; 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.SocketRole;
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
import com.github.manevolent.ts3j.protocol.header.ClientPacketHeader; 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.Ts3Crypt;
import com.github.manevolent.ts3j.util.Ts3Debugging; import com.github.manevolent.ts3j.util.Ts3Debugging;
import javafx.util.Pair; import javafx.util.Pair;
import org.bouncycastle.crypto.InvalidCipherTextException;
import java.io.IOException; import java.io.IOException;
import java.net.DatagramPacket; import java.net.DatagramPacket;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.*; import java.util.*;
@@ -39,8 +42,10 @@ public abstract class AbstractTeamspeakClientSocket
private final Map<PacketBodyType, Reassembly> reassemblyQueue = new ConcurrentHashMap<>(); private final Map<PacketBodyType, Reassembly> reassemblyQueue = new ConcurrentHashMap<>();
private final Map<String, Object> clientOptions = new ConcurrentHashMap<>(); private final Map<String, Object> clientOptions = new ConcurrentHashMap<>();
private final Map<Integer, PacketResponse> sendQueue = new ConcurrentHashMap<>(); private final Map<Integer, PacketResponse> sendQueue = new ConcurrentHashMap<>();
private final Map<Integer, PacketResponse> sendQueueLow = new ConcurrentHashMap<>();
private final Map<PacketBodyType, PacketCounter> localSendCounter = new ConcurrentHashMap<>(); private final Map<PacketBodyType, PacketCounter> localSendCounter = new ConcurrentHashMap<>();
private final Map<PacketBodyType, PacketCounter> remoteSendCounter = new ConcurrentHashMap<>(); private final Map<PacketBodyType, RingQueue> remoteQueue = new ConcurrentHashMap<>();
private CommandProcessor commandProcessor; private CommandProcessor commandProcessor;
@@ -53,6 +58,8 @@ public abstract class AbstractTeamspeakClientSocket
private int clientId = 0; private int clientId = 0;
private int packetId = 0; private int packetId = 0;
private long lastPing;
private final NetworkReader networkReader = new NetworkReader(); private final NetworkReader networkReader = new NetworkReader();
private final NetworkHandler networkHandler = new NetworkHandler(); private final NetworkHandler networkHandler = new NetworkHandler();
private Thread networkThread = null; private Thread networkThread = null;
@@ -75,13 +82,14 @@ public abstract class AbstractTeamspeakClientSocket
reassemblyQueue.put(bodyType, new Reassembly()); reassemblyQueue.put(bodyType, new Reassembly());
// Initialize counters // Initialize counters
for (PacketBodyType bodyType : PacketBodyType.values()) for (PacketBodyType bodyType : PacketBodyType.values()) {
if (bodyType == PacketBodyType.INIT1) { if (bodyType == PacketBodyType.INIT1) {
localSendCounter.put(PacketBodyType.INIT1, new PacketCounterZero()); localSendCounter.put(bodyType, new PacketCounterZero());
remoteSendCounter.put(PacketBodyType.INIT1, new PacketCounterZero());
} else { } else {
localSendCounter.put(bodyType, new PacketCounterFull()); localSendCounter.put(bodyType, new PacketCounterFull(true));
remoteSendCounter.put(bodyType, new PacketCounterFull(-1)); }
remoteQueue.put(bodyType, new RingQueue(100, 65536));
} }
} }
@@ -104,7 +112,7 @@ public abstract class AbstractTeamspeakClientSocket
setTransformation(new PacketTransformation(parameters.getIvStruct(), parameters.getFakeSignature())); 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; protected abstract void writeNetworkPacket(NetworkPacket packet) throws IOException;
@@ -166,21 +174,93 @@ public abstract class AbstractTeamspeakClientSocket
return connectionState == ClientConnectionState.CONNECTED; return connectionState == ClientConnectionState.CONNECTED;
} }
@Override private PacketHeader newOutgoingHeader() {
public void writePacket(PacketBody body) throws IOException, TimeoutException {
PacketHeader header; PacketHeader header;
try { try {
header = body.getRole().getHeaderClass().newInstance(); header = getRole().getOut().getHeaderClass().newInstance();
} catch (ReflectiveOperationException e) { } catch (ReflectiveOperationException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
return header;
}
@Override
public void writePacket(PacketBody body) throws IOException, TimeoutException {
// Construct a network packet // Construct a network packet
Packet packet = new Packet(body.getRole()); Packet packet = new Packet(body.getRole());
packet.setHeader(header);
packet.setBody(body); 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<Integer, Integer> 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); writePacket(packet);
} }
@@ -193,30 +273,6 @@ public abstract class AbstractTeamspeakClientSocket
packet.getHeader().getRole().name() + " != " + packet.getHeader().getRole().name() + " != " +
getRole().getOut().name()); getRole().getOut().name());
// Ensure type matches
packet.getHeader().setType(packet.getBody().getType());
// Set header values
packet.getBody().setHeaderValues(packet.getHeader());
Pair<Integer, Integer> 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) { if (packet.getSize() > 500) {
int totalSize = packet.getBody().getSize(); int totalSize = packet.getBody().getSize();
@@ -274,6 +330,10 @@ public abstract class AbstractTeamspeakClientSocket
ByteBuffer outputBuffer; ByteBuffer outputBuffer;
if (!packet.getHeader().getPacketFlag(HeaderFlag.UNENCRYPTED)) { if (!packet.getHeader().getPacketFlag(HeaderFlag.UNENCRYPTED)) {
Ts3Debugging.debug("[PROTOCOL] ENCRYPT " +
packet.getHeader().getType().name() + " generation=" +
packet.getHeader().getGeneration());
outputBuffer = getTransformation().encrypt(packet); outputBuffer = getTransformation().encrypt(packet);
} else { } else {
if (packet.getHeader().getType() == PacketBodyType.INIT1) { if (packet.getHeader().getType() == PacketBodyType.INIT1) {
@@ -315,7 +375,7 @@ public abstract class AbstractTeamspeakClientSocket
writeNetworkPacket(networkPacket); writeNetworkPacket(networkPacket);
// If necessary, fulfill promise for the packet acknowledgement or throw TimeoutException // 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()) { while (response.getRetries() < response.getMaxTries()) {
try { try {
if (getState() == ClientConnectionState.DISCONNECTED) if (getState() == ClientConnectionState.DISCONNECTED)
@@ -344,6 +404,11 @@ public abstract class AbstractTeamspeakClientSocket
private Packet readPacketIntl(NetworkPacket networkPacket) throws IOException { private Packet readPacketIntl(NetworkPacket networkPacket) throws IOException {
if (networkPacket.getHeader() == null) throw new NullPointerException("header"); 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 packet = new Packet(networkPacket.getHeader().getRole());
packet.setHeader(networkPacket.getHeader()); packet.setHeader(networkPacket.getHeader());
@@ -366,18 +431,25 @@ public abstract class AbstractTeamspeakClientSocket
" is unencrypted, but must be encrypted" " is unencrypted, but must be encrypted"
); );
ByteBuffer packetBuffer = ByteBuffer packetBuffer = networkPacket.getBuffer();
(ByteBuffer) networkPacket.getBuffer().position(networkPacket.getHeader().getSize());
// Decrypt // Decrypt
if (encrypted) { if (encrypted) {
Ts3Debugging.debug("[PROTOCOL] DECRYPT " + networkPacket.getHeader().getType().name()
+ " generation=" + currentGeneration);
try {
packetBuffer = ByteBuffer.wrap( packetBuffer = ByteBuffer.wrap(
getTransformation().decrypt( getTransformation().decrypt(
networkPacket.getHeader(), networkPacket.getHeader(),
packetBuffer, packetBuffer,
networkPacket.getDatagram().getLength() - networkPacket.getHeader().getSize() 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 // Fragment handling
@@ -399,7 +471,9 @@ public abstract class AbstractTeamspeakClientSocket
if (packet.getBody() instanceof PacketBodyCompressed) { if (packet.getBody() instanceof PacketBodyCompressed) {
byte[] decompressed = QuickLZ.decompress(((PacketBodyCompressed) packet.getBody()).getCompressed()); 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()); packet = new Packet(packet.getRole(), packet.getHeader());
@@ -413,43 +487,89 @@ public abstract class AbstractTeamspeakClientSocket
} }
@Override @Override
public Packet readPacket() throws IOException, TimeoutException { public Packet readPacket() throws
IOException,
TimeoutException {
NetworkPacket networkPacket;
long timeout;
while (isReading()) { 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 // Find if the packet must be acknowledged
PacketBodyType ackType = packet.getHeader().getType().getAcknolwedgedBy(); PacketBodyType ackType = packet
.getHeader()
.getType()
.getAcknolwedgedBy();
if (ackType != null) { if (ackType != null) {
switch (ackType) { switch (ackType) {
case ACK: case ACK:
Ts3Debugging.debug("[PROTOCOL] Acknowledge " + packet + " " + packet.getHeader().getType().name()
+ " id=" + packet.getHeader().getPacketId());
writePacket(new PacketBody6Ack(getRole().getOut(), packet.getHeader().getPacketId())); writePacket(new PacketBody6Ack(getRole().getOut(), packet.getHeader().getPacketId()));
break; break;
case ACK_LOW: case ACK_LOW:
writePacket(new PacketBody7AckLow(getRole().getOut(), packet.getHeader().getPacketId())); writePacket(new PacketBody7AckLow(getRole().getOut(), packet.getHeader().getPacketId()));
break; break;
case PONG: 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 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 // Find if the packet must be reassembled
if (packet.getHeader().getType().isSplittable()) { if (packet.getHeader().getType().isSplittable()) {
packet = reassemblyQueue.get(packet.getHeader().getType()).put(packet); packet = reassemblyQueue.get(packet.getHeader().getType()).put(packet);
if (packet == null) continue; 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 // Find if the packet is itself an acknowledgement
boolean handle; boolean handle;
PacketResponse response; PacketResponse response;
@@ -459,7 +579,11 @@ public abstract class AbstractTeamspeakClientSocket
handle = false; handle = false;
break; break;
case ACK_LOW: 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; handle = false;
break; break;
default: default:
@@ -680,6 +804,11 @@ public abstract class AbstractTeamspeakClientSocket
int getPacketId(); int getPacketId();
boolean setPacketId(int packetId); boolean setPacketId(int packetId);
int getGenerationId(); int getGenerationId();
void setGenerationId(int i);
default Pair<Integer,Integer> current() {
return new Pair<>(getPacketId(), getGenerationId());
}
} }
private class PacketCounterZero implements PacketCounter { private class PacketCounterZero implements PacketCounter {
@@ -702,18 +831,26 @@ public abstract class AbstractTeamspeakClientSocket
public int getGenerationId() { public int getGenerationId() {
return 0; return 0;
} }
@Override
public void setGenerationId(int i) {
throw new UnsupportedOperationException();
}
} }
private class PacketCounterFull implements PacketCounter { private class PacketCounterFull implements PacketCounter {
private final Object sendLock = new Object(); private final Object sendLock = new Object();
private final boolean counting;
private int packetId = 0; private int packetId = 0;
private int generationId = 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); setPacketId(start);
} }
@@ -727,8 +864,8 @@ public abstract class AbstractTeamspeakClientSocket
if (this.packetId == packetId) return false; if (this.packetId == packetId) return false;
if (packetId >= 65536) { if (packetId >= 65536) {
packetId = 0; this.packetId = 0; // wrap around
generationId++; if (isCounting()) setGenerationId(getGenerationId() + 1);
} else { } else {
this.packetId = packetId; this.packetId = packetId;
} }
@@ -741,13 +878,25 @@ public abstract class AbstractTeamspeakClientSocket
return generationId; return generationId;
} }
@Override
public void setGenerationId(int i) {
synchronized (sendLock) {
Ts3Debugging.debug("setGenerationId: " + i);
this.generationId = i;
}
}
@Override @Override
public Pair<Integer, Integer> next() { public Pair<Integer, Integer> next() {
synchronized (sendLock) { synchronized (sendLock) {
setPacketId(packetId + 1); setPacketId(getPacketId() + 1);
return new Pair<>(packetId, generationId); return current();
} }
} }
public boolean isCounting() {
return counting;
}
} }
private class Reassembly { private class Reassembly {
@@ -16,10 +16,7 @@ import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command;
import com.github.manevolent.ts3j.util.Ts3Debugging; import com.github.manevolent.ts3j.util.Ts3Debugging;
import java.io.IOException; import java.io.IOException;
import java.net.DatagramPacket; import java.net.*;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.*; import java.util.*;
@@ -67,10 +64,12 @@ public class LocalTeamspeakClientSocket
} }
@Override @Override
protected NetworkPacket readNetworkPacket() throws IOException { protected NetworkPacket readNetworkPacket(int timeout) throws
socket.receive(packet); IOException,
SocketTimeoutException {
Ts3Debugging.debug("[NETWORK] READ len=" + packet.getLength() + " from " + packet.getSocketAddress()); socket.setSoTimeout(timeout);
socket.receive(packet);
PacketHeader header; PacketHeader header;
@@ -86,6 +85,11 @@ public class LocalTeamspeakClientSocket
header.read(buffer); header.read(buffer);
Ts3Debugging.debug("[NETWORK] READ" +
" id=" + header.getPacketId() +
" len=" + packet.getLength() +
" from " + packet.getSocketAddress());
return new NetworkPacket( return new NetworkPacket(
packet, packet,
header, header,
@@ -325,6 +329,8 @@ public class LocalTeamspeakClientSocket
public void process(AbstractTeamspeakClientSocket client, SingleCommand singleCommand) public void process(AbstractTeamspeakClientSocket client, SingleCommand singleCommand)
throws CommandProcessException { throws CommandProcessException {
try { try {
setClientId(Integer.parseInt(singleCommand.get("aclid").getValue()));
setState(ClientConnectionState.CONNECTED); setState(ClientConnectionState.CONNECTED);
} catch (Exception e) { } catch (Exception e) {
throw new CommandProcessException(e); throw new CommandProcessException(e);
@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}
}
@@ -324,7 +324,7 @@ public final class QuickLZ
int level = (source[0] >>> 2) & 0x3; int level = (source[0] >>> 2) & 0x3;
if (level != 1 && level != 3) if (level != 1 && level != 3)
throw new RuntimeException("Java version only supports level 1 and 3"); return source;
if ((source[0] & 1) != 1) if ((source[0] & 1) != 1)
{ {
@@ -1,6 +1,16 @@
package com.github.manevolent.ts3j.util; package com.github.manevolent.ts3j.util;
public final class Ts3Debugging { 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) { public static byte[] hexStringToByteArray(String s) {
int len = s.length(); int len = s.length();
byte[] data = new byte[len / 2]; byte[] data = new byte[len / 2];
@@ -25,19 +35,23 @@ public final class Ts3Debugging {
} }
public static void info(String message) { public static void info(String message) {
if (isEnabled()) {
System.err.println("[INFO] " + message); System.err.println("[INFO] " + message);
} }
}
public static void debug(Object 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) { public static void debug(byte[] message) {
debug(getHex(message)); if (isEnabled()) debug(getHex(message));
} }
public static void debug(String message, Throwable ex) { public static void debug(String message, Throwable ex) {
if (isEnabled()) {
System.err.println("[DEBUG] " + message); System.err.println("[DEBUG] " + message);
ex.printStackTrace(); ex.printStackTrace();
} }
} }
}
@@ -39,7 +39,9 @@ public class ServerConnectionTest {
assertEquals(client.getState(), ClientConnectionState.CONNECTED); assertEquals(client.getState(), ClientConnectionState.CONNECTED);
client.getChannels().complete(); while (true) {
Thread.sleep(1000L);
}
} catch (Throwable ex) { } catch (Throwable ex) {
ex.printStackTrace(); ex.printStackTrace();
} }