Adding missing files
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
|
||||
package com.github.manevolent.ts3j;
|
||||
|
||||
import com.github.manevolent.ts3j.handler.*;
|
||||
|
||||
public enum ClientConnectionState {
|
||||
CONNECTING(TeamspeakClientHandlerConnecting.class),
|
||||
CONNECTED(TeamspeakClientHandlerConnected.class),
|
||||
DISCONNECTED(TeamspeakClientHandlerDisconnected.class);
|
||||
|
||||
private final Class<? extends TeamspeakClientHandler> clazz;
|
||||
|
||||
ClientConnectionState(Class<? extends TeamspeakClientHandler> clazz) {
|
||||
this.clazz = clazz;
|
||||
}
|
||||
|
||||
public Class<? extends TeamspeakClientHandler> getHandlerClass() {
|
||||
return clazz;
|
||||
}
|
||||
|
||||
public TeamspeakClientHandler createHandler(Teamspeak3Client client) {
|
||||
try {
|
||||
return clazz.getConstructor(Teamspeak3Client.class).newInstance(client);
|
||||
} catch (ReflectiveOperationException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package com.github.manevolent.ts3j;
|
||||
|
||||
import com.github.manevolent.ts3j.command.Command;
|
||||
import com.github.manevolent.ts3j.command.part.CommandSingleParameter;
|
||||
import com.github.manevolent.ts3j.command.response.CommandResponse;
|
||||
import com.github.manevolent.ts3j.handler.TeamspeakClientHandler;
|
||||
import com.github.manevolent.ts3j.protocol.NetworkPacket;
|
||||
import com.github.manevolent.ts3j.protocol.SocketRole;
|
||||
import com.github.manevolent.ts3j.protocol.Teamspeak3Socket;
|
||||
import com.github.manevolent.ts3j.protocol.header.ClientPacketHeader;
|
||||
import com.github.manevolent.ts3j.protocol.packet.Packet;
|
||||
import com.github.manevolent.ts3j.util.Ts3Logging;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class Teamspeak3Client {
|
||||
private final PacketReceiver packetReceiver = new PacketReceiver();
|
||||
|
||||
private final Teamspeak3Socket socket;
|
||||
private final Object connectionStateLock = new Object();
|
||||
|
||||
private final Map<String, Object> clientOptions = new LinkedHashMap<>();
|
||||
|
||||
private TeamspeakClientHandler handler;
|
||||
|
||||
private InetSocketAddress remote = null;
|
||||
private ClientConnectionState clientConnectionState = null;
|
||||
private int returnCodeIndex = 0;
|
||||
|
||||
public Teamspeak3Client(DatagramSocket datagramSocket) throws SocketException {
|
||||
this.socket = new Teamspeak3Socket(SocketRole.CLIENT, datagramSocket);
|
||||
|
||||
setClientConnectionState(ClientConnectionState.DISCONNECTED);
|
||||
}
|
||||
|
||||
public Teamspeak3Client() throws SocketException {
|
||||
this(new DatagramSocket());
|
||||
}
|
||||
|
||||
public InetSocketAddress getRemote() {
|
||||
return remote;
|
||||
}
|
||||
|
||||
public void send(Packet packet) throws IOException {
|
||||
// Construct header
|
||||
ClientPacketHeader header = new ClientPacketHeader();
|
||||
|
||||
packet.setHeaderValues(header);
|
||||
|
||||
socket.send(header, packet);
|
||||
}
|
||||
|
||||
protected void setClientConnectionState(ClientConnectionState state) {
|
||||
synchronized (connectionStateLock) {
|
||||
if (state != this.clientConnectionState) {
|
||||
Ts3Logging.debug("State changing: " + state.name());
|
||||
|
||||
boolean wasDisconnected = this.clientConnectionState == ClientConnectionState.DISCONNECTED;
|
||||
|
||||
this.clientConnectionState = state;
|
||||
|
||||
if (wasDisconnected) {
|
||||
Ts3Logging.debug("Starting network receiver thread...");
|
||||
|
||||
Thread receiverThread = new Thread(new PacketReceiver());
|
||||
receiverThread.setDaemon(true);
|
||||
receiverThread.start();
|
||||
}
|
||||
|
||||
if (handler != null)
|
||||
handler.handleConnectionStateChanging(state);
|
||||
|
||||
if (handler == null || handler.getClass() != state.getHandlerClass()) {
|
||||
Ts3Logging.debug("Assigning " + state.getHandlerClass().getName() + " handler...");
|
||||
|
||||
handler = state.createHandler(this);
|
||||
|
||||
try {
|
||||
handler.onAssigned();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.clientConnectionState == ClientConnectionState.DISCONNECTED)
|
||||
clientOptions.clear();
|
||||
|
||||
connectionStateLock.notifyAll();
|
||||
|
||||
Ts3Logging.debug("State changed: " + state.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return clientConnectionState == ClientConnectionState.CONNECTED;
|
||||
}
|
||||
|
||||
public ClientConnectionState getClientConnectionState() {
|
||||
return clientConnectionState;
|
||||
}
|
||||
|
||||
private void joinConnectionState(ClientConnectionState state, long wait)
|
||||
throws InterruptedException, TimeoutException {
|
||||
synchronized (connectionStateLock) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
while (clientConnectionState != state && System.currentTimeMillis() - start < wait) {
|
||||
connectionStateLock.wait(Math.max(0L, wait - (System.currentTimeMillis() - start)));
|
||||
}
|
||||
|
||||
if (clientConnectionState != state)
|
||||
throw new TimeoutException("timeout waiting for " + state.name() + " state");
|
||||
}
|
||||
}
|
||||
|
||||
public <T extends Object> T getOption(String key, Class<T> clazz) {
|
||||
Object value = clientOptions.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;
|
||||
}
|
||||
|
||||
public Object setOption(String key, Object value) {
|
||||
return clientOptions.put(key, value);
|
||||
}
|
||||
|
||||
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 void connect(InetSocketAddress remote, String password, long timeout) throws IOException {
|
||||
try {
|
||||
if (clientConnectionState != ClientConnectionState.DISCONNECTED)
|
||||
throw new IllegalStateException(clientConnectionState.name());
|
||||
|
||||
setOption("client.hostname", remote.getHostString());
|
||||
setOption("client.password", password);
|
||||
|
||||
socket.connect(remote);
|
||||
|
||||
setClientConnectionState(ClientConnectionState.CONNECTING);
|
||||
joinConnectionState(ClientConnectionState.CONNECTED, timeout);
|
||||
} catch (Throwable e) {
|
||||
setClientConnectionState(ClientConnectionState.DISCONNECTED);
|
||||
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePacket(NetworkPacket packet) throws Exception {
|
||||
if (handler != null) handler.handlePacket(packet);
|
||||
}
|
||||
|
||||
private class PacketReceiver implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
Ts3Logging.debug("Entering packet receiver");
|
||||
|
||||
while (clientConnectionState != ClientConnectionState.DISCONNECTED) {
|
||||
try {
|
||||
handlePacket(socket.receive());
|
||||
} catch (Exception e) {
|
||||
Ts3Logging.debug("Problem handling packet", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ts3Logging.debug("Leaving packet receiver");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.github.manevolent.ts3j.protocol;
|
||||
|
||||
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.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Represents a fully contained network packet, traveling in or out.
|
||||
*/
|
||||
public final class NetworkPacket {
|
||||
private final ProtocolRole role;
|
||||
|
||||
private PacketHeader header;
|
||||
private Packet packet;
|
||||
|
||||
public NetworkPacket(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 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
|
||||
* @param buffer Buffer to write to
|
||||
*/
|
||||
public void write(ByteBuffer buffer) {
|
||||
writeHeader(buffer);
|
||||
writeBody(buffer);
|
||||
}
|
||||
|
||||
public void writeHeader(ByteBuffer buffer) {
|
||||
if (getHeader() == null) throw new NullPointerException("header");
|
||||
|
||||
getHeader().setType(getPacket().getType());
|
||||
|
||||
getHeader().write(buffer);
|
||||
}
|
||||
|
||||
public void writeBody(ByteBuffer buffer) {
|
||||
if (getPacket() == null) throw new NullPointerException("packet");
|
||||
|
||||
getPacket().write(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads data from the specified byte buffer, filling in the packet body as needed.
|
||||
* @param buffer Buffer to read from.
|
||||
*/
|
||||
public void read(ByteBuffer buffer) {
|
||||
readHeader(buffer);
|
||||
readBody(buffer);
|
||||
}
|
||||
|
||||
public void readHeader(ByteBuffer buffer) {
|
||||
if (getHeader() == null) {
|
||||
try {
|
||||
setHeader(getRole().getHeaderClass().newInstance());
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
getHeader().read(buffer);
|
||||
}
|
||||
|
||||
public void 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,20 @@
|
||||
package com.github.manevolent.ts3j.protocol;
|
||||
|
||||
import com.github.manevolent.ts3j.protocol.header.ClientPacketHeader;
|
||||
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
|
||||
import com.github.manevolent.ts3j.protocol.header.ServerPacketHeader;
|
||||
|
||||
public enum ProtocolRole {
|
||||
CLIENT(ClientPacketHeader.class),
|
||||
SERVER(ServerPacketHeader.class);
|
||||
|
||||
private final Class<? extends PacketHeader> headerClass;
|
||||
|
||||
ProtocolRole(Class<? extends PacketHeader> headerClass) {
|
||||
this.headerClass = headerClass;
|
||||
}
|
||||
|
||||
public Class<? extends PacketHeader> getHeaderClass() {
|
||||
return headerClass;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.github.manevolent.ts3j.protocol;
|
||||
|
||||
public enum SocketRole {
|
||||
CLIENT(ProtocolRole.SERVER, ProtocolRole.CLIENT),
|
||||
SERVER(ProtocolRole.CLIENT, ProtocolRole.SERVER);
|
||||
|
||||
private final ProtocolRole in, out;
|
||||
|
||||
SocketRole(ProtocolRole in, ProtocolRole out) {
|
||||
this.in = in;
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
public ProtocolRole getIn() {
|
||||
return in;
|
||||
}
|
||||
|
||||
public ProtocolRole getOut() {
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.github.manevolent.ts3j.protocol;
|
||||
|
||||
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.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketException;
|
||||
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 final class Teamspeak3Socket {
|
||||
private final SocketRole socketRole;
|
||||
|
||||
private final DatagramSocket datagramSocket;
|
||||
private final DatagramPacket datagramPacket = new DatagramPacket(new byte[1500], 1500);
|
||||
|
||||
public Teamspeak3Socket(SocketRole socketRole, DatagramSocket datagramSocket) {
|
||||
this.socketRole = socketRole;
|
||||
this.datagramSocket = datagramSocket;
|
||||
}
|
||||
|
||||
public Teamspeak3Socket(SocketRole socketRole) throws SocketException {
|
||||
this(socketRole, new DatagramSocket());
|
||||
}
|
||||
|
||||
public SocketRole getSocketRole() {
|
||||
return socketRole;
|
||||
}
|
||||
|
||||
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() +
|
||||
"\n" + 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());
|
||||
|
||||
NetworkPacket networkPacket = new NetworkPacket(header.getRole());
|
||||
networkPacket.setHeader(header);
|
||||
networkPacket.setPacket(packet);
|
||||
|
||||
if (networkPacket.getHeader().getMac() == null) {
|
||||
// Generate mac?
|
||||
}
|
||||
|
||||
ByteBuffer outputBuffer = ByteBuffer.allocate(networkPacket.getSize());
|
||||
outputBuffer.order(ByteOrder.BIG_ENDIAN);
|
||||
networkPacket.write(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() + " to " + datagramPacket.getSocketAddress() +
|
||||
"\n" + Ts3Logging.getHex(datagramPacket.getData(), datagramPacket.getLength())
|
||||
);
|
||||
|
||||
return datagramPacket;
|
||||
}
|
||||
|
||||
public NetworkPacket receive() throws IOException {
|
||||
DatagramPacket datagramPacket = receiveNetworkPacket();
|
||||
|
||||
NetworkPacket networkPacket = new NetworkPacket(getSocketRole().getIn());
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(datagramPacket.getData());
|
||||
networkPacket.readHeader(buffer);
|
||||
|
||||
Ts3Logging.debug("[PROTOCOL] READ " + networkPacket.getHeader().getType().name());
|
||||
networkPacket.readBody(buffer);
|
||||
|
||||
return networkPacket;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.github.manevolent.ts3j.protocol.header;
|
||||
|
||||
import com.github.manevolent.ts3j.protocol.packet.PacketType;
|
||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ClientPacketHeader extends PacketHeader {
|
||||
private int clientId;
|
||||
|
||||
public ClientPacketHeader() {
|
||||
super(ProtocolRole.CLIENT);
|
||||
}
|
||||
|
||||
public int getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public void setClientId(int clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeHeader(ByteBuffer buffer) {
|
||||
super.writeHeader(buffer);
|
||||
|
||||
// https://github.com/ReSpeak/tsdeclarations/blob/master/ts3protocol.md
|
||||
|
||||
buffer.putShort((short) (getClientId() & 0x0000FFFF)); // CId / Client Id
|
||||
buffer.put((byte) ((getType().getIndex() & 0x0F) | (getPacketFlags() & 0xF0))); // PT / Packet Type + Flags
|
||||
|
||||
// (payload) -- see writeBody impl in higher levels
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void readHeader(ByteBuffer buffer) {
|
||||
super.readHeader(buffer);
|
||||
|
||||
// https://github.com/ReSpeak/tsdeclarations/blob/master/ts3protocol.md
|
||||
|
||||
setClientId((buffer.getShort() & 0xFFFF));
|
||||
|
||||
byte packetTypeAndFlags = buffer.get();
|
||||
setType(PacketType.fromId((packetTypeAndFlags & 0x0F))); // PT / Packet Type + Flags
|
||||
setPacketFlags(packetTypeAndFlags & 0xF0);
|
||||
|
||||
// (payload) -- see writeBody impl in higher levels
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return super.getSize() + 2 + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.github.manevolent.ts3j.protocol.header;
|
||||
|
||||
public enum HeaderFlag {
|
||||
NONE(0x0),
|
||||
FRAGMENTED(0X10),
|
||||
NEW_PROTOCOL(0x20),
|
||||
COMPRESSED(0x40),
|
||||
UNENCRYPTED(0x80);
|
||||
|
||||
private final int index;
|
||||
|
||||
HeaderFlag(int index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public static HeaderFlag fromId(int index) {
|
||||
for (HeaderFlag value : values())
|
||||
if (value.getIndex() == index) return value;
|
||||
|
||||
throw new IllegalArgumentException("invalid index: " + index);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.github.manevolent.ts3j.protocol.header;
|
||||
|
||||
import com.github.manevolent.ts3j.protocol.packet.PacketType;
|
||||
import com.github.manevolent.ts3j.protocol.packet.Packet;
|
||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public abstract class PacketHeader {
|
||||
private final ProtocolRole role;
|
||||
|
||||
//private final byte[] DEFAULT_MAC = new byte[] {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}; // 8x0's
|
||||
private byte[] mac = new byte[8]; // 8 bytes
|
||||
private int packetId = 0;
|
||||
private PacketType type;
|
||||
private int packetFlags;
|
||||
|
||||
protected PacketHeader(ProtocolRole role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public int getPacketFlags() {
|
||||
return packetFlags;
|
||||
}
|
||||
|
||||
public void setPacketFlags(int packetFlags) {
|
||||
this.packetFlags = packetFlags;
|
||||
}
|
||||
|
||||
public boolean getPacketFlag(HeaderFlag flag) {
|
||||
int idx = flag.getIndex();
|
||||
|
||||
return (packetFlags & idx) == idx;
|
||||
}
|
||||
|
||||
public void setPacketFlag(HeaderFlag flag, boolean value) {
|
||||
int idx = flag.getIndex();
|
||||
|
||||
if (value) {
|
||||
packetFlags = packetFlags | idx;
|
||||
} else {
|
||||
packetFlags &= ~idx;
|
||||
}
|
||||
}
|
||||
|
||||
public PacketType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(PacketType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getPacketId() {
|
||||
return packetId;
|
||||
}
|
||||
|
||||
public void setPacketId(int packetId) {
|
||||
this.packetId = packetId;
|
||||
}
|
||||
|
||||
public byte[] getMac() {
|
||||
return mac;
|
||||
}
|
||||
|
||||
public void setMac(byte[] mac) {
|
||||
Packet.assertArray("mac", mac, 8);
|
||||
|
||||
this.mac = mac;
|
||||
}
|
||||
|
||||
public final void write(ByteBuffer buffer) {
|
||||
writeHeader(buffer);
|
||||
}
|
||||
|
||||
public final void read(ByteBuffer buffer) {
|
||||
readHeader(buffer);
|
||||
}
|
||||
|
||||
protected void writeHeader(ByteBuffer buffer) {
|
||||
// https://github.com/ReSpeak/tsdeclarations/blob/master/ts3protocol.md
|
||||
buffer.put(getMac(), 0, 8);
|
||||
buffer.putShort((short) (getPacketId() & 0x0000FFFF)); // PiD / Packet ID
|
||||
}
|
||||
|
||||
protected void readHeader(ByteBuffer buffer) {
|
||||
// https://github.com/ReSpeak/tsdeclarations/blob/master/ts3protocol.md
|
||||
buffer.get(mac);
|
||||
setPacketId((int) buffer.getShort() & 0x0000FFFF);
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return 8 + 2;
|
||||
}
|
||||
|
||||
public ProtocolRole getRole() {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.github.manevolent.ts3j.protocol.header;
|
||||
|
||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
||||
import com.github.manevolent.ts3j.protocol.packet.PacketType;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ServerPacketHeader extends PacketHeader {
|
||||
private int packetFlags;
|
||||
|
||||
public ServerPacketHeader() {
|
||||
super(ProtocolRole.SERVER);
|
||||
}
|
||||
|
||||
public int getPacketFlags() {
|
||||
return packetFlags;
|
||||
}
|
||||
|
||||
public void setPacketFlags(int packetFlags) {
|
||||
this.packetFlags = packetFlags;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeHeader(ByteBuffer buffer) {
|
||||
super.writeHeader(buffer);
|
||||
|
||||
// https://github.com/ReSpeak/tsdeclarations/blob/master/ts3protocol.md
|
||||
|
||||
buffer.put((byte) ((getType().getIndex() & 0x0F) | (getPacketFlags() & 0xF0))); // PT / Packet Type + Flags
|
||||
|
||||
// (payload) -- see writeBody impl in higher levels
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readHeader(ByteBuffer buffer) {
|
||||
super.readHeader(buffer);
|
||||
|
||||
// https://github.com/ReSpeak/tsdeclarations/blob/master/ts3protocol.md
|
||||
byte packetTypeAndFlags = buffer.get();
|
||||
setType(PacketType.fromId((packetTypeAndFlags & 0x0F))); // PT / Packet Type + Flags
|
||||
setPacketFlags(packetTypeAndFlags & 0xF0);
|
||||
// (payload) -- see writeBody impl in higher levels
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return super.getSize() + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.github.manevolent.ts3j.protocol.packet;
|
||||
|
||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
||||
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public abstract class Packet {
|
||||
private final PacketType type;
|
||||
private final ProtocolRole role;
|
||||
|
||||
protected Packet(PacketType type, ProtocolRole role) {
|
||||
this.type = type;
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public PacketType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public ProtocolRole getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public abstract void read(ByteBuffer buffer);
|
||||
|
||||
public abstract void write(ByteBuffer buffer);
|
||||
|
||||
public abstract int getSize();
|
||||
|
||||
public void setHeaderValues(PacketHeader header) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that an array length is correct
|
||||
* @param field Field name being asserted
|
||||
* @param bytes Bytes being set
|
||||
* @param length Length of the byte array expected
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public static final void assertArray(String field, byte[] bytes, int length) throws IllegalArgumentException {
|
||||
if (bytes == null)
|
||||
throw new NullPointerException(field + " attempted set to null pointer");
|
||||
|
||||
if (bytes.length != length)
|
||||
throw new IllegalArgumentException(field + " len " + bytes.length + " != " + length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
package com.github.manevolent.ts3j.protocol.packet;
|
||||
|
||||
import com.github.manevolent.ts3j.protocol.header.HeaderFlag;
|
||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
||||
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import static com.github.manevolent.ts3j.protocol.packet.PacketType.INIT1;
|
||||
|
||||
public class Packet8Init1 extends Packet {
|
||||
private static final byte[] LOW_LEVEL_MAC = new byte[] {
|
||||
0x54, 0x53, 0x33, 0x49, 0x4E, 0x49, 0x54, 0x31
|
||||
};
|
||||
|
||||
private byte[] version = new byte[4]; // 4 bytes
|
||||
private Step step; // step instance
|
||||
|
||||
public Packet8Init1(ProtocolRole role) {
|
||||
// sec. 2 of https://github.com/ReSpeak/tsdeclarations/blob/master/ts3protocol.md
|
||||
// (The packet header values are set as following for all packets here:)
|
||||
super(INIT1, role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHeaderValues(PacketHeader header) {
|
||||
header.setMac(LOW_LEVEL_MAC);
|
||||
header.setPacketFlag(HeaderFlag.UNENCRYPTED, true);
|
||||
header.setPacketId((short)101);
|
||||
}
|
||||
|
||||
public byte[] getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(byte[] version) {
|
||||
assertArray("version", version, 4);
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public Step getStep() {
|
||||
return step;
|
||||
}
|
||||
|
||||
public void setStep(Step step) {
|
||||
if (step == null)
|
||||
throw new NullPointerException("step");
|
||||
|
||||
if (step.getRole() != getRole())
|
||||
throw new IllegalArgumentException(
|
||||
"Init1 step mismatches expected role: " + step.getRole().name() +
|
||||
" != " + getRole().name()
|
||||
);
|
||||
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ByteBuffer buffer) {
|
||||
// only the client sends the version
|
||||
if (getRole() == ProtocolRole.CLIENT)
|
||||
buffer.put(getVersion());
|
||||
|
||||
buffer.put(getStep().getNumber());
|
||||
|
||||
step.write(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(ByteBuffer buffer) {
|
||||
// only the client sends a version
|
||||
if (getRole() == ProtocolRole.CLIENT)
|
||||
buffer.get(version);
|
||||
|
||||
byte stepNumber = buffer.get();
|
||||
|
||||
Step step;
|
||||
|
||||
switch (stepNumber) {
|
||||
case 0:
|
||||
step = new Step0();
|
||||
break;
|
||||
case 1:
|
||||
step = new Step1();
|
||||
break;
|
||||
case 2:
|
||||
step = new Step2();
|
||||
break;
|
||||
case 3:
|
||||
step = new Step3();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("invalid Init1 step: " + stepNumber);
|
||||
}
|
||||
|
||||
step.read(buffer);
|
||||
|
||||
setStep(step);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return 4 + 1 + (step == null ? 0 : step.getSize());
|
||||
}
|
||||
|
||||
public static abstract class Step {
|
||||
private final byte stepNumber;
|
||||
private final ProtocolRole role;
|
||||
|
||||
protected Step(byte stepNumber, ProtocolRole role) {
|
||||
this.stepNumber = stepNumber;
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public byte getNumber() {
|
||||
return stepNumber;
|
||||
}
|
||||
|
||||
public ProtocolRole getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public abstract void read(ByteBuffer byteBuffer);
|
||||
public abstract void write(ByteBuffer byteBuffer);
|
||||
public abstract int getSize();
|
||||
}
|
||||
|
||||
public static class Step0 extends Step {
|
||||
private byte[] timestamp = new byte[4]; // 4 bytes
|
||||
private byte[] random = new byte[4]; // 4 bytes
|
||||
private final byte[] reserved = new byte[8]; // 8 zeros, these are reserved for future use
|
||||
|
||||
public Step0() {
|
||||
super((byte) 0, ProtocolRole.CLIENT);
|
||||
}
|
||||
|
||||
public byte[] getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(int timestamp) {
|
||||
ByteBuffer intBuffer = ByteBuffer.allocate(4);
|
||||
|
||||
intBuffer.order(ByteOrder.BIG_ENDIAN);
|
||||
intBuffer.putInt(timestamp);
|
||||
|
||||
setTimestamp(intBuffer.array());
|
||||
}
|
||||
|
||||
public void setTimestamp(byte[] timestamp) {
|
||||
Packet.assertArray("timestamp", timestamp, 4);
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public byte[] getRandom() {
|
||||
return random;
|
||||
}
|
||||
|
||||
public void setRandom(byte[] random) {
|
||||
Packet.assertArray("random", random, 4);
|
||||
this.random = random;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(ByteBuffer byteBuffer) {
|
||||
byteBuffer.get(timestamp);
|
||||
byteBuffer.get(random);
|
||||
byteBuffer.get(reserved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ByteBuffer byteBuffer) {
|
||||
byteBuffer.put(timestamp);
|
||||
byteBuffer.put(random);
|
||||
byteBuffer.put(reserved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return 4 + 4 + 8;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Step1 extends Step {
|
||||
private byte[] serverStuff = new byte[16]; // 16 bytes
|
||||
private byte[] a0reversed = new byte[4]; // 4 bytes / The bytes from [A0] in reversed order
|
||||
|
||||
public Step1() {
|
||||
super((byte) 1, ProtocolRole.SERVER);
|
||||
}
|
||||
|
||||
|
||||
public byte[] getA0reversed() {
|
||||
return a0reversed;
|
||||
}
|
||||
|
||||
public void setA0reversed(byte[] a0reversed) {
|
||||
Packet.assertArray("a0reversed", a0reversed, 4);
|
||||
this.a0reversed = a0reversed;
|
||||
}
|
||||
|
||||
public byte[] getServerStuff() {
|
||||
return serverStuff;
|
||||
}
|
||||
|
||||
public void setServerStuff(byte[] serverStuff) {
|
||||
Packet.assertArray("serverStuff", serverStuff, 16);
|
||||
this.serverStuff = serverStuff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(ByteBuffer byteBuffer) {
|
||||
byteBuffer.get(serverStuff);
|
||||
byteBuffer.get(a0reversed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ByteBuffer byteBuffer) {
|
||||
byteBuffer.put(serverStuff);
|
||||
byteBuffer.put(a0reversed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return 16 + 4;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Step2 extends Step {
|
||||
private byte[] serverStuff = new byte[16]; // 16 bytes
|
||||
private byte[] a0reversed = new byte[4]; // 4 bytes / The bytes from [A0] in reversed order
|
||||
|
||||
public Step2() {
|
||||
super((byte) 2, ProtocolRole.CLIENT);
|
||||
}
|
||||
|
||||
|
||||
public byte[] getA0reversed() {
|
||||
return a0reversed;
|
||||
}
|
||||
|
||||
public void setA0reversed(byte[] a0reversed) {
|
||||
Packet.assertArray("a0reversed", a0reversed, 4);
|
||||
this.a0reversed = a0reversed;
|
||||
}
|
||||
|
||||
public byte[] getServerStuff() {
|
||||
return serverStuff;
|
||||
}
|
||||
|
||||
public void setServerStuff(byte[] serverStuff) {
|
||||
Packet.assertArray("serverStuff", serverStuff, 16);
|
||||
this.serverStuff = serverStuff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(ByteBuffer byteBuffer) {
|
||||
byteBuffer.get(serverStuff);
|
||||
byteBuffer.get(a0reversed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ByteBuffer byteBuffer) {
|
||||
byteBuffer.put(serverStuff);
|
||||
byteBuffer.put(a0reversed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return 16 + 4;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Step3 extends Step {
|
||||
private byte[] x = new byte[64]; // x
|
||||
private byte[] n = new byte[64]; // n
|
||||
private int level;
|
||||
private byte[] serverStuff = new byte[100]; // must be a lot of really important server stuff lol
|
||||
|
||||
public Step3() {
|
||||
super((byte) 3, ProtocolRole.SERVER);
|
||||
}
|
||||
|
||||
public byte[] getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public void setX(byte[] x) {
|
||||
Packet.assertArray("x", x, 64);
|
||||
this.x = x;
|
||||
}
|
||||
|
||||
public byte[] getN() {
|
||||
return n;
|
||||
}
|
||||
|
||||
public void setN(byte[] n) {
|
||||
Packet.assertArray("n", n, 64);
|
||||
this.n = n;
|
||||
}
|
||||
|
||||
public byte[] getServerStuff() {
|
||||
return serverStuff;
|
||||
}
|
||||
|
||||
public void setServerStuff(byte[] serverStuff) {
|
||||
Packet.assertArray("serverStuff", serverStuff, 100);
|
||||
this.serverStuff = serverStuff;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public void setLevel(int level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(ByteBuffer byteBuffer) {
|
||||
byteBuffer.get(x);
|
||||
byteBuffer.get(n);
|
||||
level = byteBuffer.getInt();
|
||||
byteBuffer.get(serverStuff);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ByteBuffer byteBuffer) {
|
||||
byteBuffer.put(x);
|
||||
byteBuffer.put(n);
|
||||
byteBuffer.putInt(level);
|
||||
byteBuffer.put(serverStuff);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return 64 + 64 + 4 + 100;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Step4 extends Step {
|
||||
private byte[] x = new byte[64]; // x
|
||||
private byte[] n = new byte[64]; // n
|
||||
private int level;
|
||||
private byte[] serverStuff = new byte[100]; // a2 echo from step 3
|
||||
private byte[] y = new byte[64]; // y 0-paddded from the left
|
||||
private byte[] clientivcommand = new byte[0]; // clientivcommand
|
||||
|
||||
public Step4() {
|
||||
super((byte) 4, ProtocolRole.CLIENT);
|
||||
}
|
||||
|
||||
public byte[] getX() {
|
||||
return serverStuff;
|
||||
}
|
||||
|
||||
public void setX(byte[] x) {
|
||||
Packet.assertArray("x", x, 64);
|
||||
this.x = x;
|
||||
}
|
||||
|
||||
public byte[] getN() {
|
||||
return serverStuff;
|
||||
}
|
||||
|
||||
public void setN(byte[] n) {
|
||||
Packet.assertArray("n", n, 64);
|
||||
this.n = n;
|
||||
}
|
||||
|
||||
public byte[] getServerStuff() {
|
||||
return serverStuff;
|
||||
}
|
||||
|
||||
public void setServerStuff(byte[] serverStuff) {
|
||||
Packet.assertArray("serverStuff", serverStuff, 100);
|
||||
this.serverStuff = serverStuff;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public void setLevel(int level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public byte[] getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public void setY(byte[] y) {
|
||||
Packet.assertArray("y", y, 64);
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public byte[] getClientIVCommand() {
|
||||
return clientivcommand;
|
||||
}
|
||||
|
||||
public void setClientIVcommand(byte[] clientivcommand) {
|
||||
this.clientivcommand = clientivcommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(ByteBuffer byteBuffer) {
|
||||
byteBuffer.get(x);
|
||||
byteBuffer.get(n);
|
||||
level = byteBuffer.getInt();
|
||||
byteBuffer.get(serverStuff);
|
||||
byteBuffer.get(y);
|
||||
|
||||
clientivcommand = new byte[byteBuffer.remaining()];
|
||||
byteBuffer.get(clientivcommand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ByteBuffer byteBuffer) {
|
||||
byteBuffer.put(x);
|
||||
byteBuffer.put(n);
|
||||
byteBuffer.putInt(level);
|
||||
byteBuffer.put(serverStuff);
|
||||
byteBuffer.put(y);
|
||||
|
||||
byteBuffer.put(clientivcommand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return 64 + 64 + 4 + 100 + 64 + clientivcommand.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.github.manevolent.ts3j.protocol.packet;
|
||||
|
||||
public enum PacketType {
|
||||
VOICE(0x0, null),
|
||||
VOICE_WHISPER(0x1, null),
|
||||
COMMAND(0x2, null),
|
||||
COMMAND_LOW(0x3, null),
|
||||
PING(0x4, null),
|
||||
PONG(0x5, null),
|
||||
ACK(0x6, null),
|
||||
ACK_LOW(0x7, null),
|
||||
INIT1(0x8, Packet8Init1.class)
|
||||
;
|
||||
|
||||
private final int index;
|
||||
private final Class<? extends Packet> packetClass;
|
||||
|
||||
PacketType(int index, Class<? extends Packet> packetClass) {
|
||||
this.index = index;
|
||||
this.packetClass = packetClass;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user