We're now up to the point where we need to work with EdDSA. More to come
This commit is contained in:
@@ -1,20 +0,0 @@
|
|||||||
package com.github.manevolent.ts3j.command;
|
|
||||||
|
|
||||||
import com.github.manevolent.ts3j.command.type.client.*;
|
|
||||||
import com.github.manevolent.ts3j.command.type.server.*;
|
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public final class Commands {
|
|
||||||
private static final Map<String, Class<? extends TypedCommand>> commands = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
static {
|
|
||||||
// Client
|
|
||||||
commands.put("clientinitiv", ClientInitIV.class);
|
|
||||||
|
|
||||||
// Server
|
|
||||||
commands.put("initivexpand", ServerInitIVExpand.class);
|
|
||||||
commands.put("initivexpand2", ServerInitIVExpand2.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,30 @@
|
|||||||
package com.github.manevolent.ts3j.command;
|
package com.github.manevolent.ts3j.command;
|
||||||
|
|
||||||
|
import com.github.manevolent.ts3j.command.part.CommandOption;
|
||||||
import com.github.manevolent.ts3j.command.part.CommandParameter;
|
import com.github.manevolent.ts3j.command.part.CommandParameter;
|
||||||
|
import com.github.manevolent.ts3j.command.part.CommandSingleParameter;
|
||||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
||||||
|
import com.github.manevolent.ts3j.util.Ts3Logging;
|
||||||
|
import com.github.manevolent.ts3j.util.Ts3String;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
public class SimpleCommand implements Command {
|
public class SimpleCommand implements Command {
|
||||||
private final String name;
|
private final String name;
|
||||||
private final ProtocolRole role;
|
private final ProtocolRole role;
|
||||||
private final Collection<CommandParameter> params;
|
private final Map<String, CommandParameter> params;
|
||||||
|
|
||||||
public SimpleCommand(String name, ProtocolRole role, Collection<CommandParameter> params) {
|
public SimpleCommand(String name, ProtocolRole role, CommandParameter... params) {
|
||||||
|
this(name, role, Arrays.asList(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimpleCommand(String name, ProtocolRole role, List<CommandParameter> params) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.role = role;
|
this.role = role;
|
||||||
this.params = params;
|
|
||||||
|
this.params = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
for (CommandParameter parameter : params) add(parameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -29,6 +39,58 @@ public class SimpleCommand implements Command {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Iterable<CommandParameter> getParameters() {
|
public Iterable<CommandParameter> getParameters() {
|
||||||
return Collections.unmodifiableCollection(params);
|
return Collections.unmodifiableCollection(params.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean has(String key) {
|
||||||
|
return params.containsKey(key.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(CommandParameter parameter) {
|
||||||
|
CommandParameter existing = get(parameter.getName());
|
||||||
|
if (existing != null) throw new IllegalArgumentException(parameter.getName() + " already set");
|
||||||
|
|
||||||
|
if (parameter.getName().trim().length() <= 0)
|
||||||
|
throw new IllegalArgumentException("invalid key (empty)");
|
||||||
|
|
||||||
|
this.params.put(parameter.getName().toLowerCase(), parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandParameter get(String key) {
|
||||||
|
return this.params.get(key.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandParameter remove(String key) {
|
||||||
|
return this.params.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SimpleCommand parse(ProtocolRole role, String commandText) {
|
||||||
|
String[] commandParts = commandText.split(" ");
|
||||||
|
|
||||||
|
String commandLabel = commandParts[0].toLowerCase();
|
||||||
|
|
||||||
|
List<CommandParameter> parameterList = new LinkedList<>();
|
||||||
|
|
||||||
|
for (int i = 1; i < commandParts.length; i ++) {
|
||||||
|
String commandParamText = commandParts[i].trim();
|
||||||
|
if (commandParamText.length() <= 0) continue;
|
||||||
|
|
||||||
|
if (commandParamText.startsWith("-")) { // Option
|
||||||
|
parameterList.add(new CommandOption(commandParamText.substring(1)));
|
||||||
|
} else {
|
||||||
|
String[] commandParamParts = commandParamText.split("=", 2);
|
||||||
|
|
||||||
|
Ts3Logging.debug(commandParamText);
|
||||||
|
parameterList.add(new CommandSingleParameter(
|
||||||
|
commandParamParts[0].toLowerCase(),
|
||||||
|
commandParamParts.length > 1 ?
|
||||||
|
Ts3String.unescape(commandParamParts[1]) :
|
||||||
|
null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SimpleCommand(commandLabel, role, parameterList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
package com.github.manevolent.ts3j.command;
|
|
||||||
|
|
||||||
import com.github.manevolent.ts3j.command.part.CommandParameter;
|
|
||||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
|
||||||
import com.github.manevolent.ts3j.protocol.client.TeamspeakClient;
|
|
||||||
import com.github.manevolent.ts3j.protocol.server.TeamspeakServer;
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleServer(TeamspeakServer server) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleClient(TeamspeakClient client) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -58,6 +58,11 @@ public class CommandOption implements CommandParameter {
|
|||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue() {
|
||||||
|
return enabled ? "true" : "false";
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
if (enabled)
|
if (enabled)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.github.manevolent.ts3j.command.part;
|
package com.github.manevolent.ts3j.command.part;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
public interface CommandParameter {
|
public interface CommandParameter {
|
||||||
|
|
||||||
String getName();
|
String getName();
|
||||||
@@ -14,6 +16,12 @@ public interface CommandParameter {
|
|||||||
void set(float value);
|
void set(float value);
|
||||||
void set(double value);
|
void set(double value);
|
||||||
|
|
||||||
|
default void set(byte[] value) {
|
||||||
|
set(Base64.getEncoder().encodeToString(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
String getValue();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the command part to a usable command string element.
|
* Converts the command part to a usable command string element.
|
||||||
* @return Command string element.
|
* @return Command string element.
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ public class CommandSingleParameter implements CommandParameter {
|
|||||||
this.key = key;
|
this.key = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandSingleParameter(String key, String value) {
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
if (value == null)
|
if (value == null)
|
||||||
@@ -67,4 +72,9 @@ public class CommandSingleParameter implements CommandParameter {
|
|||||||
public void set(double value) {
|
public void set(double value) {
|
||||||
this.value = Double.toString(value);
|
this.value = Double.toString(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
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,15 +0,0 @@
|
|||||||
package com.github.manevolent.ts3j.command.type.server;
|
|
||||||
|
|
||||||
import com.github.manevolent.ts3j.command.TypedCommand;
|
|
||||||
import com.github.manevolent.ts3j.command.part.CommandSingleParameter;
|
|
||||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
|
||||||
|
|
||||||
public class ServerInitIVExpand extends TypedCommand {
|
|
||||||
public ServerInitIVExpand() {
|
|
||||||
super(ProtocolRole.CLIENT, "initivexpand");
|
|
||||||
|
|
||||||
set(new CommandSingleParameter("alpha"));
|
|
||||||
set(new CommandSingleParameter("beta"));
|
|
||||||
set(new CommandSingleParameter("omega"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.github.manevolent.ts3j.command.type.server;
|
|
||||||
|
|
||||||
import com.github.manevolent.ts3j.command.TypedCommand;
|
|
||||||
import com.github.manevolent.ts3j.command.part.CommandSingleParameter;
|
|
||||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
|
||||||
|
|
||||||
public class ServerInitIVExpand2 extends TypedCommand {
|
|
||||||
public ServerInitIVExpand2() {
|
|
||||||
super(ProtocolRole.CLIENT, "initivexpand2");
|
|
||||||
|
|
||||||
set(new CommandSingleParameter("l"));
|
|
||||||
set(new CommandSingleParameter("beta"));
|
|
||||||
set(new CommandSingleParameter("omega"));
|
|
||||||
set(new CommandSingleParameter("ot"));
|
|
||||||
set(new CommandSingleParameter("proof"));
|
|
||||||
set(new CommandSingleParameter("tvd"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.github.manevolent.ts3j.identity;
|
package com.github.manevolent.ts3j.identity;
|
||||||
|
|
||||||
|
import com.github.manevolent.ts3j.util.Ts3Crypt;
|
||||||
import com.github.manevolent.ts3j.util.Ts3Logging;
|
import com.github.manevolent.ts3j.util.Ts3Logging;
|
||||||
import org.bouncycastle.asn1.ASN1Encodable;
|
import org.bouncycastle.asn1.ASN1Encodable;
|
||||||
import org.bouncycastle.asn1.DERBitString;
|
import org.bouncycastle.asn1.DERBitString;
|
||||||
@@ -34,7 +35,7 @@ public class Identity {
|
|||||||
public Identity(ECPoint publicKey) {
|
public Identity(ECPoint publicKey) {
|
||||||
this.publicKey = publicKey;
|
this.publicKey = publicKey;
|
||||||
|
|
||||||
this.publicKeyString = Base64.getEncoder().encodeToString(encodePublicKey());
|
this.publicKeyString = Base64.getEncoder().encodeToString(Ts3Crypt.encodePublicKey(publicKey));
|
||||||
this.fingerprint = generateFingerprint();
|
this.fingerprint = generateFingerprint();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,24 +137,9 @@ public class Identity {
|
|||||||
return curr;
|
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() {
|
private String generateFingerprint() {
|
||||||
synchronized (sha1) {
|
synchronized (sha1) {
|
||||||
return Base64.getEncoder().encodeToString(sha1.digest(encodePublicKey()));
|
return Base64.getEncoder().encodeToString(sha1.digest(Ts3Crypt.encodePublicKey(publicKey)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.github.manevolent.ts3j.license;
|
||||||
|
|
||||||
|
import com.github.manevolent.ts3j.enums.LicenseType;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class EphemeralLicenseUse extends LicenseUse {
|
||||||
|
public EphemeralLicenseUse() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSize() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LicenseUseType getUseType() {
|
||||||
|
return LicenseUseType.EPHEMERAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LicenseType getLicenseType() {
|
||||||
|
return LicenseType.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer write(ByteBuffer buffer) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer read(ByteBuffer buffer) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.github.manevolent.ts3j.license;
|
||||||
|
|
||||||
|
import com.github.manevolent.ts3j.enums.LicenseType;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import static com.github.manevolent.ts3j.enums.LicenseType.NO_LICENSE;
|
||||||
|
import static com.github.manevolent.ts3j.enums.LicenseType.UNKNOWN;
|
||||||
|
|
||||||
|
public class IntermediateLicenseUse extends LicenseUse {
|
||||||
|
private byte[] unknown = new byte[4];
|
||||||
|
private String issuer;
|
||||||
|
|
||||||
|
public IntermediateLicenseUse() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSize() {
|
||||||
|
return 4 + issuer.length() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LicenseUseType getUseType() {
|
||||||
|
return LicenseUseType.EPHEMERAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LicenseType getLicenseType() {
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer write(ByteBuffer buffer) {
|
||||||
|
buffer.put(unknown);
|
||||||
|
License.writeNullTerminatedLicenseString(buffer, issuer);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer read(ByteBuffer buffer) {
|
||||||
|
buffer.get(unknown);
|
||||||
|
|
||||||
|
issuer = License.readNullTerminatedLicenseString(buffer);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getUnknown() {
|
||||||
|
return unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnknown(byte[] unknown) {
|
||||||
|
this.unknown = unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIssuer() {
|
||||||
|
return issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIssuer(String issuer) {
|
||||||
|
this.issuer = issuer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package com.github.manevolent.ts3j.license;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class License {
|
||||||
|
public static final byte[] ROOT_KEY =
|
||||||
|
{
|
||||||
|
(byte) 0xcd, 0x0d, (byte) 0xe2, (byte) 0xae, (byte) 0xd4, 0x63, 0x45, 0x50, (byte) 0x9a,
|
||||||
|
0x7e, 0x3c, (byte) 0xfd, (byte) 0x8f, 0x68, (byte) 0xb3, (byte) 0xdc, 0x75, 0x55, (byte) 0xb2,
|
||||||
|
(byte) 0x9d, (byte) 0xcc, (byte) 0xec, 0x73, (byte) 0xcd, 0x18, 0x75, 0x0f, (byte) 0x99,
|
||||||
|
0x38, 0x12, 0x40, (byte) 0x8a
|
||||||
|
};
|
||||||
|
|
||||||
|
private byte[] computedHash;
|
||||||
|
|
||||||
|
private byte[] publicKey = new byte[32];
|
||||||
|
private byte licenseBlockType;
|
||||||
|
private long start,
|
||||||
|
end;
|
||||||
|
|
||||||
|
private LicenseUse use;
|
||||||
|
|
||||||
|
public LicenseUse getUse() {
|
||||||
|
return use;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUse(LicenseUse use) {
|
||||||
|
this.use = use;
|
||||||
|
this.licenseBlockType = (byte) (use.getUseType().getIndex() & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPublicKey() {
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPublicKey(byte[] publicKey) {
|
||||||
|
this.publicKey = publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getLicenseBlockType() {
|
||||||
|
return licenseBlockType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLicenseBlockType(byte licenseBlockType) {
|
||||||
|
this.licenseBlockType = licenseBlockType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getStart() {
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStart(long start) {
|
||||||
|
this.start = start;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getEnd() {
|
||||||
|
return end;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnd(long end) {
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSize() {
|
||||||
|
return 32 + 1 + 8 + use.getSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer write(ByteBuffer buffer) {
|
||||||
|
writeHeader(buffer);
|
||||||
|
writeBody(buffer);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer read(ByteBuffer buffer) {
|
||||||
|
readHeader(buffer);
|
||||||
|
readBody(buffer);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ByteBuffer readHeader(ByteBuffer buffer) {
|
||||||
|
byte keyType = buffer.get();
|
||||||
|
if (keyType != 0x00) throw new IllegalArgumentException("invalid key type: " + keyType);
|
||||||
|
|
||||||
|
buffer.get(publicKey);
|
||||||
|
licenseBlockType = buffer.get();
|
||||||
|
start = buffer.getInt() & 0x000000FFFFFF;
|
||||||
|
end = buffer.getInt() & 0x000000FFFFFF;
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer readBody(ByteBuffer buffer) {
|
||||||
|
LicenseUseType useType = LicenseUseType.fromId(getLicenseBlockType());
|
||||||
|
|
||||||
|
try {
|
||||||
|
use = useType.getUseClass().newInstance();
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
use.read(buffer);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ByteBuffer writeHeader(ByteBuffer buffer) {
|
||||||
|
buffer.put((byte) 0x00);
|
||||||
|
buffer.put(publicKey);
|
||||||
|
buffer.put((byte) (licenseBlockType & 0xFF));
|
||||||
|
buffer.putInt((int) (start & 0x000000FFFFFF));
|
||||||
|
buffer.putInt((int) (end & 0x000000FFFFFF));
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer writeBody(ByteBuffer buffer) {
|
||||||
|
use.write(buffer);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String readNullTerminatedLicenseString(ByteBuffer buffer) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
while (buffer.remaining() > 0) {
|
||||||
|
char c = (char) buffer.get();
|
||||||
|
switch (c) {
|
||||||
|
case '\0':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
builder.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeNullTerminatedLicenseString(ByteBuffer buffer, String s) {
|
||||||
|
buffer.put((s + "\0").getBytes(Charset.forName("ASCII")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<License> readLicenses(ByteBuffer buffer) {
|
||||||
|
List<License> licenses = new ArrayList<>();
|
||||||
|
|
||||||
|
while (buffer.remaining() > 0) {
|
||||||
|
byte licenseVersion = buffer.get();
|
||||||
|
if (licenseVersion != 0x01)
|
||||||
|
throw new IllegalArgumentException("invalid license version");
|
||||||
|
|
||||||
|
License license = new License();
|
||||||
|
license.read(buffer);
|
||||||
|
licenses.add(license);
|
||||||
|
}
|
||||||
|
|
||||||
|
return licenses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getHash() {
|
||||||
|
if (computedHash == null) {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(getSize());
|
||||||
|
write(buffer);
|
||||||
|
|
||||||
|
MessageDigest digest;
|
||||||
|
try {
|
||||||
|
digest = MessageDigest.getInstance("SHA512");
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return digest.digest(buffer.array());
|
||||||
|
}
|
||||||
|
|
||||||
|
return computedHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.github.manevolent.ts3j.license;
|
||||||
|
|
||||||
|
import com.github.manevolent.ts3j.enums.LicenseType;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public abstract class LicenseUse {
|
||||||
|
protected LicenseUse() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract int getSize();
|
||||||
|
|
||||||
|
public abstract LicenseUseType getUseType();
|
||||||
|
|
||||||
|
public abstract LicenseType getLicenseType();
|
||||||
|
|
||||||
|
public abstract ByteBuffer write(ByteBuffer buffer);
|
||||||
|
|
||||||
|
public abstract ByteBuffer read(ByteBuffer buffer);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.github.manevolent.ts3j.license;
|
||||||
|
|
||||||
|
public enum LicenseUseType {
|
||||||
|
INTERMEDIATE(0, IntermediateLicenseUse.class),
|
||||||
|
SERVER(2, ServerLicenseUse.class),
|
||||||
|
EPHEMERAL(32, EphemeralLicenseUse.class)
|
||||||
|
;
|
||||||
|
|
||||||
|
private final int index;
|
||||||
|
private final Class<? extends LicenseUse> use;
|
||||||
|
|
||||||
|
LicenseUseType(int index, Class<? extends LicenseUse> use) {
|
||||||
|
this.index = index;
|
||||||
|
this.use = use;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getIndex() {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LicenseUseType fromId(int index) {
|
||||||
|
for (LicenseUseType value : values())
|
||||||
|
if (value.getIndex() == index) return value;
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("invalid index: " + index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class<? extends LicenseUse> getUseClass() {
|
||||||
|
return use;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.github.manevolent.ts3j.license;
|
||||||
|
|
||||||
|
import com.github.manevolent.ts3j.enums.LicenseType;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class ServerLicenseUse extends LicenseUse {
|
||||||
|
private byte type;
|
||||||
|
private byte[] unknown = new byte[4];
|
||||||
|
private String issuer;
|
||||||
|
|
||||||
|
public ServerLicenseUse() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSize() {
|
||||||
|
return 1 + 4 + issuer.length() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LicenseUseType getUseType() {
|
||||||
|
return LicenseUseType.SERVER;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer write(ByteBuffer buffer) {
|
||||||
|
buffer.put(type);
|
||||||
|
buffer.put(unknown);
|
||||||
|
License.writeNullTerminatedLicenseString(buffer, issuer);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer read(ByteBuffer buffer) {
|
||||||
|
type = buffer.get();
|
||||||
|
buffer.get(unknown);
|
||||||
|
issuer = License.readNullTerminatedLicenseString(buffer);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getUnknown() {
|
||||||
|
return unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnknown(byte[] unknown) {
|
||||||
|
this.unknown = unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIssuer() {
|
||||||
|
return issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIssuer(String issuer) {
|
||||||
|
this.issuer = issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LicenseType getLicenseType() {
|
||||||
|
return LicenseType.fromId(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(byte type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.github.manevolent.ts3j.protocol.packet;
|
package com.github.manevolent.ts3j.protocol.packet;
|
||||||
|
|
||||||
|
import com.github.manevolent.ts3j.command.SimpleCommand;
|
||||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
||||||
import com.github.manevolent.ts3j.util.Ts3Logging;
|
import com.github.manevolent.ts3j.util.Ts3Logging;
|
||||||
|
|
||||||
@@ -17,6 +18,10 @@ public class Packet2Command extends Packet {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SimpleCommand parse() {
|
||||||
|
return SimpleCommand.parse(getRole(), getText());
|
||||||
|
}
|
||||||
|
|
||||||
public void setText(String text) {
|
public void setText(String text) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-8
@@ -1,17 +1,31 @@
|
|||||||
package com.github.manevolent.ts3j.protocol.packet.handler.client;
|
package com.github.manevolent.ts3j.protocol.packet.handler.client;
|
||||||
|
|
||||||
import com.github.manevolent.ts3j.command.type.client.ClientInitIV;
|
import com.github.manevolent.ts3j.command.SimpleCommand;
|
||||||
|
import com.github.manevolent.ts3j.command.part.CommandSingleParameter;
|
||||||
|
import com.github.manevolent.ts3j.license.License;
|
||||||
import com.github.manevolent.ts3j.protocol.NetworkPacket;
|
import com.github.manevolent.ts3j.protocol.NetworkPacket;
|
||||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
||||||
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient;
|
import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient;
|
||||||
import com.github.manevolent.ts3j.protocol.packet.Packet2Command;
|
import com.github.manevolent.ts3j.protocol.packet.Packet2Command;
|
||||||
import com.github.manevolent.ts3j.protocol.packet.Packet8Init1;
|
import com.github.manevolent.ts3j.protocol.packet.Packet8Init1;
|
||||||
|
import com.github.manevolent.ts3j.util.Ts3Crypt;
|
||||||
import com.github.manevolent.ts3j.util.Ts3Logging;
|
import com.github.manevolent.ts3j.util.Ts3Logging;
|
||||||
|
import net.i2p.crypto.eddsa.math.GroupElement;
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
|
||||||
|
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
|
||||||
|
import org.bouncycastle.crypto.Signer;
|
||||||
|
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||||
|
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||||
|
import org.bouncycastle.crypto.signers.DSADigestSigner;
|
||||||
|
import org.bouncycastle.crypto.signers.ECDSASigner;
|
||||||
|
import org.bouncycastle.math.ec.ECPoint;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
public class LocalClientHandlerConnecting extends LocalClientHandler {
|
public class LocalClientHandlerConnecting extends LocalClientHandler {
|
||||||
@@ -118,14 +132,22 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
|
|||||||
alphaBytes = new byte[10];
|
alphaBytes = new byte[10];
|
||||||
new Random().nextBytes(alphaBytes);
|
new Random().nextBytes(alphaBytes);
|
||||||
|
|
||||||
ClientInitIV command = new ClientInitIV();
|
SimpleCommand initiv = new SimpleCommand("clientinitiv", ProtocolRole.CLIENT);
|
||||||
|
|
||||||
command.get("alpha").set(Base64.getEncoder().encodeToString(alphaBytes));
|
initiv.add(new CommandSingleParameter("alpha", 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")));
|
initiv.add(new CommandSingleParameter("omega", getClient().getLocalIdentity().getPublicKeyString()));
|
||||||
|
|
||||||
|
initiv.add(new CommandSingleParameter("ot", "1")); // constant, set to 1
|
||||||
|
|
||||||
|
initiv.add(
|
||||||
|
new CommandSingleParameter(
|
||||||
|
"ip",
|
||||||
|
getClient().getOption("client.hostname", String.class)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
step4.setClientIVcommand(initiv.build().getBytes(Charset.forName("UTF8")));
|
||||||
|
|
||||||
response4.setStep(step4);
|
response4.setStep(step4);
|
||||||
|
|
||||||
@@ -136,7 +158,38 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
|
|||||||
|
|
||||||
}
|
}
|
||||||
} else if (packet.getPacket() instanceof Packet2Command) {
|
} else if (packet.getPacket() instanceof Packet2Command) {
|
||||||
Ts3Logging.debug(((Packet2Command) packet.getPacket()).getText());
|
SimpleCommand command = ((Packet2Command) packet.getPacket()).parse();
|
||||||
|
|
||||||
|
if (command.getName().equalsIgnoreCase("initivexpand2")) {
|
||||||
|
// 3.2.2 initivexpand2 (Client <- Server)
|
||||||
|
|
||||||
|
Ts3Logging.debug("initivexpand2");
|
||||||
|
|
||||||
|
if (!command.get("ot").getValue().equals("1"))
|
||||||
|
throw new IllegalArgumentException("ot constant != 1: " + command.get("ot").getValue());
|
||||||
|
|
||||||
|
byte[] license = Base64.getDecoder().decode(command.get("l").getValue());
|
||||||
|
byte[] licsense_validation = command.get("tvd").getValue() == null ?
|
||||||
|
null :
|
||||||
|
Base64.getDecoder().decode(command.get("tvd").getValue());
|
||||||
|
|
||||||
|
byte[] random = Base64.getDecoder().decode(command.get("beta").getValue()); // beta
|
||||||
|
byte[] publicKeyBytes = Base64.getDecoder().decode(command.get("omega").getValue()); // omega
|
||||||
|
byte[] proof = Base64.getDecoder().decode(command.get("proof").getValue()); // ecdh_sign(1)
|
||||||
|
|
||||||
|
// 3.2.1.1 Verify integrity
|
||||||
|
// The proof parameter is the sign of the l parameter (not base64 encoded). The client can verify the l
|
||||||
|
// parameter with the public key of the server which is sent in omega.
|
||||||
|
|
||||||
|
ECPoint publicKey = Ts3Crypt.decodePublicKey(publicKeyBytes);
|
||||||
|
|
||||||
|
if (!Ts3Crypt.verifySignature(publicKey, license, proof))
|
||||||
|
throw new SecurityException("invalid proof signature: " + Ts3Logging.getHex(proof));
|
||||||
|
|
||||||
|
// 3.2.2.2 Parsing the license
|
||||||
|
List<License> licenses = License.readLicenses(ByteBuffer.wrap(license));
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,10 +60,11 @@ public class LocalTeamspeakSocket extends AbstractTeamspeakSocket implements Tea
|
|||||||
packet.setSocketAddress(datagramSocket.getRemoteSocketAddress());
|
packet.setSocketAddress(datagramSocket.getRemoteSocketAddress());
|
||||||
|
|
||||||
Ts3Logging.debug(
|
Ts3Logging.debug(
|
||||||
"[NETWORK] WRITE Len=" + packet.getLength() + " to " + packet.getSocketAddress() +
|
"[NETWORK] WRITE Len=" + packet.getLength() + " to " + packet.getSocketAddress()
|
||||||
"\n" + Ts3Logging.getHex(packet.getData(), packet.getLength())
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//Ts3Logging.debug(Ts3Logging.getHex(packet.getData(), packet.getLength()));
|
||||||
|
|
||||||
datagramSocket.send(packet);
|
datagramSocket.send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,10 +117,11 @@ public class LocalTeamspeakSocket extends AbstractTeamspeakSocket implements Tea
|
|||||||
datagramSocket.receive(datagramPacket);
|
datagramSocket.receive(datagramPacket);
|
||||||
|
|
||||||
Ts3Logging.debug(
|
Ts3Logging.debug(
|
||||||
"[NETWORK] READ Len=" + datagramPacket.getLength() + " from " + datagramPacket.getSocketAddress() +
|
"[NETWORK] READ Len=" + datagramPacket.getLength() + " from " + datagramPacket.getSocketAddress()
|
||||||
"\n" + Ts3Logging.getHex(datagramPacket.getData(), datagramPacket.getLength())
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//Ts3Logging.debug(Ts3Logging.getHex(datagramPacket.getData(), datagramPacket.getLength()));
|
||||||
|
|
||||||
return datagramPacket;
|
return datagramPacket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.github.manevolent.ts3j.util;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.*;
|
||||||
|
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||||
|
import org.bouncycastle.crypto.params.ECDomainParameters;
|
||||||
|
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
|
||||||
|
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||||
|
import org.bouncycastle.crypto.signers.DSADigestSigner;
|
||||||
|
import org.bouncycastle.crypto.signers.ECDSASigner;
|
||||||
|
import org.bouncycastle.jce.ECNamedCurveTable;
|
||||||
|
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
|
||||||
|
import org.bouncycastle.math.ec.ECPoint;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class Ts3Crypt {
|
||||||
|
private static ECDomainParameters getDomainParameters() {
|
||||||
|
ECNamedCurveParameterSpec ecp = ECNamedCurveTable.getParameterSpec("prime256v1");
|
||||||
|
return new ECDomainParameters(ecp.getCurve(), ecp.getG(), ecp.getN(), ecp.getH(), ecp.getSeed());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean verifySignature(ECPoint publicKey, byte[] data, byte[] signature) {
|
||||||
|
DSADigestSigner signer = new DSADigestSigner(new ECDSASigner(), new SHA256Digest());
|
||||||
|
ECPublicKeyParameters signingKey = new ECPublicKeyParameters(publicKey, getDomainParameters());
|
||||||
|
|
||||||
|
signer.init(false, signingKey);
|
||||||
|
signer.update(data, 0, data.length);
|
||||||
|
|
||||||
|
return signer.verifySignature(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From http://stackoverflow.com/questions/2409618/how-do-i-decode-a-der-encoded-string-in-java
|
||||||
|
*/
|
||||||
|
private static ASN1Encodable[] readDERObject(byte[] data) throws IOException
|
||||||
|
{
|
||||||
|
ByteArrayInputStream inStream = new ByteArrayInputStream(data);
|
||||||
|
ASN1InputStream asnInputStream = new ASN1InputStream(inStream);
|
||||||
|
|
||||||
|
ASN1Object object = asnInputStream.readObject();
|
||||||
|
|
||||||
|
if (object instanceof DLSequence) {
|
||||||
|
return ((DLSequence) object).toArray();
|
||||||
|
} else {
|
||||||
|
return new ASN1Encodable[] { object };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ECPoint decodePublicKey(byte[] bytes) {
|
||||||
|
try {
|
||||||
|
DERSequence asnKeyData = new DERSequence(readDERObject(bytes));
|
||||||
|
|
||||||
|
BigInteger x = ((org.bouncycastle.asn1.ASN1Integer)asnKeyData.getObjectAt(2)).getValue();
|
||||||
|
BigInteger y = ((org.bouncycastle.asn1.ASN1Integer)asnKeyData.getObjectAt(3)).getValue();
|
||||||
|
|
||||||
|
return getDomainParameters().getCurve().createPoint(x, y);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] encodePublicKey(ECPoint publicKey) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ public final class Ts3String {
|
|||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("invalid escape code: " + c);
|
throw new IllegalArgumentException("invalid escape code: " + c);
|
||||||
}
|
}
|
||||||
}
|
} else sb.append(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class ServerConnectionTest extends TestCase {
|
|||||||
client.setLocalIdentity(localIdentity);
|
client.setLocalIdentity(localIdentity);
|
||||||
|
|
||||||
client.connect(new InetSocketAddress(
|
client.connect(new InetSocketAddress(
|
||||||
"voice.teamspeak.com",
|
"ts.teamlixo.net",
|
||||||
9987),
|
9987),
|
||||||
null,
|
null,
|
||||||
10000L
|
10000L
|
||||||
|
|||||||
Reference in New Issue
Block a user