diff --git a/src/main/java/com/github/manevolent/ts3j/TeamspeakClient.java b/src/main/java/com/github/manevolent/ts3j/TeamspeakClient.java deleted file mode 100644 index f59237f..0000000 --- a/src/main/java/com/github/manevolent/ts3j/TeamspeakClient.java +++ /dev/null @@ -1,300 +0,0 @@ -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.enums.ConnectionState; -import com.github.manevolent.ts3j.enums.PacketFlags; -import com.github.manevolent.ts3j.enums.PacketType; -import com.github.manevolent.ts3j.handler.TeamspeakClientHandler; -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.util.Date; -import java.util.concurrent.TimeoutException; - -public class TeamspeakClient { - private final PacketReceiver packetReceiver = new PacketReceiver(); - - private final DatagramSocket datagramSocket; - private final DatagramPacket datagramPacket = new DatagramPacket(new byte[1500], 1500); - - private final Object connectionStateLock = new Object(); - - private TeamspeakClientHandler handler; - - private InetSocketAddress remote = null; - private ConnectionState connectionState = null; - private int returnCodeIndex = 0; - - public TeamspeakClient(DatagramSocket datagramSocket) { - this.datagramSocket = datagramSocket; - } - - public TeamspeakClient() throws SocketException { - this(new DatagramSocket()); - - setConnectionState(ConnectionState.DISCONNECTED); - } - - protected void setConnectionState(ConnectionState state) { - synchronized (connectionStateLock) { - if (state != this.connectionState) { - boolean wasDisconnected = this.connectionState == ConnectionState.DISCONNECTED; - - this.connectionState = 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); - } - } - - connectionStateLock.notifyAll(); - } - } - } - - public boolean isConnected() { - return connectionState == ConnectionState.CONNECTED; - } - - public ConnectionState getConnectionState() { - return connectionState; - } - - private void joinConnectionState(ConnectionState state, long wait) - throws InterruptedException, TimeoutException { - synchronized (connectionStateLock) { - if (connectionState != state) { - connectionStateLock.wait(wait); - - if (connectionState != state) - throw new TimeoutException("timeout waiting for " + state.name() + " state"); - } - } - } - - 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 (connectionState != ConnectionState.DISCONNECTED) - throw new IllegalStateException(connectionState.name()); - - datagramSocket.connect(remote); - - setConnectionState(ConnectionState.CONNECTING); - joinConnectionState(ConnectionState.CONNECTED, timeout); - } catch (Throwable e) { - setConnectionState(ConnectionState.DISCONNECTED); - - throw new IOException(e); - } - } - - public void send(PacketType type, byte[] data) throws IOException { - C2SPacket packet = createOutgoingPacket(type, data); - } - - private C2SPacket createOutgoingPacket(PacketType type, byte[] data) throws IOException { - C2SPacket packet = new C2SPacket(); - - //TODO - packet.setPacketId(0); - - //TODO - packet.setGenerationId(0); - - //TODO - packet.setClientId(0); - - //TODO - packet.packetFlags |= 0; - - switch (type) { - case VOICE: - case VOICE_WHISPER: - - break; - case COMMAND: - case COMMAND_LOW: - - break; - case PING: - - break; - case PONG: - - break; - case ACK: - case ACK_LOW: - - break; - case INIT: - packet.setPacketFlags(packet.getPacketFlags() | PacketFlags.UNENCRYPTED.getIndex()); - break; - } - - return packet; - } - - /** - * Wire packet write - * @param packet Packet to write - * @throws IOException - */ - private void sendNetworkPacket(DatagramPacket packet) throws IOException { - Ts3Logging.debug( - "[NETWORK] WRITE Len=" + packet.getLength() + " to " + datagramSocket.getRemoteSocketAddress() + - "\n" + Ts3Logging.getHex(packet.getData()) - ); - - packet.setSocketAddress(datagramSocket.getRemoteSocketAddress()); - - datagramSocket.send(packet); - } - - private DatagramPacket receiveNetworkPacket() throws IOException { - datagramSocket.receive(datagramPacket); - return datagramPacket; - } - - private void handleNetworkPacket(DatagramPacket packet) { - Ts3Logging.debug( - "[NETWORK] READ Len=" + packet.getLength() + " from " + packet.getSocketAddress() + - "\n" + Ts3Logging.getHex(packet.getData()) - ); - - if (handler != null) handler.handleNetworkPacket(packet); - } - - public InetSocketAddress getRemote() { - return remote; - } - - private class PacketReceiver implements Runnable { - @Override - public void run() { - while (connectionState != ConnectionState.DISCONNECTED) { - try { - handleNetworkPacket(receiveNetworkPacket()); - } catch (IOException e) { - Ts3Logging.info("Problem handling packet: " + e.getMessage()); - } - } - - Ts3Logging.debug("Leaving packet receiver"); - } - } - - private class C2SPacket { - private int packetId; - private int generationId; - private int clientId; - private int packetFlags; - - private PacketType type; - private byte[] data; - private Date lastSendTime; - - public PacketType getType() { - return type; - } - - public void setType(PacketType type) { - this.type = type; - } - - public byte[] getData() { - return data; - } - - public void setData(byte[] data) { - this.data = data; - } - - public Date getLastSendTime() { - return lastSendTime; - } - - public int getPacketId() { - return packetId; - } - - public void setPacketId(int packetId) { - this.packetId = packetId; - } - - public int getGenerationId() { - return generationId; - } - - public void setGenerationId(int generationId) { - this.generationId = generationId; - } - - public int getClientId() { - return clientId; - } - - public void setClientId(int clientId) { - this.clientId = clientId; - } - - public int getPacketFlags() { - return packetFlags; - } - - public void setPacketFlags(int packetFlags) { - this.packetFlags = packetFlags; - } - - /** - * Sends, or re-sends, a packet - */ - public void send() { - - } - } -} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/ConnectionState.java b/src/main/java/com/github/manevolent/ts3j/enums/ConnectionState.java deleted file mode 100644 index 05390ea..0000000 --- a/src/main/java/com/github/manevolent/ts3j/enums/ConnectionState.java +++ /dev/null @@ -1,30 +0,0 @@ - -package com.github.manevolent.ts3j.enums; - -import com.github.manevolent.ts3j.TeamspeakClient; -import com.github.manevolent.ts3j.handler.*; - -public enum ConnectionState { - CONNECTING(TeamspeakClientHandlerConnecting.class), - NEGOTIATING(TeamspeakClientHandlerNegotiating.class), - CONNECTED(TeamspeakClientHandlerConnected.class), - DISCONNECTED(TeamspeakClientHandlerDisconnected.class); - - private final Class clazz; - - ConnectionState(Class clazz) { - this.clazz = clazz; - } - - public Class getHandlerClass() { - return clazz; - } - - public TeamspeakClientHandler createHandler(TeamspeakClient client) { - try { - return clazz.getConstructor(TeamspeakClient.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/enums/PacketFlags.java b/src/main/java/com/github/manevolent/ts3j/enums/PacketFlags.java deleted file mode 100644 index 92c3c90..0000000 --- a/src/main/java/com/github/manevolent/ts3j/enums/PacketFlags.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.manevolent.ts3j.enums; - -public enum PacketFlags { - NONE(0x0), - FRAGMENTED(0X10), - NEW_PROTOCOL(0x20), - COMPRESSED(0x40), - UNENCRYPTED(0x80); - - private final int index; - - PacketFlags(int index) { - this.index = index; - } - - public int getIndex() { - return index; - } - - public static PacketFlags fromId(int index) { - for (PacketFlags value : values()) - if (value.getIndex() == index) return value; - - throw new IllegalArgumentException("invalid index: " + index); - } -} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/PacketType.java b/src/main/java/com/github/manevolent/ts3j/enums/PacketType.java deleted file mode 100644 index 23fe063..0000000 --- a/src/main/java/com/github/manevolent/ts3j/enums/PacketType.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.github.manevolent.ts3j.enums; - -public enum PacketType { - VOICE(0x0), - VOICE_WHISPER(0x1), - COMMAND(0x2), - COMMAND_LOW(0x3), - PING(0x4), - PONG(0x5), - ACK(0x6), - ACK_LOW(0x7), - INIT(0x8) - ; - - private final int index; - - PacketType(int index) { - this.index = index; - } - - 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); - } -} diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandler.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandler.java index 73697a7..e6829c0 100644 --- a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandler.java +++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandler.java @@ -1,23 +1,23 @@ package com.github.manevolent.ts3j.handler; -import com.github.manevolent.ts3j.TeamspeakClient; -import com.github.manevolent.ts3j.enums.ConnectionState; +import com.github.manevolent.ts3j.Teamspeak3Client; +import com.github.manevolent.ts3j.ClientConnectionState; +import com.github.manevolent.ts3j.protocol.NetworkPacket; import java.io.IOException; -import java.net.DatagramPacket; public abstract class TeamspeakClientHandler { - private final TeamspeakClient client; + private final Teamspeak3Client client; - protected TeamspeakClientHandler(TeamspeakClient client) { + protected TeamspeakClientHandler(Teamspeak3Client client) { this.client = client; } - public void handleConnectionStateChanging(ConnectionState connectionState) { - + protected final Teamspeak3Client getClient() { + return client; } - public void handleNetworkPacket(DatagramPacket packet) { + public void handleConnectionStateChanging(ClientConnectionState clientConnectionState) { } @@ -25,7 +25,7 @@ public abstract class TeamspeakClientHandler { } - protected TeamspeakClient getClient() { - return client; + public void handlePacket(NetworkPacket packet) throws IOException { + } } diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnected.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnected.java index 64ae81d..7b4fa1c 100644 --- a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnected.java +++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnected.java @@ -1,9 +1,9 @@ package com.github.manevolent.ts3j.handler; -import com.github.manevolent.ts3j.TeamspeakClient; +import com.github.manevolent.ts3j.Teamspeak3Client; public class TeamspeakClientHandlerConnected extends TeamspeakClientHandler { - public TeamspeakClientHandlerConnected(TeamspeakClient client) { + public TeamspeakClientHandlerConnected(Teamspeak3Client client) { super(client); } } diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnecting.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnecting.java index fd62e6a..c2d0059 100644 --- a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnecting.java +++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnecting.java @@ -1,80 +1,121 @@ package com.github.manevolent.ts3j.handler; -import com.github.manevolent.ts3j.TeamspeakClient; -import com.github.manevolent.ts3j.enums.PacketType; +import com.github.manevolent.ts3j.Teamspeak3Client; +import com.github.manevolent.ts3j.protocol.NetworkPacket; +import com.github.manevolent.ts3j.protocol.ProtocolRole; +import com.github.manevolent.ts3j.protocol.packet.Packet8Init1; import com.github.manevolent.ts3j.util.Ts3Logging; import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; +import java.math.BigInteger; import java.util.Random; public class TeamspeakClientHandlerConnecting extends TeamspeakClientHandler { + private byte[] randomBytes; - - public TeamspeakClientHandlerConnecting(TeamspeakClient client) { + public TeamspeakClientHandlerConnecting(Teamspeak3Client client) { super(client); } @Override public void onAssigned() throws IOException { - Ts3Logging.debug("Connecting: sending Init1..."); + Packet8Init1 packet = new Packet8Init1(ProtocolRole.CLIENT); - processInit1(); + Packet8Init1.Step0 step = new Packet8Init1.Step0(); + Random random = new Random(); + randomBytes = new byte[4]; + random.nextBytes(randomBytes); + step.setRandom(randomBytes); + step.setTimestamp((int) ((System.currentTimeMillis() / 1000L) & 0x000000FFFFFF)); + packet.setStep(step); + + sendInit1(packet); } - /** - * Processes the first client dispatch of the Init1 packet (C2S initial handshake packet) - */ - public void processInit1() throws IOException { - processInit1(null); + private void sendInit1(Packet8Init1 packet) throws IOException { + Ts3Logging.debug("Connecting: sending " + packet.getClass().getSimpleName() + + ":" + + packet.getStep().getClass().getSimpleName() + "..." + ); + + packet.setVersion(new byte[] { 0x09, (byte)0x83, (byte)0x8C, (byte)0xCF }); + + getClient().send(packet); } - public void processInit1(byte[] data) throws IOException { - int versionLen = 4; - int initTypeLen = 1; - Integer type = null; + @Override + public void handlePacket(NetworkPacket packet) throws IOException { + Ts3Logging.debug("Connecting: handling " + packet.getClass().getSimpleName()); - if (data != null) { - type = (int) data[0]; + if (packet.getPacket() instanceof Packet8Init1) { + Packet8Init1 init1 = (Packet8Init1) packet.getPacket(); - if (data.length < initTypeLen) - throw new IllegalArgumentException("invalid Init1 packet (too short)"); - } - - ByteBuffer buffer = null; - - if (type == null) { - buffer = ByteBuffer.allocate(versionLen + initTypeLen + 4 + 4 + 8); - - buffer.put(new byte[] { 0x09, (byte)0x83, (byte)0x8C, (byte)0xCF }); - buffer.put((byte) 0x00); // initType - buffer.order(ByteOrder.BIG_ENDIAN); - buffer.putInt((int) (System.currentTimeMillis() / 1000L)); - - Random random = new Random(); - for (int i = 0; i < 4; i++) - buffer.put((byte) random.nextInt(256)); // 4byte random - } else { - switch (type) { + switch (init1.getStep().getNumber()) { case 1: + Packet8Init1.Step1 serverReplyStep1 = (Packet8Init1.Step1)init1.getStep(); + // Check nonce. It's received backwards, so walk backwards over the array received + for (int i = 0; i < 4; i ++) { + if (randomBytes[3 - i] != serverReplyStep1.getA0reversed()[i]) + throw new IllegalArgumentException("invalid server nonce"); + } + // Build response + Packet8Init1 response2 = new Packet8Init1(ProtocolRole.CLIENT); + + Packet8Init1.Step2 step2 = new Packet8Init1.Step2(); + step2.setA0reversed(serverReplyStep1.getA0reversed()); + step2.setServerStuff(serverReplyStep1.getServerStuff()); + response2.setStep(step2); + + sendInit1(response2); break; case 3: + Packet8Init1.Step3 serverReplyStep3 = (Packet8Init1.Step3)init1.getStep(); - break; - case 0x7F: - // 0x7F: Some strange servers do this - // the normal client responds by starting again + // Calculate 'y' + // which is the result of x ^ (2 ^ level) % n as an unsigned + // BigInteger. Padded from the lower side with '0x00' when shorter + // than 64 bytes. + // CITE: https://github.com/Splamy/TS3AudioBot/blob/master/TS3Client/Full/Ts3Crypt.cs + // Prepare solution + if (serverReplyStep3.getLevel() < 0 || serverReplyStep3.getLevel() > 1_000_000) + throw new IllegalArgumentException("RSA challange level is not within an acceptable range"); + + BigInteger x = new BigInteger(1, serverReplyStep3.getX()); + BigInteger n = new BigInteger(1, serverReplyStep3.getN()); + + byte[] y = new byte[64]; + + byte[] solution = + x.modPow(BigInteger.valueOf(2L).pow(serverReplyStep3.getLevel()), n).toByteArray(); + + System.arraycopy( + solution, Math.max(0, solution.length - 64), + y, 64 - solution.length, + solution.length + ); + + // Build response + Packet8Init1 response4 = new Packet8Init1(ProtocolRole.CLIENT); + + Packet8Init1.Step4 step4 = new Packet8Init1.Step4(); + + step4.setLevel(serverReplyStep3.getLevel()); + step4.setX(serverReplyStep3.getX()); + step4.setN(serverReplyStep3.getN()); + step4.setY(y); + step4.setServerStuff(serverReplyStep3.getServerStuff()); + + step4.setClientIVcommand(new byte[4]); // TODO + + response4.setStep(step4); + + sendInit1(response4); break; default: - throw new IllegalArgumentException("invalid Init1 packet id: " + type); + throw new IllegalArgumentException("unexpected Init1 server step: " + init1.getStep().getNumber()); + } } - - if (buffer == null || buffer.position() <= 0) - throw new IllegalStateException("invalid send buffer"); - - getClient().send(PacketType.INIT, buffer.array()); } } diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerDisconnected.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerDisconnected.java index 25549d2..021f7e7 100644 --- a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerDisconnected.java +++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerDisconnected.java @@ -1,9 +1,9 @@ package com.github.manevolent.ts3j.handler; -import com.github.manevolent.ts3j.TeamspeakClient; +import com.github.manevolent.ts3j.Teamspeak3Client; public class TeamspeakClientHandlerDisconnected extends TeamspeakClientHandler { - public TeamspeakClientHandlerDisconnected(TeamspeakClient client) { + public TeamspeakClientHandlerDisconnected(Teamspeak3Client client) { super(client); } } diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerNegotiating.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerNegotiating.java deleted file mode 100644 index 0b9cb38..0000000 --- a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerNegotiating.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.manevolent.ts3j.handler; - -import com.github.manevolent.ts3j.TeamspeakClient; - -public class TeamspeakClientHandlerNegotiating extends TeamspeakClientHandler { - public TeamspeakClientHandlerNegotiating(TeamspeakClient client) { - super(client); - } -} diff --git a/src/main/java/com/github/manevolent/ts3j/util/Ts3Logging.java b/src/main/java/com/github/manevolent/ts3j/util/Ts3Logging.java index 09d05c7..7e7fe38 100644 --- a/src/main/java/com/github/manevolent/ts3j/util/Ts3Logging.java +++ b/src/main/java/com/github/manevolent/ts3j/util/Ts3Logging.java @@ -3,8 +3,12 @@ package com.github.manevolent.ts3j.util; public final class Ts3Logging { private static final String HEXES = "0123456789ABCDEF"; public static String getHex(byte[] raw) { + return getHex(raw, raw.length); + } + public static String getHex(byte[] raw, int len) { final StringBuilder hex = new StringBuilder(2 * raw.length); - for (final byte b : raw) { + for (int x = 0; x < len; x ++) { + byte b = raw[x]; hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))); } return hex.toString(); @@ -17,4 +21,9 @@ public final class Ts3Logging { public static void debug(String message) { System.err.println("[DEBUG] " + message); } + + public static void debug(String message, Throwable ex) { + System.err.println("[DEBUG] " + message); + ex.printStackTrace(); + } } diff --git a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java index 772b4f8..9f1cbf4 100644 --- a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java +++ b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java @@ -1,6 +1,5 @@ package com.github.manevolent.ts3j; -import com.github.manevolent.ts3j.enums.ConnectionState; import junit.framework.TestCase; import java.net.InetSocketAddress; @@ -11,7 +10,7 @@ public class ServerConnectionTest extends TestCase { } public void testParser() throws Exception { - TeamspeakClient client = new TeamspeakClient(); + Teamspeak3Client client = new Teamspeak3Client(); client.connect(new InetSocketAddress( "voice.teamspeak.com", @@ -20,6 +19,6 @@ public class ServerConnectionTest extends TestCase { 10000L ); - assertEquals(client.getConnectionState(), ConnectionState.CONNECTED); + assertEquals(client.getClientConnectionState(), ClientConnectionState.CONNECTED); } }