Huge refactor

This commit is contained in:
Manevolent
2018-07-25 21:54:28 -04:00
parent 567ee472d6
commit e941a89c55
42 changed files with 1059 additions and 1115 deletions
+1 -1
View File
@@ -116,7 +116,7 @@ for making modifications to it. "Object code" means any non-source
form of a work. form of a work.
A "Standard Interface" means an interface that either is an official A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of standard defined by a recognized standards buffer, or, in the case of
interfaces specified for a particular programming language, one that interfaces specified for a particular programming language, one that
is widely used among developers working in that language. is widely used among developers working in that language.
+6
View File
@@ -35,5 +35,11 @@
<artifactId>bcprov-jdk15on</artifactId> <artifactId>bcprov-jdk15on</artifactId>
<version>1.60</version> <version>1.60</version>
</dependency> </dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
</dependencies> </dependencies>
</project> </project>
@@ -1,14 +0,0 @@
package com.github.manevolent.ts3j.protocol;
import com.github.manevolent.ts3j.protocol.packet.Packet;
import java.io.IOException;
import java.util.function.Function;
public interface Endpoint {
SocketRole getRole();
void send(Packet packet) throws IOException;
}
@@ -1,119 +0,0 @@
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;
import java.net.InetSocketAddress;
import java.net.SocketException;
public abstract class LocalEndpoint implements Endpoint {
private final LocalTeamspeakSocket socket;
private LocalIdentity localIdentity = null;
private final ReassemblyQueue reassemblyQueue = new ReassemblyQueue();
private final Object runLock = new Object();
private Thread receiverThread;
private boolean running = false;
protected LocalEndpoint(LocalTeamspeakSocket socket) {
this.socket = socket;
}
protected LocalEndpoint(SocketRole socketRole) throws SocketException {
this.socket = new LocalTeamspeakSocket(socketRole);
}
public InetSocketAddress getLocalAddress() {
return getSocket().getLocalAddress();
}
public InetSocketAddress getRemoteAddress() {
return getSocket().getLocalAddress();
}
protected final LocalTeamspeakSocket getSocket() {
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;
if (running) {
Ts3Logging.debug("Starting network receiver thread...");
receiverThread = new Thread(new PacketReceiver());
receiverThread.setDaemon(true);
receiverThread.start();
} else {
if (receiverThread != null) {
receiverThread.interrupt();
receiverThread = null;
}
}
}
}
@Override
public SocketRole getRole() {
return socket.getSocketRole();
}
private NetworkPacket receive() throws IOException {
return socket.receive();
}
public LocalIdentity getLocalIdentity() {
return localIdentity;
}
public void setLocalIdentity(LocalIdentity localIdentity) {
this.localIdentity = localIdentity;
}
private class PacketReceiver implements Runnable {
@Override
public void run() {
Ts3Logging.debug("Entering packet receiver");
while (running) {
try {
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;
Ts3Logging.debug("Problem handling packet", e);
}
}
Ts3Logging.debug("Leaving packet receiver");
}
}
}
@@ -1,127 +1,42 @@
package com.github.manevolent.ts3j.protocol; package com.github.manevolent.ts3j.protocol;
import com.github.manevolent.ts3j.protocol.header.PacketHeader; import com.github.manevolent.ts3j.protocol.header.PacketHeader;
import com.github.manevolent.ts3j.protocol.packet.Packet;
import com.github.manevolent.ts3j.util.Ts3Logging;
import java.net.DatagramPacket;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/**
* Represents a fully contained network packet, traveling in or out.
*/
public final class NetworkPacket { public final class NetworkPacket {
private final ProtocolRole role; private final DatagramPacket datagram;
private final PacketHeader header;
private final ByteBuffer buffer;
private PacketHeader header; public NetworkPacket(DatagramPacket datagram, PacketHeader header, ByteBuffer buffer) {
private Packet packet; this.datagram = datagram;
this.header = header;
public NetworkPacket(ProtocolRole role) { this.buffer = buffer;
if (role == null) throw new NullPointerException("role");
this.role = role;
} }
public ProtocolRole getRole() { /**
return role; * Gets the original packet datagram.
* @return Packet datagram.
*/
public DatagramPacket getDatagram() {
return datagram;
} }
/**
* Gets the received header.
* @return Packet header.
*/
public PacketHeader getHeader() { public PacketHeader getHeader() {
return header; return header;
} }
public void setHeader(PacketHeader header) {
if (header == null) throw new NullPointerException("header");
this.header = header;
}
public Packet getPacket() {
return packet;
}
public void setPacket(Packet packet) {
if (packet == null) throw new NullPointerException("packet");
this.packet = packet;
}
/** /**
* Writes the existing data out to the specified byte buffer * Gets the original packet buffer.
* @param buffer Buffer to write to * @return Packet buffer.
*/ */
public ByteBuffer write(ByteBuffer buffer) { public ByteBuffer getBuffer() {
writeHeader(buffer);
writeBody(buffer);
return buffer; return buffer;
} }
public ByteBuffer writeHeader(ByteBuffer buffer) {
if (getHeader() == null) throw new NullPointerException("header");
getHeader().setType(getPacket().getType());
getHeader().write(buffer);
return 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 ByteBuffer read(ByteBuffer buffer) {
readHeader(buffer);
readBody(buffer);
return buffer;
}
public ByteBuffer readHeader(ByteBuffer buffer) {
if (getHeader() == null) {
try {
setHeader(getRole().getHeaderClass().newInstance());
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
getHeader().read(buffer);
return buffer;
}
public ByteBuffer readBody(ByteBuffer buffer) {
if (getPacket() == null) {
try {
setPacket(
getHeader().getType().getPacketClass()
.getConstructor(ProtocolRole.class)
.newInstance(getHeader().getRole())
);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
getPacket().read(buffer);
return buffer;
}
/**
* Gets the size of this packet. This method is generally only used when writing a fully defined packet.
* @return packet size.
*/
public int getSize() {
return getHeader().getSize() + getPacket().getSize();
}
} }
@@ -0,0 +1,127 @@
package com.github.manevolent.ts3j.protocol;
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
import com.github.manevolent.ts3j.protocol.packet.PacketBody;
import java.nio.ByteBuffer;
/**
* Represents a fully contained network packet, traveling in or out.
*/
public final class Packet {
private final ProtocolRole role;
private PacketHeader header;
private PacketBody body;
public Packet(ProtocolRole role) {
if (role == null) throw new NullPointerException("role");
this.role = role;
}
public ProtocolRole getRole() {
return role;
}
public PacketHeader getHeader() {
return header;
}
public void setHeader(PacketHeader header) {
if (header == null) throw new NullPointerException("header");
this.header = header;
}
public PacketBody getBody() {
return body;
}
public void setBody(PacketBody packetBody) {
if (packetBody == null) throw new NullPointerException("packet");
this.body = packetBody;
}
/**
* Writes the existing data out to the specified byte buffer
* @param buffer Buffer to write to
*/
public ByteBuffer write(ByteBuffer buffer) {
writeHeader(buffer);
writeBody(buffer);
return buffer;
}
public ByteBuffer writeHeader(ByteBuffer buffer) {
if (getHeader() == null) throw new NullPointerException("header");
if (getBody() != null)
getHeader().setType(getBody().getType());
getHeader().write(buffer);
return buffer;
}
public ByteBuffer writeBody(ByteBuffer buffer) {
if (getBody() == null) throw new NullPointerException("packet");
getBody().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 ByteBuffer read(ByteBuffer buffer) {
readHeader(buffer);
readBody(buffer);
return buffer;
}
public ByteBuffer readHeader(ByteBuffer buffer) {
if (getHeader() == null) {
try {
setHeader(getRole().getHeaderClass().newInstance());
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
getHeader().read(buffer);
return buffer;
}
public ByteBuffer readBody(ByteBuffer buffer) {
if (getBody() == null) {
try {
setBody(
getHeader().getType().getPacketClass()
.getConstructor(ProtocolRole.class)
.newInstance(getHeader().getRole())
);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
getBody().read(buffer);
return buffer;
}
/**
* Gets the size of this packet. This method is generally only used when writing a fully defined packet.
* @return packet size.
*/
public int getSize() {
return (getHeader() == null ? 0 : getHeader().getSize()) + (getBody() == null ? 0 : getBody().getSize());
}
}
@@ -1,7 +0,0 @@
package com.github.manevolent.ts3j.protocol;
import java.net.InetSocketAddress;
public abstract class RemoteEndpoint implements Endpoint {
public abstract InetSocketAddress getRemoteAddress();
}
@@ -1,29 +1,34 @@
package com.github.manevolent.ts3j.protocol.client; package com.github.manevolent.ts3j.protocol.client;
import com.github.manevolent.ts3j.protocol.packet.handler.client.LocalClientHandler; import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler;
import com.github.manevolent.ts3j.protocol.packet.handler.client.LocalClientHandlerConnected; import com.github.manevolent.ts3j.protocol.packet.handler.local.LocalClientHandler;
import com.github.manevolent.ts3j.protocol.packet.handler.client.LocalClientHandlerConnecting; import com.github.manevolent.ts3j.protocol.packet.handler.local.LocalClientHandlerConnected;
import com.github.manevolent.ts3j.protocol.packet.handler.client.LocalClientHandlerDisconnected; import com.github.manevolent.ts3j.protocol.packet.handler.local.LocalClientHandlerConnecting;
import com.github.manevolent.ts3j.protocol.packet.handler.local.LocalClientHandlerDisconnected;
import com.github.manevolent.ts3j.protocol.socket.client.AbstractTeamspeakClientSocket;
import com.github.manevolent.ts3j.protocol.socket.client.TeamspeakClientSocket;
import org.apache.commons.lang.reflect.ConstructorUtils;
public enum ClientConnectionState { public enum ClientConnectionState {
CONNECTING(LocalClientHandlerConnecting.class), CONNECTING(LocalClientHandlerConnecting.class),
NEGOTIATING(LocalClientHandlerConnecting.class),
CONNECTED(LocalClientHandlerConnected.class), CONNECTED(LocalClientHandlerConnected.class),
DISCONNECTED(LocalClientHandlerDisconnected.class); DISCONNECTED(LocalClientHandlerDisconnected.class);
private final Class<? extends LocalClientHandler> clazz; private final Class<? extends PacketHandler> clazz;
ClientConnectionState(Class<? extends LocalClientHandler> clazz) { ClientConnectionState(Class<? extends PacketHandler> clazz) {
this.clazz = clazz; this.clazz = clazz;
} }
public Class<? extends LocalClientHandler> getHandlerClass() { public Class<? extends PacketHandler> getHandlerClass() {
return clazz; return clazz;
} }
public LocalClientHandler createHandler(LocalTeamspeakClient client) { public PacketHandler createHandler(TeamspeakClientSocket client) {
try { try {
return clazz.getConstructor(LocalTeamspeakClient.class).newInstance(client); return (PacketHandler) ConstructorUtils.invokeConstructor(clazz, client);
} catch (ReflectiveOperationException ex) { } catch (ReflectiveOperationException ex) {
throw new RuntimeException(ex); throw new RuntimeException(ex);
} }
@@ -1,218 +0,0 @@
package com.github.manevolent.ts3j.protocol.client;
import com.github.manevolent.ts3j.command.Command;
import com.github.manevolent.ts3j.command.response.CommandResponse;
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.header.HeaderFlag;
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.packet.transformation.PacketTransformation;
import com.github.manevolent.ts3j.protocol.socket.LocalTeamspeakSocket;
import com.github.manevolent.ts3j.protocol.packet.Packet;
import com.github.manevolent.ts3j.util.Ts3Crypt;
import com.github.manevolent.ts3j.util.Ts3Logging;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
public class LocalTeamspeakClient extends LocalEndpoint implements TeamspeakClient {
private final Object connectionStateLock = new Object();
private final Map<String, Object> clientOptions = new LinkedHashMap<>();
private final PacketChannel channel = new PacketChannel();
private ClientConnectionState connectionState = null;
private int packetId = 0;
private int clientId = 0;
private int returnCodeIndex = 0;
public LocalTeamspeakClient(LocalTeamspeakSocket socket) {
super(socket);
if (socket.getSocketRole() != SocketRole.CLIENT)
throw new IllegalArgumentException("invalid socket role: " + socket.getSocketRole().name());
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<String, Object> getOptions() {
return clientOptions;
}
public void setSecureParameters(Ts3Crypt.SecureChannelParameters parameters) {
getSocket().setPacketTransformation(new PacketTransformation(parameters.getIvStruct()));
}
/**
* 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");
CommandResponse response;
if (command.willExpectResponse()) {
int returnCodeIndex = ++ this.returnCodeIndex;
//command.appendParameter(new CommandSingleParameter("return_code", returnCodeIndex));
response = new CommandResponse(command, returnCodeIndex);
} else {
response = new CommandResponse(command, -1);
}
String commandString = command.toString();
response.setDispatchedTime(System.currentTimeMillis());
return response;
}
public LocalTeamspeakClient() throws SocketException {
this(new LocalTeamspeakSocket(SocketRole.CLIENT));
}
/**
* Sends a packet to the server
* @param packet Packet to send
* @throws IOException
*/
int z = 0;
public void send(Packet packet) throws IOException {
// Construct header
ClientPacketHeader header = new ClientPacketHeader();
header.setClientId(clientId);
header.setPacketId(packetId);
packet.setHeaderValues(header);
if (connectionState == ClientConnectionState.CONNECTING) {
header.setPacketId(z++);
header.setPacketFlag(HeaderFlag.NEW_PROTOCOL, true);
}
getSocket().send(header, packet);
}
public void setConnectionState(ClientConnectionState state) {
synchronized (connectionStateLock) {
if (state != this.connectionState) {
Ts3Logging.debug("State changing: " + state.name());
boolean wasDisconnected = this.connectionState == ClientConnectionState.DISCONNECTED;
this.connectionState = state;
if (wasDisconnected)
setRunning(true);
if (channel.getHandler() != null)
((LocalClientHandler)channel.getHandler()).handleConnectionStateChanging(state);
if (channel.getHandler() == null || channel.getHandler().getClass() != state.getHandlerClass()) {
Ts3Logging.debug("Assigning " + state.getHandlerClass().getName() + " handler...");
try {
channel.setHandler(state.createHandler(this));
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
if (this.connectionState == ClientConnectionState.DISCONNECTED)
clientOptions.clear();
connectionStateLock.notifyAll();
Ts3Logging.debug("State changed: " + state.name());
}
}
}
public ClientConnectionState getConnectionState() {
return connectionState;
}
private void joinConnectionState(ClientConnectionState state, long wait)
throws InterruptedException, TimeoutException {
synchronized (connectionStateLock) {
long start = System.currentTimeMillis();
while (connectionState != state && System.currentTimeMillis() - start < wait) {
connectionStateLock.wait(Math.max(0L, wait - (System.currentTimeMillis() - start)));
}
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 (connectionState != ClientConnectionState.DISCONNECTED)
throw new IllegalStateException(connectionState.name());
setOption("client.hostname", remote.getHostString());
setOption("client.password", password);
getSocket().connect(remote);
setConnectionState(ClientConnectionState.CONNECTING);
joinConnectionState(ClientConnectionState.CONNECTED, timeout);
} catch (Throwable e) {
setConnectionState(ClientConnectionState.DISCONNECTED);
throw new IOException(e);
}
}
}
@@ -1,38 +0,0 @@
package com.github.manevolent.ts3j.protocol.client;
import com.github.manevolent.ts3j.protocol.Endpoint;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Map;
import java.util.function.Function;
public interface TeamspeakClient extends Endpoint {
void connect(InetSocketAddress remote, String password, long timeout) throws IOException;
SocketAddress getRemoteAddress();
boolean isConnected();
Map<String, Object> getOptions();
default <T extends Object> T getOption(String key, Class<T> clazz) {
Object value = getOptions().get(key);
if (value == null) return (T) null;
else if (!value.getClass().isAssignableFrom(clazz)) {
throw new ClassCastException(
clazz.getName()
+ " is not assignable to option type " +
value.getClass().getName()
);
} else
return (T) value;
}
default Object setOption(String key, Object value) {
return getOptions().put(key, value);
}
}
@@ -1,6 +1,6 @@
package com.github.manevolent.ts3j.protocol.header; package com.github.manevolent.ts3j.protocol.header;
import com.github.manevolent.ts3j.protocol.packet.PacketType; import com.github.manevolent.ts3j.protocol.packet.PacketBodyType;
import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.ProtocolRole;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@@ -44,7 +44,7 @@ public class ClientPacketHeader extends PacketHeader {
setClientId((buffer.getShort() & 0xFFFF)); setClientId((buffer.getShort() & 0xFFFF));
byte packetTypeAndFlags = buffer.get(); byte packetTypeAndFlags = buffer.get();
setType(PacketType.fromId((packetTypeAndFlags & 0x0F))); // PT / Packet Type + Flags setType(PacketBodyType.fromId((packetTypeAndFlags & 0x0F))); // PT / Packet Type + Flags
setPacketFlags(packetTypeAndFlags & 0xF0); setPacketFlags(packetTypeAndFlags & 0xF0);
// (payload) -- see writeBody impl in higher levels // (payload) -- see writeBody impl in higher levels
@@ -1,7 +1,7 @@
package com.github.manevolent.ts3j.protocol.header; package com.github.manevolent.ts3j.protocol.header;
import com.github.manevolent.ts3j.protocol.packet.PacketType; import com.github.manevolent.ts3j.protocol.packet.PacketBodyType;
import com.github.manevolent.ts3j.protocol.packet.Packet; import com.github.manevolent.ts3j.protocol.packet.PacketBody;
import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.ProtocolRole;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@@ -13,7 +13,7 @@ public abstract class PacketHeader {
private int packetGeneration; private int packetGeneration;
private byte[] mac = new byte[8]; // 8 bytes private byte[] mac = new byte[8]; // 8 bytes
private int packetId = 0; private int packetId = 0;
private PacketType type; private PacketBodyType type;
private int packetFlags; private int packetFlags;
protected PacketHeader(ProtocolRole role) { protected PacketHeader(ProtocolRole role) {
@@ -44,11 +44,11 @@ public abstract class PacketHeader {
} }
} }
public PacketType getType() { public PacketBodyType getType() {
return type; return type;
} }
public void setType(PacketType type) { public void setType(PacketBodyType type) {
this.type = type; this.type = type;
} }
@@ -65,7 +65,7 @@ public abstract class PacketHeader {
} }
public void setMac(byte[] mac) { public void setMac(byte[] mac) {
Packet.assertArray("mac", mac, 8); PacketBody.assertArray("mac", mac, 8);
this.mac = mac; this.mac = mac;
} }
@@ -1,8 +1,7 @@
package com.github.manevolent.ts3j.protocol.header; package com.github.manevolent.ts3j.protocol.header;
import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.ProtocolRole;
import com.github.manevolent.ts3j.protocol.packet.PacketType; import com.github.manevolent.ts3j.protocol.packet.PacketBodyType;
import com.github.manevolent.ts3j.util.Ts3Logging;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@@ -40,7 +39,7 @@ public class ServerPacketHeader extends PacketHeader {
// https://github.com/ReSpeak/tsdeclarations/blob/master/ts3protocol.md // https://github.com/ReSpeak/tsdeclarations/blob/master/ts3protocol.md
byte packetTypeAndFlags = buffer.get(); byte packetTypeAndFlags = buffer.get();
setType(PacketType.fromId((packetTypeAndFlags & 0x0F))); // PT / Packet Type + Flags setType(PacketBodyType.fromId((packetTypeAndFlags & 0x0F))); // PT / Packet Type + Flags
setPacketFlags(packetTypeAndFlags & 0xF0); setPacketFlags(packetTypeAndFlags & 0xF0);
// (payload) -- see writeBody impl in higher levels // (payload) -- see writeBody impl in higher levels
@@ -5,16 +5,16 @@ import com.github.manevolent.ts3j.protocol.header.PacketHeader;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
public abstract class Packet { public abstract class PacketBody {
private final PacketType type; private final PacketBodyType type;
private final ProtocolRole role; private final ProtocolRole role;
protected Packet(PacketType type, ProtocolRole role) { protected PacketBody(PacketBodyType type, ProtocolRole role) {
this.type = type; this.type = type;
this.role = role; this.role = role;
} }
public PacketType getType() { public PacketBodyType getType() {
return type; return type;
} }
@@ -2,16 +2,15 @@ package com.github.manevolent.ts3j.protocol.packet;
import com.github.manevolent.ts3j.command.SimpleCommand; import com.github.manevolent.ts3j.command.SimpleCommand;
import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.ProtocolRole;
import com.github.manevolent.ts3j.util.Ts3Logging;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
public class Packet2Command extends Packet { public class PacketBody2Command extends PacketBody {
private String text; private String text;
public Packet2Command(ProtocolRole role) { public PacketBody2Command(ProtocolRole role) {
super(PacketType.COMMAND, role); super(PacketBodyType.COMMAND, role);
} }
public String getText() { public String getText() {
@@ -3,13 +3,18 @@ package com.github.manevolent.ts3j.protocol.packet;
import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.ProtocolRole;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.Charset;
public class Packet6Ack extends Packet { public class PacketBody6Ack extends PacketBody {
private int packetId; private int packetId;
public Packet6Ack(ProtocolRole role) { public PacketBody6Ack(ProtocolRole role) {
super(PacketType.ACK, role); super(PacketBodyType.ACK, role);
}
public PacketBody6Ack(ProtocolRole role, int packetId) {
super(PacketBodyType.ACK, role);
setPacketId(packetId);
} }
public int getPacketId() { public int getPacketId() {
@@ -7,9 +7,9 @@ import com.github.manevolent.ts3j.protocol.header.PacketHeader;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import static com.github.manevolent.ts3j.protocol.packet.PacketType.INIT1; import static com.github.manevolent.ts3j.protocol.packet.PacketBodyType.INIT1;
public class Packet8Init1 extends Packet { public class PacketBody8Init1 extends PacketBody {
private static final byte[] LOW_LEVEL_MAC = new byte[] { private static final byte[] LOW_LEVEL_MAC = new byte[] {
0x54, 0x53, 0x33, 0x49, 0x4E, 0x49, 0x54, 0x31 0x54, 0x53, 0x33, 0x49, 0x4E, 0x49, 0x54, 0x31
}; };
@@ -17,7 +17,7 @@ public class Packet8Init1 extends Packet {
private byte[] version = new byte[4]; // 4 bytes private byte[] version = new byte[4]; // 4 bytes
private Step step; // step instance private Step step; // step instance
public Packet8Init1(ProtocolRole role) { public PacketBody8Init1(ProtocolRole role) {
// sec. 2 of https://github.com/ReSpeak/tsdeclarations/blob/master/ts3protocol.md // sec. 2 of https://github.com/ReSpeak/tsdeclarations/blob/master/ts3protocol.md
// (The packet header values are set as following for all packets here:) // (The packet header values are set as following for all packets here:)
super(INIT1, role); super(INIT1, role);
@@ -152,7 +152,7 @@ public class Packet8Init1 extends Packet {
} }
public void setTimestamp(byte[] timestamp) { public void setTimestamp(byte[] timestamp) {
Packet.assertArray("timestamp", timestamp, 4); PacketBody.assertArray("timestamp", timestamp, 4);
this.timestamp = timestamp; this.timestamp = timestamp;
} }
@@ -161,7 +161,7 @@ public class Packet8Init1 extends Packet {
} }
public void setRandom(byte[] random) { public void setRandom(byte[] random) {
Packet.assertArray("random", random, 4); PacketBody.assertArray("random", random, 4);
this.random = random; this.random = random;
} }
@@ -199,7 +199,7 @@ public class Packet8Init1 extends Packet {
} }
public void setA0reversed(byte[] a0reversed) { public void setA0reversed(byte[] a0reversed) {
Packet.assertArray("a0reversed", a0reversed, 4); PacketBody.assertArray("a0reversed", a0reversed, 4);
this.a0reversed = a0reversed; this.a0reversed = a0reversed;
} }
@@ -208,7 +208,7 @@ public class Packet8Init1 extends Packet {
} }
public void setServerStuff(byte[] serverStuff) { public void setServerStuff(byte[] serverStuff) {
Packet.assertArray("serverStuff", serverStuff, 16); PacketBody.assertArray("serverStuff", serverStuff, 16);
this.serverStuff = serverStuff; this.serverStuff = serverStuff;
} }
@@ -244,7 +244,7 @@ public class Packet8Init1 extends Packet {
} }
public void setA0reversed(byte[] a0reversed) { public void setA0reversed(byte[] a0reversed) {
Packet.assertArray("a0reversed", a0reversed, 4); PacketBody.assertArray("a0reversed", a0reversed, 4);
this.a0reversed = a0reversed; this.a0reversed = a0reversed;
} }
@@ -253,7 +253,7 @@ public class Packet8Init1 extends Packet {
} }
public void setServerStuff(byte[] serverStuff) { public void setServerStuff(byte[] serverStuff) {
Packet.assertArray("serverStuff", serverStuff, 16); PacketBody.assertArray("serverStuff", serverStuff, 16);
this.serverStuff = serverStuff; this.serverStuff = serverStuff;
} }
@@ -290,7 +290,7 @@ public class Packet8Init1 extends Packet {
} }
public void setX(byte[] x) { public void setX(byte[] x) {
Packet.assertArray("x", x, 64); PacketBody.assertArray("x", x, 64);
this.x = x; this.x = x;
} }
@@ -299,7 +299,7 @@ public class Packet8Init1 extends Packet {
} }
public void setN(byte[] n) { public void setN(byte[] n) {
Packet.assertArray("n", n, 64); PacketBody.assertArray("n", n, 64);
this.n = n; this.n = n;
} }
@@ -308,7 +308,7 @@ public class Packet8Init1 extends Packet {
} }
public void setServerStuff(byte[] serverStuff) { public void setServerStuff(byte[] serverStuff) {
Packet.assertArray("serverStuff", serverStuff, 100); PacketBody.assertArray("serverStuff", serverStuff, 100);
this.serverStuff = serverStuff; this.serverStuff = serverStuff;
} }
@@ -359,7 +359,7 @@ public class Packet8Init1 extends Packet {
} }
public void setX(byte[] x) { public void setX(byte[] x) {
Packet.assertArray("x", x, 64); PacketBody.assertArray("x", x, 64);
this.x = x; this.x = x;
} }
@@ -368,7 +368,7 @@ public class Packet8Init1 extends Packet {
} }
public void setN(byte[] n) { public void setN(byte[] n) {
Packet.assertArray("n", n, 64); PacketBody.assertArray("n", n, 64);
this.n = n; this.n = n;
} }
@@ -377,7 +377,7 @@ public class Packet8Init1 extends Packet {
} }
public void setServerStuff(byte[] serverStuff) { public void setServerStuff(byte[] serverStuff) {
Packet.assertArray("serverStuff", serverStuff, 100); PacketBody.assertArray("serverStuff", serverStuff, 100);
this.serverStuff = serverStuff; this.serverStuff = serverStuff;
} }
@@ -394,7 +394,7 @@ public class Packet8Init1 extends Packet {
} }
public void setY(byte[] y) { public void setY(byte[] y) {
Packet.assertArray("y", y, 64); PacketBody.assertArray("y", y, 64);
this.y = y; this.y = y;
} }
@@ -0,0 +1,37 @@
package com.github.manevolent.ts3j.protocol.packet;
import com.github.manevolent.ts3j.protocol.ProtocolRole;
import java.nio.ByteBuffer;
public class PacketBodyFragment extends PacketBody {
private byte[] raw;
public PacketBodyFragment(PacketBodyType type, ProtocolRole role) {
super(type, role);
}
public byte[] getRaw() {
return raw;
}
public void setRaw(byte[] raw) {
this.raw = raw;
}
@Override
public void read(ByteBuffer buffer) {
raw = new byte[buffer.remaining()];
buffer.get(raw);
}
@Override
public void write(ByteBuffer buffer) {
buffer.put(raw);
}
@Override
public int getSize() {
return raw.length;
}
}
@@ -0,0 +1,74 @@
package com.github.manevolent.ts3j.protocol.packet;
public enum PacketBodyType {
VOICE(0x0, null, null, false, false, false, false, false),
VOICE_WHISPER(0x1, null, null, false, false, false, false, false),
PONG(0x5, null, null, false, false, false, false, false),
PING(0x4, null, PONG, false, false, false, false, false),
ACK(0x6, PacketBody6Ack.class, null, true, true, false, false, true),
ACK_LOW(0x7, null, null, true, true, false, false, true),
INIT1(0x8, PacketBody8Init1.class, null, false, true, false, false, false),
COMMAND_LOW(0x3, null, ACK_LOW, true, true, true, true, true),
COMMAND(0x2, PacketBody2Command.class, ACK, true, true, true, true, true);
private final int index;
private final Class<? extends PacketBody> packetClass;
private final PacketBodyType acknolwedgedBy;
private final boolean encrypted, resend, splittable, compressible, mustEncrypt;
PacketBodyType(int index, Class<? extends PacketBody> packetClass,
PacketBodyType acknolwedgedBy,
boolean encrypted,
boolean resend,
boolean splittable,
boolean compressible, boolean mustEncrypt) {
this.index = index;
this.packetClass = packetClass;
this.acknolwedgedBy = acknolwedgedBy;
this.resend = resend;
this.encrypted = encrypted;
this.splittable = splittable;
this.compressible = compressible;
this.mustEncrypt = mustEncrypt;
}
public int getIndex() {
return index;
}
public static PacketBodyType fromId(int index) {
for (PacketBodyType value : values())
if (value.getIndex() == index) return value;
throw new IllegalArgumentException("invalid index: " + index);
}
public Class<? extends PacketBody> getPacketClass() {
return packetClass;
}
public boolean canEncrypt() {
return encrypted;
}
public PacketBodyType getAcknolwedgedBy() {
return acknolwedgedBy;
}
public boolean canResend() {
return resend;
}
public boolean isSplittable() {
return splittable;
}
public boolean isCompressible() {
return compressible;
}
public boolean mustEncrypt() {
return mustEncrypt;
}
}
@@ -1,38 +0,0 @@
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;
}
}
@@ -1,69 +0,0 @@
package com.github.manevolent.ts3j.protocol.packet;
public enum PacketType {
VOICE(0x0, null, null, false, false, false, false),
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, 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);
private final int index;
private final Class<? extends Packet> packetClass;
private final PacketType acknolwedgedBy;
private final boolean encrypted, resend, splittable, compressible;
PacketType(int index, Class<? extends Packet> packetClass,
PacketType acknolwedgedBy,
boolean encrypted,
boolean resend,
boolean splittable,
boolean compressible) {
this.index = index;
this.packetClass = packetClass;
this.acknolwedgedBy = acknolwedgedBy;
this.resend = resend;
this.encrypted = encrypted;
this.splittable = splittable;
this.compressible = compressible;
}
public int getIndex() {
return index;
}
public static PacketType fromId(int index) {
for (PacketType value : values())
if (value.getIndex() == index) return value;
throw new IllegalArgumentException("invalid index: " + index);
}
public Class<? extends Packet> getPacketClass() {
return packetClass;
}
public boolean isEncrypted() {
return encrypted;
}
public PacketType getAcknolwedgedBy() {
return acknolwedgedBy;
}
public boolean canResend() {
return resend;
}
public boolean isSplittable() {
return splittable;
}
public boolean isCompressible() {
return compressible;
}
}
@@ -1,50 +0,0 @@
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<NetworkPacket> 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();
}
}
}
@@ -1,88 +0,0 @@
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<Integer, NetworkPacket> queue = new LinkedHashMap<>();
private final Map<PacketType, Boolean> 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<NetworkPacket> 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;
}
}
}
}
@@ -1,30 +1,31 @@
package com.github.manevolent.ts3j.protocol.packet.handler; package com.github.manevolent.ts3j.protocol.packet.handler;
import com.github.manevolent.ts3j.protocol.NetworkPacket; import com.github.manevolent.ts3j.protocol.Packet;
import com.github.manevolent.ts3j.protocol.LocalEndpoint; import com.github.manevolent.ts3j.protocol.socket.client.TeamspeakClientSocket;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.TimeoutException;
public abstract class PacketHandler { public abstract class PacketHandler {
private final LocalEndpoint endpoint; private final TeamspeakClientSocket clientSocket;
protected PacketHandler(LocalEndpoint endpoint) { protected PacketHandler(TeamspeakClientSocket clientSocket) {
this.endpoint = endpoint; this.clientSocket = clientSocket;
} }
public void onUnassigning() throws IOException { public void onUnassigning() throws IOException {
} }
public void onAssigned() throws IOException { public void onAssigned() throws IOException, TimeoutException {
} }
public void handlePacket(NetworkPacket packet) throws IOException { public void handlePacket(Packet packet) throws IOException, TimeoutException {
} }
public LocalEndpoint getEndpoint() { public TeamspeakClientSocket getClient() {
return endpoint; return clientSocket;
} }
} }
@@ -1,9 +0,0 @@
package com.github.manevolent.ts3j.protocol.packet.handler.client;
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient;
public class LocalClientHandlerConnected extends LocalClientHandler {
public LocalClientHandlerConnected(LocalTeamspeakClient client) {
super(client);
}
}
@@ -1,9 +0,0 @@
package com.github.manevolent.ts3j.protocol.packet.handler.client;
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient;
public class LocalClientHandlerDisconnected extends LocalClientHandler {
public LocalClientHandlerDisconnected(LocalTeamspeakClient client) {
super(client);
}
}
@@ -1,16 +1,14 @@
package com.github.manevolent.ts3j.protocol.packet.handler.client; package com.github.manevolent.ts3j.protocol.packet.handler.local;
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient;
import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler; import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler;
import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket;
import java.io.IOException;
public abstract class LocalClientHandler extends PacketHandler { public abstract class LocalClientHandler extends PacketHandler {
private final LocalTeamspeakClient client; private final LocalTeamspeakClientSocket client;
protected LocalClientHandler(LocalTeamspeakClient client) { protected LocalClientHandler(LocalTeamspeakClientSocket client) {
super(client); super(client);
this.client = client; this.client = client;
@@ -20,7 +18,7 @@ public abstract class LocalClientHandler extends PacketHandler {
} }
protected LocalTeamspeakClient getClient() { public LocalTeamspeakClientSocket getClient() {
return client; return client;
} }
} }
@@ -0,0 +1,9 @@
package com.github.manevolent.ts3j.protocol.packet.handler.local;
import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket;
public class LocalClientHandlerConnected extends LocalClientHandler {
public LocalClientHandlerConnected(LocalTeamspeakClientSocket client) {
super(client);
}
}
@@ -1,14 +1,12 @@
package com.github.manevolent.ts3j.protocol.packet.handler.client; package com.github.manevolent.ts3j.protocol.packet.handler.local;
import com.github.manevolent.ts3j.command.SimpleCommand; import com.github.manevolent.ts3j.command.SimpleCommand;
import com.github.manevolent.ts3j.command.part.CommandSingleParameter; import com.github.manevolent.ts3j.command.part.CommandSingleParameter;
import com.github.manevolent.ts3j.license.License; import com.github.manevolent.ts3j.protocol.Packet;
import com.github.manevolent.ts3j.protocol.NetworkPacket;
import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.ProtocolRole;
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command;
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient; import com.github.manevolent.ts3j.protocol.packet.PacketBody8Init1;
import com.github.manevolent.ts3j.protocol.packet.Packet2Command; import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket;
import com.github.manevolent.ts3j.protocol.packet.Packet8Init1;
import com.github.manevolent.ts3j.util.Ts3Crypt; import com.github.manevolent.ts3j.util.Ts3Crypt;
import com.github.manevolent.ts3j.util.Ts3Logging; import com.github.manevolent.ts3j.util.Ts3Logging;
import javafx.util.Pair; import javafx.util.Pair;
@@ -16,27 +14,26 @@ import org.bouncycastle.math.ec.ECPoint;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Base64; import java.util.Base64;
import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.concurrent.TimeoutException;
public class LocalClientHandlerConnecting extends LocalClientHandler { public class LocalClientHandlerConnecting extends LocalClientHandler {
private byte[] randomBytes; private byte[] randomBytes;
private byte[] alphaBytes; private byte[] alphaBytes;
public LocalClientHandlerConnecting(LocalTeamspeakClient client) { public LocalClientHandlerConnecting(LocalTeamspeakClientSocket client) {
super(client); super(client);
} }
@Override @Override
public void onAssigned() throws IOException { public void onAssigned() throws IOException, TimeoutException {
Packet8Init1 packet = new Packet8Init1(ProtocolRole.CLIENT); PacketBody8Init1 packet = new PacketBody8Init1(ProtocolRole.CLIENT);
// Initialize connection // Initialize connection
Packet8Init1.Step0 step = new Packet8Init1.Step0(); PacketBody8Init1.Step0 step = new PacketBody8Init1.Step0();
Random random = new Random(); Random random = new Random();
randomBytes = new byte[4]; randomBytes = new byte[4];
random.nextBytes(randomBytes); random.nextBytes(randomBytes);
@@ -47,7 +44,7 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
sendInit1(packet); sendInit1(packet);
} }
private void sendInit1(Packet8Init1 packet) throws IOException { private void sendInit1(PacketBody8Init1 packet) throws IOException, TimeoutException {
Ts3Logging.debug("Connecting: sending " + packet.getClass().getSimpleName() Ts3Logging.debug("Connecting: sending " + packet.getClass().getSimpleName()
+ ":" + ":"
+ packet.getStep().getClass().getSimpleName() + "..." + packet.getStep().getClass().getSimpleName() + "..."
@@ -55,19 +52,19 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
packet.setVersion(new byte[] { 0x09, (byte)0x83, (byte)0x8C, (byte)0xCF }); packet.setVersion(new byte[] { 0x09, (byte)0x83, (byte)0x8C, (byte)0xCF });
getClient().send(packet); getClient().writePacket(packet);
} }
@Override @Override
public void handlePacket(NetworkPacket packet) throws IOException { public void handlePacket(Packet packet) throws IOException, TimeoutException {
Ts3Logging.debug("Connecting: handling " + packet.getPacket().getClass().getSimpleName()); Ts3Logging.debug("Connecting: handling " + packet.getBody().getClass().getSimpleName());
if (packet.getPacket() instanceof Packet8Init1) { if (packet.getBody() instanceof PacketBody8Init1) {
Packet8Init1 init1 = (Packet8Init1) packet.getPacket(); PacketBody8Init1 init1 = (PacketBody8Init1) packet.getBody();
switch (init1.getStep().getNumber()) { switch (init1.getStep().getNumber()) {
case 1: case 1:
Packet8Init1.Step1 serverReplyStep1 = (Packet8Init1.Step1)init1.getStep(); PacketBody8Init1.Step1 serverReplyStep1 = (PacketBody8Init1.Step1)init1.getStep();
// Check nonce. It's received backwards, so walk backwards over the array received // Check nonce. It's received backwards, so walk backwards over the array received
for (int i = 0; i < 4; i ++) { for (int i = 0; i < 4; i ++) {
if (randomBytes[3 - i] != serverReplyStep1.getA0reversed()[i]) if (randomBytes[3 - i] != serverReplyStep1.getA0reversed()[i])
@@ -75,9 +72,9 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
} }
// Build response // Build response
Packet8Init1 response2 = new Packet8Init1(ProtocolRole.CLIENT); PacketBody8Init1 response2 = new PacketBody8Init1(ProtocolRole.CLIENT);
Packet8Init1.Step2 step2 = new Packet8Init1.Step2(); PacketBody8Init1.Step2 step2 = new PacketBody8Init1.Step2();
step2.setA0reversed(serverReplyStep1.getA0reversed()); step2.setA0reversed(serverReplyStep1.getA0reversed());
step2.setServerStuff(serverReplyStep1.getServerStuff()); step2.setServerStuff(serverReplyStep1.getServerStuff());
response2.setStep(step2); response2.setStep(step2);
@@ -85,7 +82,7 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
sendInit1(response2); sendInit1(response2);
break; break;
case 3: case 3:
Packet8Init1.Step3 serverReplyStep3 = (Packet8Init1.Step3)init1.getStep(); PacketBody8Init1.Step3 serverReplyStep3 = (PacketBody8Init1.Step3)init1.getStep();
// Calculate 'y' // Calculate 'y'
// which is the result of x ^ (2 ^ level) % n as an unsigned // which is the result of x ^ (2 ^ level) % n as an unsigned
@@ -113,9 +110,9 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
); );
// Build response // Build response
Packet8Init1 response4 = new Packet8Init1(ProtocolRole.CLIENT); PacketBody8Init1 response4 = new PacketBody8Init1(ProtocolRole.CLIENT);
Packet8Init1.Step4 step4 = new Packet8Init1.Step4(); PacketBody8Init1.Step4 step4 = new PacketBody8Init1.Step4();
step4.setLevel(serverReplyStep3.getLevel()); step4.setLevel(serverReplyStep3.getLevel());
step4.setX(serverReplyStep3.getX()); step4.setX(serverReplyStep3.getX());
@@ -130,7 +127,7 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
initiv.add(new CommandSingleParameter("alpha", Base64.getEncoder().encodeToString(alphaBytes))); initiv.add(new CommandSingleParameter("alpha", Base64.getEncoder().encodeToString(alphaBytes)));
initiv.add(new CommandSingleParameter("omega", getClient().getLocalIdentity().getPublicKeyString())); initiv.add(new CommandSingleParameter("omega", getClient().getIdentity().getPublicKeyString()));
initiv.add(new CommandSingleParameter("ot", "1")); // constant, set to 1 initiv.add(new CommandSingleParameter("ot", "1")); // constant, set to 1
@@ -151,8 +148,8 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
throw new IllegalArgumentException("unexpected Init1 server step: " + init1.getStep().getNumber()); throw new IllegalArgumentException("unexpected Init1 server step: " + init1.getStep().getNumber());
} }
} else if (packet.getPacket() instanceof Packet2Command) { } else if (packet.getBody() instanceof PacketBody2Command) {
SimpleCommand command = ((Packet2Command) packet.getPacket()).parse(); SimpleCommand command = ((PacketBody2Command) packet.getBody()).parse();
if (command.getName().equalsIgnoreCase("initivexpand2")) { if (command.getName().equalsIgnoreCase("initivexpand2")) {
// 3.2.2 initivexpand2 (Client <- Server) // 3.2.2 initivexpand2 (Client <- Server)
@@ -186,12 +183,12 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
byte[] toSign = new byte[86]; byte[] toSign = new byte[86];
System.arraycopy(keyPair.getKey(), 0, toSign, 0, 32); System.arraycopy(keyPair.getKey(), 0, toSign, 0, 32);
System.arraycopy(beta, 0, toSign, 32, 54); System.arraycopy(beta, 0, toSign, 32, 54);
byte[] sign = getClient().getLocalIdentity().sign(toSign); byte[] sign = getClient().getIdentity().sign(toSign);
Ts3Logging.debug("Created proof (clientek): " + Ts3Logging.getHex(sign)); Ts3Logging.debug("Created proof (clientek): " + Ts3Logging.getHex(sign));
// Send clientek (telling them the shared secret) // Send clientek (telling them the shared secret)
Packet2Command clientek = new Packet2Command(ProtocolRole.CLIENT); PacketBody2Command clientek = new PacketBody2Command(ProtocolRole.CLIENT);
clientek.setText( clientek.setText(
new SimpleCommand( new SimpleCommand(
"clientek", ProtocolRole.CLIENT, "clientek", ProtocolRole.CLIENT,
@@ -199,12 +196,14 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
new CommandSingleParameter("proof", Base64.getEncoder().encodeToString(sign)) new CommandSingleParameter("proof", Base64.getEncoder().encodeToString(sign))
).build() ).build()
); );
getClient().send(clientek); getClient().writePacket(clientek);
Ts3Crypt.SecureChannelParameters secureChannelParameters = getClient().setSecureParameters(
Ts3Crypt.cryptoInit2(license, alphaBytes, omega, proof, beta, keyPair.getValue()); Ts3Crypt.cryptoInit2(license, alphaBytes, omega, proof, beta, keyPair.getValue())
);
PacketBody2Command clientinit = new PacketBody2Command(ProtocolRole.CLIENT);
Packet2Command clientinit = new Packet2Command(ProtocolRole.CLIENT);
clientinit.setText( clientinit.setText(
new SimpleCommand( new SimpleCommand(
"clientinit", ProtocolRole.CLIENT, "clientinit", ProtocolRole.CLIENT,
@@ -220,11 +219,12 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
new CommandSingleParameter("client_nickname_phonetic", "test"), new CommandSingleParameter("client_nickname_phonetic", "test"),
new CommandSingleParameter("client_meta_data", ""), new CommandSingleParameter("client_meta_data", ""),
new CommandSingleParameter("client_default_token", ""), new CommandSingleParameter("client_default_token", ""),
new CommandSingleParameter("client_key_offset", Long.toString(getClient().getLocalIdentity().getKeyOffset())), new CommandSingleParameter("client_key_offset", Long.toString(getClient().getIdentity().getKeyOffset())),
new CommandSingleParameter("hwid", "87056c6e1268aaf5055abf8256415e0e,408978b6d98810cc03f0aa16a4c75600") new CommandSingleParameter("hwid", "87056c6e1268aaf5055abf8256415e0e,408978b6d98810cc03f0aa16a4c75600")
).build() ).build()
); );
getClient().send(clientinit);
getClient().writePacket(clientinit);
} }
} }
} }
@@ -0,0 +1,9 @@
package com.github.manevolent.ts3j.protocol.packet.handler.local;
import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket;
public class LocalClientHandlerDisconnected extends LocalClientHandler {
public LocalClientHandlerDisconnected(LocalTeamspeakClientSocket client) {
super(client);
}
}
@@ -0,0 +1,9 @@
package com.github.manevolent.ts3j.protocol.packet.handler.local;
import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket;
public class LocalClientHandlerNegotiating extends LocalClientHandler {
public LocalClientHandlerNegotiating(LocalTeamspeakClientSocket client) {
super(client);
}
}
@@ -1,6 +1,6 @@
package com.github.manevolent.ts3j.protocol.packet.transformation; package com.github.manevolent.ts3j.protocol.packet.transformation;
import com.github.manevolent.ts3j.protocol.NetworkPacket; import com.github.manevolent.ts3j.protocol.Packet;
import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.ProtocolRole;
import com.github.manevolent.ts3j.protocol.header.PacketHeader; import com.github.manevolent.ts3j.protocol.header.PacketHeader;
import com.github.manevolent.ts3j.util.Ts3Logging; import com.github.manevolent.ts3j.util.Ts3Logging;
@@ -72,7 +72,7 @@ public class PacketTransformation {
return new Pair<>(key, nonce); return new Pair<>(key, nonce);
} }
public ByteBuffer encrypt(NetworkPacket packet) { public ByteBuffer encrypt(Packet packet) {
Pair<byte[], byte[]> parameters = computeParameters(packet.getHeader()); Pair<byte[], byte[]> parameters = computeParameters(packet.getHeader());
// Write header (this is temporary) // Write header (this is temporary)
@@ -93,7 +93,7 @@ public class PacketTransformation {
headerWithoutMac headerWithoutMac
); );
int dataLen = packet.getPacket().getSize(); int dataLen = packet.getBody().getSize();
ByteBuffer packetBuffer = packet.writeBody(ByteBuffer.allocate(dataLen).order(ByteOrder.BIG_ENDIAN)); ByteBuffer packetBuffer = packet.writeBody(ByteBuffer.allocate(dataLen).order(ByteOrder.BIG_ENDIAN));
byte[] result; byte[] result;
@@ -1,36 +0,0 @@
package com.github.manevolent.ts3j.protocol.server;
import com.github.manevolent.ts3j.protocol.NetworkPacket;
import com.github.manevolent.ts3j.protocol.RemoteEndpoint;
import com.github.manevolent.ts3j.protocol.SocketRole;
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient;
import com.github.manevolent.ts3j.protocol.packet.Packet;
import java.io.IOException;
import java.net.InetSocketAddress;
public class RemoteTeamspeakServer extends RemoteEndpoint implements TeamspeakServer {
private final LocalTeamspeakClient client;
private final InetSocketAddress address;
public RemoteTeamspeakServer(LocalTeamspeakClient client,
InetSocketAddress address) {
this.client = client;
this.address = address;
}
@Override
public SocketRole getRole() {
return SocketRole.SERVER;
}
@Override
public void send(Packet packet) throws IOException {
client.send(packet);
}
@Override
public InetSocketAddress getRemoteAddress() {
return address;
}
}
@@ -1,11 +0,0 @@
package com.github.manevolent.ts3j.protocol.server;
import com.github.manevolent.ts3j.protocol.Endpoint;
import java.net.InetSocketAddress;
public interface TeamspeakServer extends Endpoint {
}
@@ -1,18 +1,15 @@
package com.github.manevolent.ts3j.protocol.socket; package com.github.manevolent.ts3j.protocol.socket;
import com.github.manevolent.ts3j.protocol.packet.transformation.DefaultPacketTransformation; import com.github.manevolent.ts3j.protocol.SocketRole;
import com.github.manevolent.ts3j.protocol.packet.transformation.PacketTransformation;
public abstract class AbstractTeamspeakSocket implements TeamspeakSocket { public abstract class AbstractTeamspeakSocket implements TeamspeakSocket {
private PacketTransformation transformation = new DefaultPacketTransformation(); private final SocketRole role;
@Override protected AbstractTeamspeakSocket(SocketRole role) {
public PacketTransformation getPacketTransformation() { this.role = role;
return transformation;
} }
@Override public SocketRole getRole() {
public void setPacketTransformation(PacketTransformation transformation) { return role;
this.transformation = transformation;
} }
} }
@@ -1,176 +0,0 @@
package com.github.manevolent.ts3j.protocol.socket;
import com.github.manevolent.ts3j.protocol.NetworkPacket;
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;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Low-level protocol socket (not a client, used for server implementation also)
*
* Used simply to transcribe packets on the network
*/
public class LocalTeamspeakSocket extends AbstractTeamspeakSocket implements TeamspeakSocket {
private final SocketRole socketRole;
private final DatagramSocket datagramSocket;
private final DatagramPacket datagramPacket = new DatagramPacket(new byte[1500], 1500);
public LocalTeamspeakSocket(SocketRole socketRole, DatagramSocket datagramSocket) {
this.socketRole = socketRole;
this.datagramSocket = datagramSocket;
}
public LocalTeamspeakSocket(SocketRole socketRole) throws SocketException {
this(socketRole, new DatagramSocket());
}
public SocketRole getSocketRole() {
return socketRole;
}
public InetSocketAddress getLocalAddress() {
return new InetSocketAddress(datagramSocket.getLocalAddress(), datagramSocket.getLocalPort());
}
public InetSocketAddress getRemoteAddress() {
return new InetSocketAddress(datagramSocket.getInetAddress(), datagramSocket.getPort());
}
public void connect(SocketAddress remote) throws SocketException {
datagramSocket.connect(remote);
}
/**
* Writes an actual datagram to the network
* @param packet Packet to write
* @throws IOException
*/
private void sendNetworkPacket(DatagramPacket packet) throws IOException {
if (packet.getAddress() == null || packet.getPort() < 0)
packet.setSocketAddress(datagramSocket.getRemoteSocketAddress());
Ts3Logging.debug(
"[NETWORK] WRITE Len=" + packet.getLength() + " to " + packet.getSocketAddress()
);
//Ts3Logging.debug(Ts3Logging.getHex(packet.getData(), packet.getLength()));
datagramSocket.send(packet);
}
/**
* Sends a NetworkPacket to the network, first converting it to a datagram.
* @param header Header to send
* @throws IOException
*/
public void send(PacketHeader header, Packet packet) throws IOException {
if (header == null)
throw new NullPointerException("header");
else if (header.getRole() != getSocketRole().getOut())
throw new IllegalArgumentException("packet role mismatch: " +
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);
// Flush to a buffer
ByteBuffer outputBuffer;
if (networkPacket.getHeader().getType().isEncrypted() &&
!networkPacket.getHeader().getPacketFlag(HeaderFlag.UNENCRYPTED)) {
outputBuffer = getPacketTransformation().encrypt(networkPacket);
} else {
outputBuffer = ByteBuffer.allocate(networkPacket.getSize());
outputBuffer.order(ByteOrder.BIG_ENDIAN);
networkPacket.writeHeader(outputBuffer);
networkPacket.writeBody(outputBuffer);
}
Ts3Logging.debug("[PROTOCOL] WRITE " + packet.getType().name());
sendNetworkPacket(new DatagramPacket(outputBuffer.array(), outputBuffer.position()));
}
/**
* Receives a datagram from the network.
* @return Datagram.
* @throws IOException
*/
private DatagramPacket receiveNetworkPacket() throws IOException {
datagramSocket.receive(datagramPacket);
Ts3Logging.debug(
"[NETWORK] READ Len=" + datagramPacket.getLength() + " from " + datagramPacket.getSocketAddress()
);
//Ts3Logging.debug(Ts3Logging.getHex(datagramPacket.getData(), datagramPacket.getLength()));
return datagramPacket;
}
public NetworkPacket receive() throws IOException {
while (true) {
DatagramPacket datagramPacket = receiveNetworkPacket();
NetworkPacket networkPacket = new NetworkPacket(getSocketRole().getIn());
ByteBuffer buffer = ByteBuffer.wrap(datagramPacket.getData());
networkPacket.readHeader(buffer);
boolean fragment = networkPacket.getHeader().getPacketFlag(HeaderFlag.FRAGMENTED);
if (fragment && !networkPacket.getHeader().getType().isSplittable()) continue; // drop packet
ByteBuffer packetBuffer;
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;
}
}
}
@@ -1,19 +1,14 @@
package com.github.manevolent.ts3j.protocol.socket; package com.github.manevolent.ts3j.protocol.socket;
import com.github.manevolent.ts3j.protocol.SocketRole; import com.github.manevolent.ts3j.protocol.SocketRole;
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
import com.github.manevolent.ts3j.protocol.packet.Packet;
import com.github.manevolent.ts3j.protocol.packet.transformation.PacketTransformation;
import java.io.IOException; import java.io.Closeable;
import java.net.InetSocketAddress;
public interface TeamspeakSocket { public interface TeamspeakSocket extends Closeable {
SocketRole getSocketRole();
void send(PacketHeader header, Packet packet) throws IOException; SocketRole getRole();
PacketTransformation getPacketTransformation(); InetSocketAddress getLocalSocketAddress();
void setPacketTransformation(PacketTransformation transformation);
} }
@@ -0,0 +1,452 @@
package com.github.manevolent.ts3j.protocol.socket.client;
import com.github.manevolent.ts3j.command.Command;
import com.github.manevolent.ts3j.command.response.CommandResponse;
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.SocketRole;
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
import com.github.manevolent.ts3j.protocol.header.HeaderFlag;
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
import com.github.manevolent.ts3j.protocol.packet.PacketBody;
import com.github.manevolent.ts3j.protocol.packet.PacketBody6Ack;
import com.github.manevolent.ts3j.protocol.packet.PacketBodyFragment;
import com.github.manevolent.ts3j.protocol.packet.PacketBodyType;
import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler;
import com.github.manevolent.ts3j.protocol.packet.handler.local.LocalClientHandler;
import com.github.manevolent.ts3j.protocol.packet.transformation.DefaultPacketTransformation;
import com.github.manevolent.ts3j.protocol.packet.transformation.PacketTransformation;
import com.github.manevolent.ts3j.protocol.socket.AbstractTeamspeakSocket;
import com.github.manevolent.ts3j.util.Ts3Crypt;
import com.github.manevolent.ts3j.util.Ts3Logging;
import java.io.IOException;
import java.net.DatagramPacket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
import java.util.concurrent.TimeoutException;
public abstract class AbstractTeamspeakClientSocket
extends AbstractTeamspeakSocket
implements TeamspeakClientSocket {
private final Object connectionStateLock = new Object();
private final Map<PacketBodyType, ReassemblyQueue> reassemblyQueue = new LinkedHashMap<>();
private final NetworkReader networkReader = new NetworkReader();
private final Map<String, Object> clientOptions = new LinkedHashMap<>();
private Identity identity = null;
private ClientConnectionState connectionState = null;
private PacketTransformation transformation = new DefaultPacketTransformation();
private PacketHandler handler;
private Thread networkThread = null;
private boolean reading = false;
protected AbstractTeamspeakClientSocket() {
super(SocketRole.CLIENT);
try {
setState(ClientConnectionState.DISCONNECTED);
} catch (Exception e) {
throw new RuntimeException(e);
}
// Initialize reassembly queue
for (PacketBodyType bodyType : PacketBodyType.values())
if (bodyType.isSplittable())
reassemblyQueue.put(bodyType, new ReassemblyQueue());
}
protected void start() {
Thread networkThread = new Thread(networkReader);
networkThread.setDaemon(true);
networkThread.start();
}
@Override
public Map<String, Object> getOptions() {
return clientOptions;
}
public final void setSecureParameters(Ts3Crypt.SecureChannelParameters parameters) {
setTransformation(new PacketTransformation(parameters.getIvStruct()));
}
protected abstract NetworkPacket readNetworkPacket() throws IOException;
protected abstract void writeNetworkPacket(NetworkPacket packet) throws IOException;
public void setState(ClientConnectionState state) throws IOException, TimeoutException {
synchronized (connectionStateLock) {
if (state != this.connectionState) {
Ts3Logging.debug("State changing: " + state.name());
boolean wasDisconnected = this.connectionState == ClientConnectionState.DISCONNECTED;
if (wasDisconnected) setReading(true);
this.connectionState = state;
if (this.connectionState == ClientConnectionState.DISCONNECTED) setReading(false);
PacketHandler handler = getHandler();
if (handler != null)
((LocalClientHandler) handler).handleConnectionStateChanging(state);
if (handler == null || handler.getClass() != state.getHandlerClass()) {
Ts3Logging.debug("Assigning " + state.getHandlerClass().getName() + " handler...");
setHandler(state.createHandler(this));
}
if (this.connectionState == ClientConnectionState.DISCONNECTED)
clientOptions.clear();
connectionStateLock.notifyAll();
Ts3Logging.debug("State changed: " + state.name());
}
}
}
public ClientConnectionState getState() {
return connectionState;
}
public void waitForState(ClientConnectionState state, long wait)
throws InterruptedException, TimeoutException {
synchronized (connectionStateLock) {
long start = System.currentTimeMillis();
while (connectionState != state && System.currentTimeMillis() - start < wait) {
connectionStateLock.wait(Math.max(0L, wait - (System.currentTimeMillis() - start)));
}
if (connectionState != state)
throw new TimeoutException("timeout waiting for " + state.name() + " state");
}
}
public boolean isConnected() {
return connectionState == ClientConnectionState.CONNECTED;
}
/**
* 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");
CommandResponse response;
if (command.willExpectResponse()) {
//command.appendParameter(new CommandSingleParameter("return_code", returnCodeIndex));
response = new CommandResponse(command, 0); // TODO
} else {
response = new CommandResponse(command, -1);
}
String commandString = command.toString();
response.setDispatchedTime(System.currentTimeMillis());
return response;
}
@Override
public void writePacket(PacketBody body) throws IOException, TimeoutException {
PacketHeader header;
try {
header = body.getRole().getHeaderClass().newInstance();
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
// Construct a network packet
Packet packet = new Packet(body.getRole());
packet.setHeader(header);
packet.setBody(body);
writePacket(packet);
}
@Override
public void writePacket(Packet packet) throws IOException, TimeoutException {
if (packet.getHeader() == null)
throw new NullPointerException("header");
else if (packet.getHeader().getRole() != getRole().getOut())
throw new IllegalArgumentException("packet role mismatch: " +
packet.getHeader().getRole().name() + " != " +
getRole().getOut().name());
// Ensure type matches
packet.getHeader().setType(packet.getHeader().getType());
// Make sure new protocol is set if it must be set
if (getState() == ClientConnectionState.CONNECTING)
packet.getHeader().setPacketFlag(HeaderFlag.NEW_PROTOCOL, true);
// Set header values
packet.getBody().setHeaderValues(packet.getHeader());
// Flush to a buffer
ByteBuffer outputBuffer;
if (packet.getBody().getType().canEncrypt() &&
!packet.getHeader().getPacketFlag(HeaderFlag.UNENCRYPTED)) {
outputBuffer = getTransformation().encrypt(packet);
} else {
outputBuffer = ByteBuffer.allocate(packet.getSize());
outputBuffer.order(ByteOrder.BIG_ENDIAN);
packet.writeHeader(outputBuffer);
packet.writeBody(outputBuffer);
}
Ts3Logging.debug("[PROTOCOL] WRITE " + packet.getHeader().getType().name());
// Find if the packet must be acknowledged and create a promise for that action
// TODO
writeNetworkPacket(new NetworkPacket(
new DatagramPacket(outputBuffer.array(), outputBuffer.position()),
packet.getHeader(),
(ByteBuffer) outputBuffer.position(0)
));
// If necessary, fulfill promise for the packet acknowledgement or throw TimeoutException
// TODO
}
private Packet readPacketIntl() throws IOException {
NetworkPacket networkPacket = readNetworkPacket();
if (networkPacket.getHeader() == null) throw new NullPointerException("header");
Packet packet = new Packet(networkPacket.getHeader().getRole());
packet.setHeader(networkPacket.getHeader());
boolean fragment = networkPacket.getHeader().getPacketFlag(HeaderFlag.FRAGMENTED);
if (networkPacket.getHeader().getType().isSplittable()) {
if (fragment)
throw new IllegalArgumentException("packet is fragment, but not splittable");
else {
fragment = reassemblyQueue.get(networkPacket.getHeader().getType()).isReassembling();
}
}
boolean encrypted = !networkPacket.getHeader().getPacketFlag(HeaderFlag.UNENCRYPTED) &&
networkPacket.getHeader().getType().canEncrypt();
if (networkPacket.getHeader().getType().mustEncrypt() && !encrypted)
throw new IllegalArgumentException("packet is unencrypted, but must encrypt");
ByteBuffer packetBuffer =
(ByteBuffer) networkPacket.getBuffer().position(networkPacket.getHeader().getSize());
// Decrypt
if (encrypted) {
packetBuffer = ByteBuffer.wrap(
getTransformation().decrypt(
networkPacket.getHeader(),
packetBuffer,
networkPacket.getDatagram().getLength() - networkPacket.getHeader().getSize()
)
);
}
// Fragment handling
if (fragment) {
Ts3Logging.debug("[PROTOCOL] READ " + networkPacket.getHeader().getType().name() + " fragment");
packet.setBody(
new PacketBodyFragment(
networkPacket.getHeader().getType(),
getRole().getIn()
)
);
} else {
Ts3Logging.debug("[PROTOCOL] READ " + networkPacket.getHeader().getType().name());
}
// Compression?
// TODO
// Read packet body
packet.readBody(packetBuffer);
return packet;
}
@Override
public Packet readPacket() throws IOException, TimeoutException {
while (true) {
Packet packet = readPacketIntl();
// Find if the packet must be acknowledged
PacketBodyType ackType = packet.getHeader().getType().getAcknolwedgedBy();
if (ackType != null) {
switch (ackType) {
case ACK:
writePacket(new PacketBody6Ack(getRole().getOut(), packet.getHeader().getPacketId()));
break;
}
}
// Find if the packet is itself an acknowledgement
switch (packet.getHeader().getType()) {
case ACK:
break;
case ACK_LOW:
break;
default:
getHandler().handlePacket(packet);
}
}
}
@Override
public PacketTransformation getTransformation() {
return transformation;
}
@Override
public void setTransformation(PacketTransformation transformation) {
this.transformation = transformation;
}
@Override
public PacketHandler getHandler() {
return handler;
}
protected void setHandler(PacketHandler handler) throws IOException, TimeoutException {
if (this.handler != handler) {
if (this.handler != null) this.handler.onUnassigning();
this.handler = handler;
if (handler != null) handler.onAssigned();
}
}
public Identity getIdentity() {
return identity;
}
public void setIdentity(Identity identity) {
this.identity = identity;
}
protected boolean isReading() {
return reading;
}
protected void setReading(boolean b) {
if (this.reading != b) {
this.reading = b;
if (!reading) {
if (networkThread != null) networkThread.interrupt();
} else {
start();
}
}
}
private class NetworkReader implements Runnable {
@Override
public void run() {
while (isReading()) {
try {
readPacket();
} catch (Exception e) {
if (e instanceof InterruptedException) continue;
Ts3Logging.debug("Problem handling packet", e);
}
}
}
}
private class ReassemblyQueue {
private final Map<Integer, Packet> queue = new LinkedHashMap<>();
private boolean state = false;
public Packet reassemble(Packet lastFragment) {
// Pull out all other packets in the queue before this one which would also be fragmented
List<Packet> reassemblyList = new ArrayList<>();
for (int packetId = lastFragment.getHeader().getPacketId();; packetId--) {
Packet olderPacket = queue.get(packetId);
if (olderPacket == null)
break; // Chain breaks here
else if (olderPacket.getHeader().getType() != lastFragment.getHeader().getType())
continue; // skip
else {
reassemblyList.add(olderPacket);
}
}
// Reverse the collection
Collections.reverse(reassemblyList);
// Rebuild a master packet from the contents of all previous packet fragments
int totalLength = reassemblyList.stream().mapToInt(x -> x.getBody().getSize()).sum() + lastFragment.getBody().getSize();
if (totalLength < 0) throw new IllegalArgumentException("reassembly too small: " + totalLength);
ByteBuffer reassemblyBuffer = ByteBuffer.allocate(totalLength);
for (Packet old : reassemblyList)
old.writeBody(reassemblyBuffer);
Packet firstPacket = reassemblyList.get(0);
Packet reassembledPacket = new Packet(firstPacket.getRole());
reassembledPacket.setHeader(firstPacket.getHeader());
reassembledPacket.getHeader().setPacketFlag(HeaderFlag.FRAGMENTED, false);
reassembledPacket.readBody((ByteBuffer) reassemblyBuffer.position(0));
return reassembledPacket;
}
public Packet put(Packet packet) {
if (packet.getHeader().getPacketFlag(HeaderFlag.FRAGMENTED)) {
boolean oldState = state;
state = !state;
if (!(packet.getBody() instanceof PacketBodyFragment))
throw new IllegalArgumentException("packet fragment object is not representative of a fragment");
boolean fragmentationEnded = oldState;
queue.put(packet.getHeader().getPacketId(), packet);
if (fragmentationEnded)
return reassemble(packet);
return null;
} else {
if (state) {
queue.put(packet.getHeader().getPacketId(), packet);
return null;
} else {
return packet;
}
}
}
public boolean isReassembling() {
return state;
}
}
}
@@ -0,0 +1,124 @@
package com.github.manevolent.ts3j.protocol.socket.client;
import com.github.manevolent.ts3j.identity.LocalIdentity;
import com.github.manevolent.ts3j.protocol.NetworkPacket;
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
import com.github.manevolent.ts3j.util.Ts3Logging;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.TimeoutException;
public class LocalTeamspeakClientSocket extends AbstractTeamspeakClientSocket {
private final DatagramSocket socket;
private final DatagramPacket packet = new DatagramPacket(new byte[500], 500);
{
try {
socket = new DatagramSocket();
} catch (SocketException e) {
throw new RuntimeException(e);
}
}
public LocalTeamspeakClientSocket() {
super();
}
@Override
protected NetworkPacket readNetworkPacket() throws IOException {
socket.receive(packet);
PacketHeader header;
try {
header = getRole().getIn().getHeaderClass().newInstance();
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
ByteBuffer buffer = ByteBuffer
.wrap(packet.getData(), packet.getOffset(), packet.getLength())
.order(ByteOrder.BIG_ENDIAN);
header.read(buffer);
return new NetworkPacket(
packet,
header,
buffer
);
}
@Override
protected void writeNetworkPacket(NetworkPacket packet) throws IOException {
socket.send(packet.getDatagram());
}
@Override
public LocalIdentity getIdentity() {
return (LocalIdentity) super.getIdentity();
}
public void setIdentity(LocalIdentity identity) {
super.setIdentity(identity);
}
/**
* 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, TimeoutException {
try {
Ts3Logging.debug("Connecting to " + remote + "...");
ClientConnectionState connectionState = getState();
if (connectionState != ClientConnectionState.DISCONNECTED)
throw new IllegalStateException(connectionState.name());
setOption("client.hostname", remote.getHostString());
setOption("client.password", password);
socket.connect(remote);
Ts3Logging.debug("Changing state...");
setState(ClientConnectionState.CONNECTING);
waitForState(ClientConnectionState.CONNECTED, timeout);
} catch (TimeoutException e) {
throw e;
} catch (Throwable e) {
setState(ClientConnectionState.DISCONNECTED);
throw new IOException(e);
}
}
@Override
protected boolean isReading() {
return socket.isConnected();
}
@Override
public InetSocketAddress getLocalSocketAddress() {
return (InetSocketAddress) socket.getLocalSocketAddress();
}
@Override
public void close() throws IOException {
socket.close();
}
}
@@ -0,0 +1,56 @@
package com.github.manevolent.ts3j.protocol.socket.client;
import com.github.manevolent.ts3j.identity.Identity;
import com.github.manevolent.ts3j.protocol.Packet;
import com.github.manevolent.ts3j.protocol.packet.PacketBody;
import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler;
import com.github.manevolent.ts3j.protocol.packet.transformation.PacketTransformation;
import com.github.manevolent.ts3j.protocol.socket.TeamspeakSocket;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.TimeoutException;
public interface TeamspeakClientSocket extends TeamspeakSocket {
Map<String, Object> getOptions();
default <T extends Object> T getOption(String key, Class<T> clazz) {
Object value = getOptions().get(key);
if (value == null) return (T) null;
else if (!value.getClass().isAssignableFrom(clazz)) {
throw new ClassCastException(
clazz.getName()
+ " is not assignable to option type " +
value.getClass().getName()
);
} else
return (T) value;
}
default Object setOption(String key, Object value) {
return getOptions().put(key, value);
}
boolean isConnected();
Identity getIdentity();
void setIdentity(Identity identity);
PacketTransformation getTransformation();
void setTransformation(PacketTransformation transformation);
PacketHandler getHandler();
Packet readPacket() throws IOException, TimeoutException;
void writePacket(PacketBody body) throws IOException, TimeoutException;
void writePacket(Packet packet) throws IOException, TimeoutException;
}
@@ -0,0 +1,9 @@
package com.github.manevolent.ts3j.protocol.socket.server;
import com.github.manevolent.ts3j.protocol.socket.client.TeamspeakClientSocket;
public interface TeamspeakServerSocket {
TeamspeakClientSocket accept();
}
@@ -2,7 +2,7 @@ package com.github.manevolent.ts3j;
import com.github.manevolent.ts3j.identity.LocalIdentity; import com.github.manevolent.ts3j.identity.LocalIdentity;
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient; import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket;
import junit.framework.TestCase; import junit.framework.TestCase;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
@@ -16,9 +16,9 @@ public class ServerConnectionTest extends TestCase {
try { try {
LocalIdentity localIdentity = LocalIdentity.generateNew(8); LocalIdentity localIdentity = LocalIdentity.generateNew(8);
LocalTeamspeakClient client = new LocalTeamspeakClient(); LocalTeamspeakClientSocket client = new LocalTeamspeakClientSocket();
client.setLocalIdentity(localIdentity); client.setIdentity(localIdentity);
client.connect(new InetSocketAddress( client.connect(new InetSocketAddress(
"ts.teamlixo.net", "ts.teamlixo.net",
@@ -27,7 +27,7 @@ public class ServerConnectionTest extends TestCase {
10000L 10000L
); );
assertEquals(client.getConnectionState(), ClientConnectionState.CONNECTED); assertEquals(client.getState(), ClientConnectionState.CONNECTED);
} catch (Throwable ex) { } catch (Throwable ex) {
ex.printStackTrace(); ex.printStackTrace();
} }