Got past Init1, required a large refactor and the addition of bouncycastle.

Serious thanks to Splamy for doing a lot of the hard work already.
This commit is contained in:
Manevolent
2018-07-23 20:35:36 -04:00
parent 932fd69769
commit c4924eb335
32 changed files with 911 additions and 282 deletions
@@ -1,28 +0,0 @@
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);
}
}
}
@@ -1,8 +1,48 @@
package com.github.manevolent.ts3j.command;
import com.github.manevolent.ts3j.command.part.CommandParameter;
import com.github.manevolent.ts3j.protocol.ProtocolRole;
public interface Command {
/**
* Gets the name, or label, of this command.
* @return Command label.
*/
String getName();
/**
* Gets the intended use role of this command (source).
* @return Command role.
*/
ProtocolRole getRole();
/**
* Gets the parameter list of this command.
* @return Parameter list.
*/
Iterable<CommandParameter> getParameters();
void appendParameter(CommandParameter parameter);
/**
* Finds if the command is expected to be responded to with a specific return_code.
* This is handled by supporting protocol handlers, not by the command code itself.
* @return true if the command is expected to support a return_code.
*/
default boolean willExpectResponse() { return false; }
/**
* Builds the command to a netowrk-issueable string.
* The implementation currently defaults to the basic command format for TS3:
* label -flag parameter=value
* @return Baked command string.
*/
default String build() {
StringBuilder builder = new StringBuilder();
builder.append(getName());
for (CommandParameter parameter : getParameters()) {
builder.append(" " + (parameter.toString()).trim());
}
return builder.toString();
}
}
@@ -1,39 +1,34 @@
package com.github.manevolent.ts3j.command;
import com.github.manevolent.ts3j.command.part.CommandParameter;
import com.github.manevolent.ts3j.protocol.ProtocolRole;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class SimpleCommand implements Command {
private final String name;
private final List<CommandParameter> parameterList = new LinkedList<>();
private final ProtocolRole role;
private final Collection<CommandParameter> params;
public SimpleCommand(String name) {
public SimpleCommand(String name, ProtocolRole role, Collection<CommandParameter> params) {
this.name = name;
this.role = role;
this.params = params;
}
@Override
public String getName() {
return name;
}
@Override
public ProtocolRole getRole() {
return role;
}
@Override
public Iterable<CommandParameter> getParameters() {
return Collections.unmodifiableList(parameterList);
}
@Override
public void appendParameter(CommandParameter parameter) {
parameterList.add(parameter);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(name);
for (CommandParameter parameter : getParameters()) {
builder.append(" ");
builder.append(parameter.toString());
}
return builder.toString();
return Collections.unmodifiableCollection(params);
}
}
@@ -0,0 +1,50 @@
package com.github.manevolent.ts3j.command;
import com.github.manevolent.ts3j.command.part.CommandParameter;
import com.github.manevolent.ts3j.protocol.ProtocolRole;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
public abstract class TypedCommand implements Command {
private final ProtocolRole role;
private final String name;
private final Map<String, CommandParameter> parameters = new LinkedHashMap<>();
protected TypedCommand(ProtocolRole role, String name) {
this.role = role;
this.name = name;
}
protected CommandParameter set(CommandParameter commandParameter) {
this.parameters.put(commandParameter.getName(), commandParameter);
return commandParameter;
}
public CommandParameter get(String key) {
return this.parameters.get(key);
}
@Override
public Iterable<CommandParameter> getParameters() {
return Collections.unmodifiableCollection(parameters.values());
}
@Override
public String getName() {
return name;
}
@Override
public ProtocolRole getRole() {
return role;
}
@Override
public String toString() {
return build();
}
}
@@ -1,17 +1,68 @@
package com.github.manevolent.ts3j.command.part;
import com.github.manevolent.ts3j.enums.CommandPartType;
public class CommandOption implements CommandParameter {
private final String value;
private final String key;
private boolean enabled = false;
public CommandOption(String value) {
this.value = " -" + value;
public CommandOption(String key) {
this.key = key;
}
@Override
public void set(boolean value) {
this.enabled = value;
}
@Override
public CommandPartType getType() {
return CommandPartType.OPTION;
public String getName() {
return key;
}
@Override
public void set(char value) {
throw new UnsupportedOperationException();
}
@Override
public void set(String value) {
throw new UnsupportedOperationException();
}
@Override
public void set(byte value) {
throw new UnsupportedOperationException();
}
@Override
public void set(short value) {
throw new UnsupportedOperationException();
}
@Override
public void set(int value) {
throw new UnsupportedOperationException();
}
@Override
public void set(long value) {
throw new UnsupportedOperationException();
}
@Override
public void set(float value) {
throw new UnsupportedOperationException();
}
@Override
public void set(double value) {
throw new UnsupportedOperationException();
}
@Override
public String toString() {
if (enabled)
return "-" + key;
else
return "";
}
}
@@ -1,9 +1,23 @@
package com.github.manevolent.ts3j.command.part;
import com.github.manevolent.ts3j.enums.CommandPartType;
public interface CommandParameter {
CommandPartType getType();
String getName();
void set(char value);
void set(String value);
void set(boolean value);
void set(byte value);
void set(short value);
void set(int value);
void set(long value);
void set(float value);
void set(double value);
/**
* Converts the command part to a usable command string element.
* @return Command string element.
*/
String toString();
}
@@ -1,81 +1,70 @@
package com.github.manevolent.ts3j.command.part;
import com.github.manevolent.ts3j.enums.CommandPartType;
import com.github.manevolent.ts3j.util.Ts3String;
public class CommandSingleParameter implements CommandParameter {
private final String key, value;
private final String key;
private String value;
public CommandSingleParameter(String key, String value) {
public CommandSingleParameter(String key) {
this.key = key;
this.value = value;
}
public CommandSingleParameter(String key, boolean value) {
this.key = key;
this.value = serialize(value);
}
public CommandSingleParameter(String key, byte value) {
this.key = key;
this.value = serialize(value);
}
public CommandSingleParameter(String key, short value) {
this.key = key;
this.value = serialize(value);
}
public CommandSingleParameter(String key, int value) {
this.key = key;
this.value = serialize(value);
}
public CommandSingleParameter(String key, long value) {
this.key = key;
this.value = serialize(value);
}
public CommandSingleParameter(String key, float value) {
this.key = key;
this.value = serialize(value);
}
public CommandSingleParameter(String key, double value) {
this.key = key;
this.value = serialize(value);
}
@Override
public CommandPartType getType() {
return CommandPartType.SINGLE_PARAMETER;
public String toString() {
if (value == null)
return "";
else
return (key + "=" + Ts3String.escape(value.trim()));
}
public static String serialize(boolean value) {
return value ? "1": "0";
@Override
public String getName() {
return key;
}
public static String serialize(byte value) {
return Byte.toString(value);
@Override
public void set(char value) {
this.value = Character.toString(value);
}
public static String serialize(short value) {
return Short.toString(value);
@Override
public void set(String value) {
this.value = value;
}
public static String serialize(int value) {
return Integer.toString(value);
@Override
public void set(boolean value) {
this.value = (value ? "1" : "0");
}
public static String serialize(long value) {
return Long.toString(value);
@Override
public void set(byte value) {
this.value = Byte.toString(value);
}
public static String serialize(float value) {
return Float.toString(value);
@Override
public void set(short value) {
this.value = Short.toString(value);
}
public static String serialize(double value) {
return Double.toString(value);
@Override
public void set(int value) {
this.value = Integer.toString(value);
}
@Override
public void set(long value) {
this.value = Long.toString(value);
}
@Override
public void set(float value) {
this.value = Float.toString(value);
}
@Override
public void set(double value) {
this.value = Double.toString(value);
}
}
@@ -0,0 +1,16 @@
package com.github.manevolent.ts3j.command.type.client;
import com.github.manevolent.ts3j.command.TypedCommand;
import com.github.manevolent.ts3j.command.part.CommandSingleParameter;
import com.github.manevolent.ts3j.protocol.ProtocolRole;
public class ClientInitIV extends TypedCommand {
public ClientInitIV() {
super(ProtocolRole.CLIENT, "clientinitiv");
set(new CommandSingleParameter("ot"));
set(new CommandSingleParameter("alpha"));
set(new CommandSingleParameter("omega"));
set(new CommandSingleParameter("ip"));
}
}
@@ -1,31 +0,0 @@
package com.github.manevolent.ts3j.handler;
import com.github.manevolent.ts3j.Teamspeak3Client;
import com.github.manevolent.ts3j.ClientConnectionState;
import com.github.manevolent.ts3j.protocol.NetworkPacket;
import java.io.IOException;
public abstract class TeamspeakClientHandler {
private final Teamspeak3Client client;
protected TeamspeakClientHandler(Teamspeak3Client client) {
this.client = client;
}
protected final Teamspeak3Client getClient() {
return client;
}
public void handleConnectionStateChanging(ClientConnectionState clientConnectionState) {
}
public void onAssigned() throws IOException {
}
public void handlePacket(NetworkPacket packet) throws IOException {
}
}
@@ -1,9 +0,0 @@
package com.github.manevolent.ts3j.handler;
import com.github.manevolent.ts3j.Teamspeak3Client;
public class TeamspeakClientHandlerConnected extends TeamspeakClientHandler {
public TeamspeakClientHandlerConnected(Teamspeak3Client client) {
super(client);
}
}
@@ -1,9 +0,0 @@
package com.github.manevolent.ts3j.handler;
import com.github.manevolent.ts3j.Teamspeak3Client;
public class TeamspeakClientHandlerDisconnected extends TeamspeakClientHandler {
public TeamspeakClientHandlerDisconnected(Teamspeak3Client client) {
super(client);
}
}
@@ -0,0 +1,159 @@
package com.github.manevolent.ts3j.identity;
import com.github.manevolent.ts3j.util.Ts3Logging;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DERInteger;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.math.ec.ECPoint;
import java.io.IOException;
import java.nio.charset.Charset;
import java.security.DigestException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class Identity {
private static MessageDigest sha1;
static {
try {
sha1 = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private final ECPoint publicKey;
private final String fingerprint;
private final String publicKeyString;
private long keyOffset = 0L, lastCheckedKeyOffset = 0L;
public Identity(ECPoint publicKey) {
this.publicKey = publicKey;
this.publicKeyString = Base64.getEncoder().encodeToString(encodePublicKey());
this.fingerprint = generateFingerprint();
}
public ECPoint getPublicKey() {
return publicKey;
}
public String getPublicKeyString() {
return publicKeyString;
}
public String getFingerprint() {
return fingerprint;
}
public long getLastCheckedKeyOffset() {
return lastCheckedKeyOffset;
}
public void setLastCheckedKeyOffset(long lastCheckedKeyOffset) {
this.lastCheckedKeyOffset = lastCheckedKeyOffset;
}
public long getKeyOffset() {
return keyOffset;
}
public void setKeyOffset(long keyOffset) {
this.keyOffset = keyOffset;
}
public void improveSecurity(int target) {
byte[] hashBuffer = new byte[getPublicKeyString().length() + 20];
byte[] pubKeyBytes = getPublicKeyString().getBytes(Charset.forName("ASCII"));
System.arraycopy(pubKeyBytes, 0, hashBuffer, 0, pubKeyBytes.length);
setLastCheckedKeyOffset(Math.max(getKeyOffset(), getLastCheckedKeyOffset()));
int best = getSecurityLevel(hashBuffer, pubKeyBytes.length, getKeyOffset());
while (true)
{
if (best >= target) return;
int curr = getSecurityLevel(hashBuffer, pubKeyBytes.length, lastCheckedKeyOffset);
if (curr > best)
{
Ts3Logging.debug("Improved identity security level: from " + best + " to " + curr);
keyOffset = lastCheckedKeyOffset;
best = curr;
}
lastCheckedKeyOffset ++;
}
}
public int getSecurityLevel() {
byte[] hashBuffer = new byte[getPublicKeyString().length() + 20];
byte[] pubKeyBytes = getPublicKeyString().getBytes(Charset.forName("ASCII"));
System.arraycopy(pubKeyBytes, 0, hashBuffer, 0, pubKeyBytes.length);
return getSecurityLevel(hashBuffer, pubKeyBytes.length, getKeyOffset());
}
private static int getSecurityLevel(byte[] hashBuffer, int pubKeyLen, long offset) {
byte[] numBuffer = new byte[20];
int numLen = 0;
do
{
numBuffer[numLen] = (byte)((offset % 10) & 0xFF);
offset /= 10;
numLen ++;
} while (offset > 0);
for (int i = 0; i < numLen; i ++)
hashBuffer[pubKeyLen + i] = numBuffer[numLen - (i + 1)];
byte[] outHash;
synchronized (sha1) {
sha1.update(hashBuffer, 0, pubKeyLen + numLen);
outHash = sha1.digest();
}
return getLeadingZeroBits(outHash);
}
// GOOD
private static int getLeadingZeroBits(byte[] data) {
int curr = 0;
int i ;
for (i = 0; i < data.length; i ++) {
if (data[i] == 0) curr += 8;
else break;
}
if (i < data.length)
for (int bit = 0; bit < 8; bit ++) {
if ((data[i] & (1 << bit)) == 0) curr++;
else break;
}
return curr;
}
private byte[] encodePublicKey() {
try {
byte[] dataArray = new DERSequence(new ASN1Encodable[]{
new DERBitString(new byte[]{0b0000_0000}, 7),
new DERInteger(32),
new DERInteger(publicKey.getAffineXCoord().toBigInteger()),
new DERInteger(publicKey.getAffineYCoord().toBigInteger())
}).getEncoded();
return dataArray;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String generateFingerprint() {
synchronized (sha1) {
return Base64.getEncoder().encodeToString(sha1.digest(encodePublicKey()));
}
}
}
@@ -0,0 +1,59 @@
package com.github.manevolent.ts3j.identity;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.math.ec.ECPoint;
import java.math.BigInteger;
import java.security.*;
public class LocalIdentity extends Identity {
static {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
}
private final BigInteger privateKey;
public LocalIdentity(ECPoint publicKey, BigInteger privateKey) {
super(publicKey);
this.privateKey = privateKey;
}
public static LocalIdentity load(ECPoint publicKey,
BigInteger privateKey,
long keyOffset,
long lastCheckedKeyOffset) {
LocalIdentity localIdentity = new LocalIdentity(publicKey, privateKey);
localIdentity.setKeyOffset(keyOffset);
localIdentity.setLastCheckedKeyOffset(lastCheckedKeyOffset);
return localIdentity;
}
public static LocalIdentity generateNew(int securityLevel) throws GeneralSecurityException {
ECNamedCurveParameterSpec ecp = ECNamedCurveTable.getParameterSpec("prime256v1");
ECDomainParameters domainParams =
new ECDomainParameters(ecp.getCurve(), ecp.getG(), ecp.getN(), ecp.getH(), ecp.getSeed());
ECKeyGenerationParameters keyGenParams = new ECKeyGenerationParameters(domainParams, new SecureRandom());
ECKeyPairGenerator generator = new ECKeyPairGenerator();
generator.init(keyGenParams);
AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();
ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic();
LocalIdentity localIdentity = load(publicKey.getQ().normalize(), privateKey.getD(), 0, 0);
localIdentity.improveSecurity(securityLevel);
return localIdentity;
}
}
@@ -0,0 +1,13 @@
package com.github.manevolent.ts3j.protocol;
import com.github.manevolent.ts3j.protocol.packet.Packet;
import java.io.IOException;
public interface Endpoint {
SocketRole getRole();
void send(Packet packet) throws IOException;
}
@@ -0,0 +1,109 @@
package com.github.manevolent.ts3j.protocol;
import com.github.manevolent.ts3j.identity.LocalIdentity;
import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler;
import com.github.manevolent.ts3j.protocol.socket.LocalTeamspeakSocket;
import com.github.manevolent.ts3j.util.Ts3Logging;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
public abstract class LocalEndpoint implements Endpoint {
private final LocalTeamspeakSocket socket;
private PacketHandler handler = null;
private LocalIdentity localIdentity = null;
private final Object runLock = new Object();
private Thread receiverThread;
private boolean running = false;
protected LocalEndpoint(LocalTeamspeakSocket socket) {
this.socket = socket;
}
protected LocalEndpoint(SocketRole socketRole) throws SocketException {
this.socket = new LocalTeamspeakSocket(socketRole);
}
public InetSocketAddress getLocalAddress() {
return getSocket().getLocalAddress();
}
public InetSocketAddress getRemoteAddress() {
return getSocket().getLocalAddress();
}
protected final LocalTeamspeakSocket getSocket() {
return socket;
}
public void setRunning(boolean running) {
if (this.running != running) {
this.running = running;
if (running) {
Ts3Logging.debug("Starting network receiver thread...");
receiverThread = new Thread(new PacketReceiver());
receiverThread.setDaemon(true);
receiverThread.start();
} else {
if (receiverThread != null) {
receiverThread.interrupt();
receiverThread = null;
}
}
}
}
@Override
public SocketRole getRole() {
return socket.getSocketRole();
}
private NetworkPacket receive() throws IOException {
return socket.receive();
}
public void setHandler(PacketHandler handler) throws IOException {
if (this.handler != handler) {
if (this.handler != null) this.handler.onUnassigning();
this.handler = handler;
if (handler != null) handler.onAssigned();
}
}
protected void handlePacket(NetworkPacket packet) throws IOException {
if (this.handler != null) this.handler.handlePacket(packet);
}
public LocalIdentity getLocalIdentity() {
return localIdentity;
}
public void setLocalIdentity(LocalIdentity localIdentity) {
this.localIdentity = localIdentity;
}
private class PacketReceiver implements Runnable {
@Override
public void run() {
Ts3Logging.debug("Entering packet receiver");
while (running) {
try {
handlePacket(receive());
} catch (Exception e) {
if (e instanceof InterruptedException) continue;
Ts3Logging.debug("Problem handling packet", e);
}
}
Ts3Logging.debug("Leaving packet receiver");
}
}
}
@@ -0,0 +1,7 @@
package com.github.manevolent.ts3j.protocol;
import java.net.InetSocketAddress;
public abstract class RemoteEndpoint implements Endpoint {
public abstract InetSocketAddress getRemoteAddress();
}
@@ -0,0 +1,12 @@
package com.github.manevolent.ts3j.protocol;
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
import com.github.manevolent.ts3j.protocol.packet.Packet;
import java.io.IOException;
public interface TeamspeakSocket {
SocketRole getSocketRole();
void send(PacketHeader header, Packet packet) throws IOException;
}
@@ -0,0 +1,31 @@
package com.github.manevolent.ts3j.protocol.client;
import com.github.manevolent.ts3j.protocol.packet.handler.client.LocalClientHandler;
import com.github.manevolent.ts3j.protocol.packet.handler.client.LocalClientHandlerConnected;
import com.github.manevolent.ts3j.protocol.packet.handler.client.LocalClientHandlerConnecting;
import com.github.manevolent.ts3j.protocol.packet.handler.client.LocalClientHandlerDisconnected;
public enum ClientConnectionState {
CONNECTING(LocalClientHandlerConnecting.class),
CONNECTED(LocalClientHandlerConnected.class),
DISCONNECTED(LocalClientHandlerDisconnected.class);
private final Class<? extends LocalClientHandler> clazz;
ClientConnectionState(Class<? extends LocalClientHandler> clazz) {
this.clazz = clazz;
}
public Class<? extends LocalClientHandler> getHandlerClass() {
return clazz;
}
public LocalClientHandler createHandler(LocalTeamspeakClient client) {
try {
return clazz.getConstructor(LocalTeamspeakClient.class).newInstance(client);
} catch (ReflectiveOperationException ex) {
throw new RuntimeException(ex);
}
}
}
@@ -1,50 +1,71 @@
package com.github.manevolent.ts3j;
package com.github.manevolent.ts3j.protocol.client;
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.identity.LocalIdentity;
import com.github.manevolent.ts3j.protocol.LocalEndpoint;
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.handler.client.LocalClientHandler;
import com.github.manevolent.ts3j.protocol.socket.LocalTeamspeakSocket;
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;
public class LocalTeamspeakClient extends LocalEndpoint implements TeamspeakClient {
private final Object connectionStateLock = new Object();
private final Map<String, Object> clientOptions = new LinkedHashMap<>();
private TeamspeakClientHandler handler;
private InetSocketAddress remote = null;
private LocalClientHandler handler;
private ClientConnectionState clientConnectionState = null;
private int returnCodeIndex = 0;
public Teamspeak3Client(DatagramSocket datagramSocket) throws SocketException {
this.socket = new Teamspeak3Socket(SocketRole.CLIENT, datagramSocket);
public LocalTeamspeakClient(LocalTeamspeakSocket socket) {
super(socket);
if (socket.getSocketRole() != SocketRole.CLIENT)
throw new IllegalArgumentException("invalid socket role: " + socket.getSocketRole().name());
setClientConnectionState(ClientConnectionState.DISCONNECTED);
}
public Teamspeak3Client() throws SocketException {
this(new DatagramSocket());
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 InetSocketAddress getRemote() {
return remote;
public LocalTeamspeakClient() throws SocketException {
this(new LocalTeamspeakSocket(SocketRole.CLIENT));
}
@Override
public SocketRole getRole() {
return SocketRole.CLIENT;
}
public void send(Packet packet) throws IOException {
@@ -53,7 +74,7 @@ public class Teamspeak3Client {
packet.setHeaderValues(header);
socket.send(header, packet);
getSocket().send(header, packet);
}
protected void setClientConnectionState(ClientConnectionState state) {
@@ -65,13 +86,8 @@ public class Teamspeak3Client {
this.clientConnectionState = state;
if (wasDisconnected) {
Ts3Logging.debug("Starting network receiver thread...");
Thread receiverThread = new Thread(new PacketReceiver());
receiverThread.setDaemon(true);
receiverThread.start();
}
if (wasDisconnected)
setRunning(true);
if (handler != null)
handler.handleConnectionStateChanging(state);
@@ -82,7 +98,7 @@ public class Teamspeak3Client {
handler = state.createHandler(this);
try {
handler.onAssigned();
setHandler(handler);
} catch (IOException e) {
throw new IllegalStateException(e);
}
@@ -102,6 +118,11 @@ public class Teamspeak3Client {
return clientConnectionState == ClientConnectionState.CONNECTED;
}
@Override
public Map<String, Object> getOptions() {
return clientOptions;
}
public ClientConnectionState getClientConnectionState() {
return clientConnectionState;
}
@@ -120,46 +141,6 @@ public class Teamspeak3Client {
}
}
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)
@@ -168,7 +149,7 @@ public class Teamspeak3Client {
setOption("client.hostname", remote.getHostString());
setOption("client.password", password);
socket.connect(remote);
getSocket().connect(remote);
setClientConnectionState(ClientConnectionState.CONNECTING);
joinConnectionState(ClientConnectionState.CONNECTED, timeout);
@@ -178,25 +159,4 @@ public class Teamspeak3Client {
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,37 @@
package com.github.manevolent.ts3j.protocol.client;
import com.github.manevolent.ts3j.protocol.Endpoint;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Map;
public interface TeamspeakClient extends Endpoint {
void connect(InetSocketAddress remote, String password, long timeout) throws IOException;
SocketAddress getRemoteAddress();
boolean isConnected();
Map<String, Object> getOptions();
default <T extends Object> T getOption(String key, Class<T> clazz) {
Object value = getOptions().get(key);
if (value == null) return (T) null;
else if (!value.getClass().isAssignableFrom(clazz)) {
throw new ClassCastException(
clazz.getName()
+ " is not assignable to option type " +
value.getClass().getName()
);
} else
return (T) value;
}
default Object setOption(String key, Object value) {
return getOptions().put(key, value);
}
}
@@ -90,6 +90,9 @@ public class Packet8Init1 extends Packet {
case 3:
step = new Step3();
break;
case 4:
step = new Step4();
break;
default:
throw new IllegalArgumentException("invalid Init1 step: " + stepNumber);
}
@@ -0,0 +1,30 @@
package com.github.manevolent.ts3j.protocol.packet.handler;
import com.github.manevolent.ts3j.protocol.NetworkPacket;
import com.github.manevolent.ts3j.protocol.LocalEndpoint;
import java.io.IOException;
public abstract class PacketHandler {
private final LocalEndpoint endpoint;
protected PacketHandler(LocalEndpoint endpoint) {
this.endpoint = endpoint;
}
public void onUnassigning() throws IOException {
}
public void onAssigned() throws IOException {
}
public void handlePacket(NetworkPacket packet) throws IOException {
}
public LocalEndpoint getEndpoint() {
return endpoint;
}
}
@@ -0,0 +1,26 @@
package com.github.manevolent.ts3j.protocol.packet.handler.client;
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient;
import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler;
import java.io.IOException;
public abstract class LocalClientHandler extends PacketHandler {
private final LocalTeamspeakClient client;
protected LocalClientHandler(LocalTeamspeakClient client) {
super(client);
this.client = client;
}
public void handleConnectionStateChanging(ClientConnectionState clientConnectionState) {
}
protected LocalTeamspeakClient getClient() {
return client;
}
}
@@ -0,0 +1,9 @@
package com.github.manevolent.ts3j.protocol.packet.handler.client;
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient;
public class LocalClientHandlerConnected extends LocalClientHandler {
public LocalClientHandlerConnected(LocalTeamspeakClient client) {
super(client);
}
}
@@ -1,20 +1,23 @@
package com.github.manevolent.ts3j.handler;
package com.github.manevolent.ts3j.protocol.packet.handler.client;
import com.github.manevolent.ts3j.Teamspeak3Client;
import com.github.manevolent.ts3j.command.Command;
import com.github.manevolent.ts3j.command.type.client.ClientInitIV;
import com.github.manevolent.ts3j.protocol.NetworkPacket;
import com.github.manevolent.ts3j.protocol.ProtocolRole;
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient;
import com.github.manevolent.ts3j.protocol.packet.Packet8Init1;
import com.github.manevolent.ts3j.util.Ts3Logging;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.util.Base64;
import java.util.Random;
public class TeamspeakClientHandlerConnecting extends TeamspeakClientHandler {
public class LocalClientHandlerConnecting extends LocalClientHandler {
private byte[] randomBytes;
private byte[] alphaBytes;
public TeamspeakClientHandlerConnecting(Teamspeak3Client client) {
public LocalClientHandlerConnecting(LocalTeamspeakClient client) {
super(client);
}
@@ -90,10 +93,12 @@ public class TeamspeakClientHandlerConnecting extends TeamspeakClientHandler {
byte[] solution =
x.modPow(BigInteger.valueOf(2L).pow(serverReplyStep3.getLevel()), n).toByteArray();
Ts3Logging.debug(Ts3Logging.getHex(solution));
System.arraycopy(
solution, Math.max(0, solution.length - 64),
y, 64 - solution.length,
solution.length
solution, Math.abs(solution.length - 64),
y, solution.length < 64 ? 64 - solution.length : 0,
solution.length < 64 ? solution.length : 64
);
// Build response
@@ -107,8 +112,17 @@ public class TeamspeakClientHandlerConnecting extends TeamspeakClientHandler {
step4.setY(y);
step4.setServerStuff(serverReplyStep3.getServerStuff());
alphaBytes = new byte[10];
new Random().nextBytes(alphaBytes);
step4.setClientIVcommand(new byte[4]); // TODO
ClientInitIV command = new ClientInitIV();
command.get("alpha").set(Base64.getEncoder().encodeToString(alphaBytes));
command.get("omega").set(getClient().getLocalIdentity().getPublicKeyString());
command.get("ot").set(1); // ?
command.get("ip").set(null);
step4.setClientIVcommand(command.build().getBytes(Charset.forName("UTF8")));
response4.setStep(step4);
@@ -0,0 +1,9 @@
package com.github.manevolent.ts3j.protocol.packet.handler.client;
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient;
public class LocalClientHandlerDisconnected extends LocalClientHandler {
public LocalClientHandlerDisconnected(LocalTeamspeakClient client) {
super(client);
}
}
@@ -0,0 +1,36 @@
package com.github.manevolent.ts3j.protocol.server;
import com.github.manevolent.ts3j.protocol.NetworkPacket;
import com.github.manevolent.ts3j.protocol.RemoteEndpoint;
import com.github.manevolent.ts3j.protocol.SocketRole;
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient;
import com.github.manevolent.ts3j.protocol.packet.Packet;
import java.io.IOException;
import java.net.InetSocketAddress;
public class RemoteTeamspeakServer extends RemoteEndpoint implements TeamspeakServer {
private final LocalTeamspeakClient client;
private final InetSocketAddress address;
public RemoteTeamspeakServer(LocalTeamspeakClient client,
InetSocketAddress address) {
this.client = client;
this.address = address;
}
@Override
public SocketRole getRole() {
return SocketRole.SERVER;
}
@Override
public void send(Packet packet) throws IOException {
client.send(packet);
}
@Override
public InetSocketAddress getRemoteAddress() {
return address;
}
}
@@ -0,0 +1,11 @@
package com.github.manevolent.ts3j.protocol.server;
import com.github.manevolent.ts3j.protocol.Endpoint;
import java.net.InetSocketAddress;
public interface TeamspeakServer extends Endpoint {
}
@@ -1,14 +1,14 @@
package com.github.manevolent.ts3j.protocol;
package com.github.manevolent.ts3j.protocol.socket;
import com.github.manevolent.ts3j.protocol.NetworkPacket;
import com.github.manevolent.ts3j.protocol.SocketRole;
import com.github.manevolent.ts3j.protocol.TeamspeakSocket;
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.net.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -17,18 +17,18 @@ import java.nio.ByteOrder;
*
* Used simply to transcribe packets on the network
*/
public final class Teamspeak3Socket {
public class LocalTeamspeakSocket implements TeamspeakSocket {
private final SocketRole socketRole;
private final DatagramSocket datagramSocket;
private final DatagramPacket datagramPacket = new DatagramPacket(new byte[1500], 1500);
public Teamspeak3Socket(SocketRole socketRole, DatagramSocket datagramSocket) {
public LocalTeamspeakSocket(SocketRole socketRole, DatagramSocket datagramSocket) {
this.socketRole = socketRole;
this.datagramSocket = datagramSocket;
}
public Teamspeak3Socket(SocketRole socketRole) throws SocketException {
public LocalTeamspeakSocket(SocketRole socketRole) throws SocketException {
this(socketRole, new DatagramSocket());
}
@@ -36,6 +36,14 @@ public final class Teamspeak3Socket {
return socketRole;
}
public InetSocketAddress getLocalAddress() {
return new InetSocketAddress(datagramSocket.getLocalAddress(), datagramSocket.getLocalPort());
}
public InetSocketAddress getRemoteAddress() {
return new InetSocketAddress(datagramSocket.getInetAddress(), datagramSocket.getPort());
}
public void connect(SocketAddress remote) throws SocketException {
datagramSocket.connect(remote);
}
@@ -116,4 +124,5 @@ public final class Teamspeak3Socket {
return networkPacket;
}
}
@@ -18,8 +18,8 @@ public final class Ts3Logging {
System.err.println("[INFO] " + message);
}
public static void debug(String message) {
System.err.println("[DEBUG] " + message);
public static void debug(Object message) {
System.err.println("[DEBUG] " + message.toString());
}
public static void debug(String message, Throwable ex) {
@@ -1,5 +1,8 @@
package com.github.manevolent.ts3j;
import com.github.manevolent.ts3j.identity.LocalIdentity;
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient;
import junit.framework.TestCase;
import java.net.InetSocketAddress;
@@ -10,15 +13,23 @@ public class ServerConnectionTest extends TestCase {
}
public void testParser() throws Exception {
Teamspeak3Client client = new Teamspeak3Client();
try {
LocalIdentity localIdentity = LocalIdentity.generateNew(8);
client.connect(new InetSocketAddress(
"voice.teamspeak.com",
9987),
null,
10000L
);
LocalTeamspeakClient client = new LocalTeamspeakClient();
assertEquals(client.getClientConnectionState(), ClientConnectionState.CONNECTED);
client.setLocalIdentity(localIdentity);
client.connect(new InetSocketAddress(
"voice.teamspeak.com",
9987),
null,
10000L
);
assertEquals(client.getClientConnectionState(), ClientConnectionState.CONNECTED);
} catch (Throwable ex) {
ex.printStackTrace();
}
}
}