diff --git a/README.md b/README.md index c641fa0..0de18c1 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ Discord: https://discord.gg/4q9jpCa -Teamspeak: teamlixo.net (that's an SRV record; ts.teamlixo.net is the A record) - TS3J is an open-source implementation of the reverse-engineered Teamspeak3 full server/client protocol, as an adaptation of Splamy's C# TS3Client source code. You can find that here: https://github.com/Splamy/TS3AudioBot/. # Maven @@ -112,4 +110,6 @@ public byte[] provide() { ``` # Receiving audio -This is definitely possible, but it's a work in progress for now. Once I have a need for this in my own bot (I can forsee there being an opportunity), I will be adding full support to TS3J. +Refer to the `setVoiceHandler` and `setWhisperHandler` methods to supply a Consumer object to receive Voice and Whisper packets. You will need to decode the packets yourself, and insert packet-loss-correction as needed. + +Note that the first 5 packets starting a voice session are marked with the COMPRESSED flag. The final voice packet, intended to singal to close your decoder and flush samples, is always empty (0-length byte array). diff --git a/pom.xml b/pom.xml index 9e6206b..faabc8b 100644 --- a/pom.xml +++ b/pom.xml @@ -45,5 +45,11 @@ commons-lang 2.6 + + + dnsjava + dnsjava + 2.1.8 + \ No newline at end of file diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/TS3DNS.java b/src/main/java/com/github/manevolent/ts3j/protocol/TS3DNS.java new file mode 100644 index 0000000..8b6fab9 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/TS3DNS.java @@ -0,0 +1,43 @@ +package com.github.manevolent.ts3j.protocol; + +import org.xbill.DNS.*; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.util.*; +import java.util.stream.Collectors; + +public class TS3DNS { + private static final int TS3_VOICE_DEFAULT_PORT = 9987; + private static final int TS3_QUERY_DEFAULT_PORT = 10011; + private static final int TS3_FILETRANSFER_DEFAULT_PORT = 30033; + private static final int TSDNS_DEFAULT_PORT = 41144; + + private static final String DNS_PREFIX_TCP = "_tsdns._tcp."; + private static final String DNS_PREFIX_UDP = "_ts3._udp."; + + private static List lookupSRV(String domain) throws IOException { + org.xbill.DNS.Record[] records = new Lookup(domain, Type.SRV).run(); + + if (records == null || records.length <= 0) throw new UnknownHostException("Host not found"); + + return Arrays.stream(records) + .filter(Objects::nonNull) + .map(x -> (SRVRecord) x) + .filter(x -> x.getTarget() != null) + .sorted((a, b) -> { + // Loose sorting + int prioComp = Integer.compare(a.getPriority(), b.getPriority()); + if (prioComp != 0) return prioComp; + else return Integer.compare(a.getWeight(), b.getWeight()); + }) + .map(x -> new InetSocketAddress(x.getTarget().toString(), x.getPort())) + .collect(Collectors.toList()); + } + + public static List lookup(String domain) throws IOException { + return lookupSRV(DNS_PREFIX_UDP + domain); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/fragment/PacketReassembly.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/fragment/PacketReassembly.java index e313725..36ad7c6 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/fragment/PacketReassembly.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/fragment/PacketReassembly.java @@ -40,7 +40,7 @@ public class PacketReassembly { temporaryCounter.setPacketId(currentPacketId.getKey()); temporaryCounter.setGeneration(currentPacketId.getValue()); - while (true) { + while (queue.size() > 0) { Pair thisPacketId = temporaryCounter.current(); Packet packet = queue.get(thisPacketId.getKey()); diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java index 09f4b88..78a2bbb 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java @@ -1,9 +1,11 @@ package com.github.manevolent.ts3j.protocol.packet.handler.client; +import com.github.manevolent.ts3j.command.Command; import com.github.manevolent.ts3j.command.SingleCommand; import com.github.manevolent.ts3j.command.parameter.CommandSingleParameter; import com.github.manevolent.ts3j.protocol.Packet; import com.github.manevolent.ts3j.protocol.ProtocolRole; +import com.github.manevolent.ts3j.protocol.TS3DNS; import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command; import com.github.manevolent.ts3j.protocol.packet.PacketBody8Init1; @@ -188,49 +190,48 @@ public class LocalClientHandlerConnecting extends LocalClientHandler { alphaBytes = null; - getClient().writePacket(new PacketBody2Command( + Command clientinit = new SingleCommand( + "clientinit", ProtocolRole.CLIENT, - new SingleCommand( - "clientinit", - ProtocolRole.CLIENT, - new CommandSingleParameter("client_nickname", getClient().getNickname()), - new CommandSingleParameter("client_version", "3.?.? [Build: 5680278000]"), - new CommandSingleParameter("client_platform", "Windows"), - new CommandSingleParameter( - "client_version_sign", - "DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg==" - ), - new CommandSingleParameter("client_input_hardware", "1"), - new CommandSingleParameter("client_output_hardware", "1"), - new CommandSingleParameter("client_default_channel", - getClient().getOption("client.default_channel", String.class) - ), - new CommandSingleParameter("client_default_channel_password", - getClient().getOption("client.default_channel_password", String.class) - ), - new CommandSingleParameter( - "client_server_password", - getClient().getOption("client.server_password", String.class) - ), - new CommandSingleParameter("client_nickname_phonetic", - getClient().getOption("client.nickname_phonetic", String.class) - ), - new CommandSingleParameter("client_meta_data", ""), - new CommandSingleParameter("client_default_token", - getClient().getOption("client.default_token", String.class) - ), - new CommandSingleParameter( - "client_key_offset", - Long.toString(getClient().getIdentity().getKeyOffset()) - ), - new CommandSingleParameter( - "hwid", - getClient().getOption("client.hwid", String.class) != null ? - getClient().getOption("client.hwid", String.class) : - "+LyYqbDqOvEEpN5pdAbF8/v5kZ0=" - ) + new CommandSingleParameter("client_nickname", getClient().getNickname()), + new CommandSingleParameter("client_version", "3.?.? [Build: 5680278000]"), + new CommandSingleParameter("client_platform", "Windows"), + new CommandSingleParameter( + "client_version_sign", + "DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg==" + ), + new CommandSingleParameter("client_input_hardware", "1"), + new CommandSingleParameter("client_output_hardware", "1"), + new CommandSingleParameter("client_default_channel", + getClient().getOption("client.default_channel", String.class) + ), + new CommandSingleParameter("client_default_channel_password", + getClient().getOption("client.default_channel_password", String.class) + ), + new CommandSingleParameter("client_server_password", + getClient().getOption("client.server_password", String.class) + ), + new CommandSingleParameter("client_nickname_phonetic", + getClient().getOption("client.nickname_phonetic", String.class) + ), + new CommandSingleParameter("client_meta_data", ""), + new CommandSingleParameter("client_default_token", + getClient().getOption("client.default_token", String.class) + ), + new CommandSingleParameter("client_key_offset", + Long.toString(getClient().getIdentity().getKeyOffset()) + ), + new CommandSingleParameter( + "hwid", + getClient().getOption("client.hwid", String.class) != null ? + getClient().getOption("client.hwid", String.class) : + "+LyYqbDqOvEEpN5pdAbF8/v5kZ0=" ) - )); + ); + + Ts3Debugging.debug(clientinit.build()); + + getClient().writePacket(new PacketBody2Command(ProtocolRole.CLIENT, clientinit)); getClient().setCommandProcessor(getClient()); getClient().setState(ClientConnectionState.RETRIEVING_DATA); diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerRetrievingData.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerRetrievingData.java index 3804d90..77bbf49 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerRetrievingData.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerRetrievingData.java @@ -1,9 +1,31 @@ package com.github.manevolent.ts3j.protocol.packet.handler.client; +import com.github.manevolent.ts3j.command.CommandProcessException; +import com.github.manevolent.ts3j.command.CommandProcessor; +import com.github.manevolent.ts3j.command.MultiCommand; +import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket; + public class LocalClientHandlerRetrievingData extends LocalClientHandlerFull { public LocalClientHandlerRetrievingData(LocalTeamspeakClientSocket client) { super(client); } + + @Override + protected void handleCommand(MultiCommand command) throws CommandProcessException { + if (command.getName().equalsIgnoreCase("error")) { + try { + getClient().setState(ClientConnectionState.DISCONNECTED); + } catch (Exception e) { + getClient().getExceptionHandler().accept(e); + } + + throw new CommandProcessException(command.simplifyOne().get("msg").getValue()); + } + + CommandProcessor processor = getClient().getCommandProcessor(); + if (processor != null) processor.process(getClient(), command); + } + } diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java index a17f60c..ed99b09 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java @@ -107,8 +107,8 @@ public abstract class AbstractTeamspeakClientSocket private boolean reading = false; - protected AbstractTeamspeakClientSocket() { - super(SocketRole.CLIENT); + protected AbstractTeamspeakClientSocket(SocketRole role) { + super(role); // Initialize reassembly queue for (PacketBodyType bodyType : PacketBodyType.values()) @@ -205,8 +205,6 @@ public abstract class AbstractTeamspeakClientSocket } protected void onDisconnect() { - clientOptions.clear(); - clientId = -1; readQueue.clear(); diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java index d9ec749..e7e6d31 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java @@ -12,9 +12,7 @@ import com.github.manevolent.ts3j.command.response.CommandResponse; import com.github.manevolent.ts3j.event.*; import com.github.manevolent.ts3j.identity.LocalIdentity; import com.github.manevolent.ts3j.identity.Uid; -import com.github.manevolent.ts3j.protocol.NetworkPacket; -import com.github.manevolent.ts3j.protocol.PacketKind; -import com.github.manevolent.ts3j.protocol.ProtocolRole; +import com.github.manevolent.ts3j.protocol.*; import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.header.PacketHeader; import com.github.manevolent.ts3j.protocol.packet.PacketBody0Voice; @@ -60,7 +58,7 @@ public class LocalTeamspeakClientSocket private Map awaitingCommands = new ConcurrentHashMap<>(); public LocalTeamspeakClientSocket() { - super(); + super(SocketRole.CLIENT); namedProcessors.put("initserver", new InitServerHandler()); @@ -254,12 +252,34 @@ public class LocalTeamspeakClientSocket remote = null; } + + /** + * Initiates a connection to a server + * @param hostname Hostname to contact + * @param password server password (may be null) + * @param timeout timeout, in milliseconds, to complete a connection. + */ + public void connect(String hostname, String password, long timeout) throws + IOException, + TimeoutException { + + InetSocketAddress address; + + try { + // Attempt TS3DNS + address = TS3DNS.lookup(hostname).stream().findFirst().orElseThrow(() -> new UnknownHostException(hostname)); + } catch (UnknownHostException e) { + address = new InetSocketAddress(hostname, 9987); + } + + connect(address, password, timeout); + } + /** * Initiates a connection to a server * @param remote remote sever to contact - * @param password server password + * @param password server password (may be null) * @param timeout timeout, in milliseconds, to complete a connection. - * @throws IOException */ public void connect(InetSocketAddress remote, String password, long timeout) throws IOException, TimeoutException { @@ -286,7 +306,7 @@ public class LocalTeamspeakClientSocket setOption("client.hostname", remote.getHostString()); - if (password != null) setOption("client.password", password); + if (password != null && password.length() > 0) setOption("client.password", password); setState(ClientConnectionState.CONNECTING); @@ -297,6 +317,7 @@ public class LocalTeamspeakClientSocket 0.01F, new MicrophoneTask() ); + microphoneThread.setDaemon(true); microphoneThread.start(); } catch (TimeoutException e) { @@ -629,6 +650,14 @@ public class LocalTeamspeakClientSocket super.setNickname(nickname); } + public void setHWID(String hwid) { + setOption("client.hwid", hwid); + } + + public String getHWID() { + return getOption("client.hwid", String.class); + } + /** * Commands */ @@ -878,6 +907,26 @@ public class LocalTeamspeakClientSocket return executeCommand(command, x -> new Client(x.toMap())).get(); } + public void serverGroupAddClient(int groupId, int clientDatabaseId) + throws IOException, TimeoutException, ExecutionException, InterruptedException { + Command command = new SingleCommand("servergroupaddclient", ProtocolRole.CLIENT); + + command.add(new CommandSingleParameter("sgid", Integer.toString(groupId))); + command.add(new CommandSingleParameter("cldbid", Integer.toString(clientDatabaseId))); + + executeCommand(command).complete(); + } + + public void serverGroupRemoveClient(int groupId, int clientDatabaseId) + throws IOException, TimeoutException, ExecutionException, InterruptedException { + Command command = new SingleCommand("servergroupdelclient", ProtocolRole.CLIENT); + + command.add(new CommandSingleParameter("sgid", Integer.toString(groupId))); + command.add(new CommandSingleParameter("cldbid", Integer.toString(clientDatabaseId))); + + executeCommand(command).complete(); + } + /** * Moves one or more clients specified with clid to the channel with ID cid. If the target channel has a password, it needs to be specified with cpw. If the diff --git a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java index c332d85..97c710d 100644 --- a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java +++ b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java @@ -31,135 +31,25 @@ public class ServerConnectionTest { identity.improveSecurity(10); client.setIdentity(identity); - client.setNickname("Hello from Java"); + client.setHWID("JAVAJAVAJAVA"); - client.setOption("client.hwid", "JAVAJAVAJAVA"); - client.connect( - new InetSocketAddress( - "ts.teamlixo.net", - 9987 - ), - null, - 10000L - ); + while (true) { + try { - client.addListener(new TS3Listener() { - @Override - public void onTextMessage(TextMessageEvent e) { + client.connect( + "teamlixo.net", + null, + 10000L + ); + assertEquals(client.getState(), ClientConnectionState.CONNECTED); + } catch (Exception ex) { + ex.printStackTrace(); } - @Override - public void onClientJoin(ClientJoinEvent e) { - System.err.println(e.get("cid")); - } - - @Override - public void onClientLeave(ClientLeaveEvent e) { - - } - - @Override - public void onServerEdit(ServerEditedEvent e) { - - } - - @Override - public void onChannelEdit(ChannelEditedEvent e) { - - } - - @Override - public void onChannelDescriptionChanged(ChannelDescriptionEditedEvent e) { - - } - - @Override - public void onClientMoved(ClientMovedEvent e) { - - } - - @Override - public void onChannelCreate(ChannelCreateEvent e) { - - } - - @Override - public void onChannelDeleted(ChannelDeletedEvent e) { - - } - - @Override - public void onChannelMoved(ChannelMovedEvent e) { - - } - - @Override - public void onChannelPasswordChanged(ChannelPasswordChangedEvent e) { - - } - - @Override - public void onChannelList(ChannelListEvent e) { - - } - - @Override - public void onPrivilegeKeyUsed(PrivilegeKeyUsedEvent e) { - - } - - @Override - public void onChannelGroupList(ChannelGroupListEvent e) { - - } - - @Override - public void onServerGroupList(ServerGroupListEvent e) { - - } - - @Override - public void onClientNeededPermissions(ClientNeededPermissionsEvent e) { - - } - - @Override - public void onClientChannelGroupChanged(ClientChannelGroupChangedEvent e) { - - } - - @Override - public void onClientChanged(ClientUpdatedEvent e) { - - } - - @Override - public void onDisconnected(DisconnectedEvent e) { - - } - - @Override - public void onChannelSubscribed(ChannelSubscribedEvent e) { - - } - }); - - assertEquals(client.getState(), ClientConnectionState.CONNECTED); - - client.subscribeAll(); - - try { - client.setVoiceHandler(packetBody0Voice -> { - System.err.println("clientId=" + packetBody0Voice.getClientId()); - System.err.println("packetId=" + packetBody0Voice.getPacketId()); - System.err.println("type=" + packetBody0Voice.getCodecType()); - System.err.println("len=" + packetBody0Voice.getCodecData().length); - }); - } catch (Exception ex) { - ex.printStackTrace(); + client.disconnect(); } } } diff --git a/src/test/java/com/github/manevolent/ts3j/TS3DNSTest.java b/src/test/java/com/github/manevolent/ts3j/TS3DNSTest.java new file mode 100644 index 0000000..a34fbb6 --- /dev/null +++ b/src/test/java/com/github/manevolent/ts3j/TS3DNSTest.java @@ -0,0 +1,27 @@ +package com.github.manevolent.ts3j; + +import com.github.manevolent.ts3j.protocol.TS3DNS; +import com.github.manevolent.ts3j.util.Ts3Debugging; +import junit.framework.TestCase; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.List; + +public class TS3DNSTest extends TestCase { + + public static void main(String[] args) throws Exception { + new TS3DNSTest().testParser(); + } + + public void testParser() throws Exception { + Ts3Debugging.setEnabled(true); + + List soaddr = TS3DNS.lookup("voice.teamspeak.com"); + + assertTrue(soaddr.size() > 0); + + for (SocketAddress socketAddress : soaddr) + Ts3Debugging.debug(socketAddress); + } +}