diff --git a/src/main/java/com/github/manevolent/ts3j/ClientConnectionState.java b/src/main/java/com/github/manevolent/ts3j/ClientConnectionState.java new file mode 100644 index 0000000..a355cb9 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/ClientConnectionState.java @@ -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 clazz; + + ClientConnectionState(Class clazz) { + this.clazz = clazz; + } + + public Class getHandlerClass() { + return clazz; + } + + public TeamspeakClientHandler createHandler(Teamspeak3Client client) { + try { + return clazz.getConstructor(Teamspeak3Client.class).newInstance(client); + } catch (ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/manevolent/ts3j/Teamspeak3Client.java b/src/main/java/com/github/manevolent/ts3j/Teamspeak3Client.java new file mode 100644 index 0000000..4126d13 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/Teamspeak3Client.java @@ -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 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 getOption(String key, Class 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"); + } + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/NetworkPacket.java b/src/main/java/com/github/manevolent/ts3j/protocol/NetworkPacket.java new file mode 100644 index 0000000..9736f63 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/NetworkPacket.java @@ -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(); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/ProtocolRole.java b/src/main/java/com/github/manevolent/ts3j/protocol/ProtocolRole.java new file mode 100644 index 0000000..ab60850 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/ProtocolRole.java @@ -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 headerClass; + + ProtocolRole(Class headerClass) { + this.headerClass = headerClass; + } + + public Class getHeaderClass() { + return headerClass; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/SocketRole.java b/src/main/java/com/github/manevolent/ts3j/protocol/SocketRole.java new file mode 100644 index 0000000..438f33b --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/SocketRole.java @@ -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; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/Teamspeak3Socket.java b/src/main/java/com/github/manevolent/ts3j/protocol/Teamspeak3Socket.java new file mode 100644 index 0000000..194f458 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/Teamspeak3Socket.java @@ -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; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/header/ClientPacketHeader.java b/src/main/java/com/github/manevolent/ts3j/protocol/header/ClientPacketHeader.java new file mode 100644 index 0000000..0b940c0 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/header/ClientPacketHeader.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/header/HeaderFlag.java b/src/main/java/com/github/manevolent/ts3j/protocol/header/HeaderFlag.java new file mode 100644 index 0000000..d40b53b --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/header/HeaderFlag.java @@ -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); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/header/PacketHeader.java b/src/main/java/com/github/manevolent/ts3j/protocol/header/PacketHeader.java new file mode 100644 index 0000000..710db54 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/header/PacketHeader.java @@ -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; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/header/ServerPacketHeader.java b/src/main/java/com/github/manevolent/ts3j/protocol/header/ServerPacketHeader.java new file mode 100644 index 0000000..d6b5d3e --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/header/ServerPacketHeader.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet.java new file mode 100644 index 0000000..82473cd --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet.java @@ -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); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet8Init1.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet8Init1.java new file mode 100644 index 0000000..2a18fe3 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet8Init1.java @@ -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; + } + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketType.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketType.java new file mode 100644 index 0000000..1b45b86 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketType.java @@ -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 packetClass; + + PacketType(int index, Class 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 getPacketClass() { + return packetClass; + } +}