Report a chosen client version and operating system
Other clients see the version and platform from clientinit in the info panel, and until now that was whatever ts3j sends. The new Client Version tab picks one instead. Servers refuse a version they do not know with "client is modified", so free text is not enough: the list holds genuine version/platform/signature triples from released clients, newest first per platform. The fields stay editable for a version whose signature is known from elsewhere, and a refused connection now says where to change it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
package com.ts3client.config;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The version, platform and signature a client reports to the server in
|
||||
* {@code clientinit}. Other clients see the version and platform in the info
|
||||
* panel ("3.6.2 on Windows").
|
||||
*
|
||||
* <p>The signature is TeamSpeak's, over the version and platform strings
|
||||
* together, so the three only work as a set. Servers answer "client is modified"
|
||||
* for a version they do not accept — an invented one, or one older than their
|
||||
* configured minimum — which is why {@link #PRESETS} holds genuine triples taken
|
||||
* from released clients, newest first per platform.
|
||||
*
|
||||
* @param label human-readable name, e.g. "3.6.2 on Windows"
|
||||
* @param version {@code client_version}, e.g. "3.6.2 [Build: 1695203293]"
|
||||
* @param platform {@code client_platform}, e.g. "Windows"
|
||||
* @param sign {@code client_version_sign}, base64
|
||||
*/
|
||||
public record ClientVersion(String label, String version, String platform, String sign) {
|
||||
|
||||
/** Platform names the official clients report. */
|
||||
public static final List<String> PLATFORMS =
|
||||
List.of("Windows", "Linux", "OS X", "Android", "iOS");
|
||||
|
||||
/**
|
||||
* Signed triples from released TeamSpeak clients. "3.?.?" is the version the
|
||||
* SDK-based clients report; it is signed for every platform and is what this
|
||||
* client uses by default.
|
||||
*/
|
||||
public static final List<ClientVersion> PRESETS = List.of(
|
||||
new ClientVersion("3.?.? on Windows", "3.?.? [Build: 5680278000]", "Windows",
|
||||
"DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg=="),
|
||||
new ClientVersion("3.6.3 on Windows", "3.6.3 [Build: 1701166057]", "Windows",
|
||||
"ML3yE6pV0QGyhc8o7k1bgqDSj+5gZIQFMjBNhWJ6L0FtJwniBq8l1wivx3DzgVVCvyRbzfg8MsdV/rjl5sHNCw=="),
|
||||
new ClientVersion("3.6.2 on Windows", "3.6.2 [Build: 1695203293]", "Windows",
|
||||
"4BdaZpdgUSMCuIs8qcloJPNxNlJ4o7QKnxMCRO60mSOTtJZyKjOrGLAmeAEtLIJjcjmdSpycMbQOIV92K2vXAw=="),
|
||||
new ClientVersion("3.6.1 on Windows", "3.6.1 [Build: 1690872913]", "Windows",
|
||||
"+YmPaYUBL4F9+TgxyFhlT8qIi1Ym+So4NNMcuohla/u4kfJCGabZ1vDF4cIOAghIqWfO+bQ0OkORZrCBdpi2Cw=="),
|
||||
new ClientVersion("3.6.0 on Windows", "3.6.0 [Build: 1686749764]", "Windows",
|
||||
"gfzG6CfwNYeqrjT68s7ahPd24A/XS0P8LLAB7hrE54MnrlO12RiflD2w69VZ7EYlqnd9GAiVzdGJHCmCFWtlAQ=="),
|
||||
|
||||
new ClientVersion("3.?.? on Linux", "3.?.? [Build: 5680278000]", "Linux",
|
||||
"Hjd+N58Gv3ENhoKmGYy2bNRBsNNgm5kpiaQWxOj5HN2DXttG6REjymSwJtpJ8muC2gSwRuZi0R+8Laan5ts5CQ=="),
|
||||
new ClientVersion("3.6.2 on Linux", "3.6.2 [Build: 1695203293]", "Linux",
|
||||
"p4iF1jZ3ZOz9MEkKSZ2bvnFtm9WmUcQy9mAP//erFE4PF1sB6K1CSANrr+3X4B0aZR0u+K2pjnv8kiKsWKQCBQ=="),
|
||||
new ClientVersion("3.6.1 on Linux", "3.6.1 [Build: 1690193193]", "Linux",
|
||||
"UAcE5h5FgPcdChWTtiO816HX8f2qdoDcDEw6crbJ96Yo7rp1Bt+qx/rkKK9bzlVWMhOd4fI7pr++1XaPkEBnAQ=="),
|
||||
new ClientVersion("3.6.0 on Linux", "3.6.0 [Build: 1686749764]", "Linux",
|
||||
"TccRBHwn5LSA0lfpa2B42QmfCOiG+MqqyhlOqy5RcuRQNUGPD0e47lUz+Z/8+Gsdc1TPG4+vYlRl8+v2v+26Aw=="),
|
||||
new ClientVersion("3.5.8 on Linux", "3.5.8 [Build: 1644595825]", "Linux",
|
||||
"CK+cWJQESZgUdw1AFHINMbwiLjFGSiupour0Dfj/j1/xwx4L3nW3G+alYtiQw/wQwlWm873HS6PvXUBarz/tCw=="),
|
||||
|
||||
new ClientVersion("3.?.? on OS X", "3.?.? [Build: 5680278000]", "OS X",
|
||||
"SttEnjoWE8jqIM6BOHSfiZP9DGjW0EP/ajU4bdKqgGMV4aYq/kzwVA9gxbmdIzV4lbaokvXBqrRjfBHrTVh8Cg=="),
|
||||
new ClientVersion("3.6.2 on OS X", "3.6.2 [Build: 1695203293]", "OS X",
|
||||
"SOCkLcsxINIGVc+3OQ5Vrmv2BVNNFA9LX5Jj9tqbIiYv2HU6Z5E8AL5+v5G2LDN0F+U55+uW43ezUoYAr1WuBA=="),
|
||||
new ClientVersion("3.6.1 on OS X", "3.6.1 [Build: 1690193193]", "OS X",
|
||||
"P+KOw42V8K0nN5FbGItCaJzrDuWcRgfE3MRzrtbYAhRMTXHzRxvwLkoLM7+vKTW135NtWZlKbZuDAvxgwhSLBg=="),
|
||||
new ClientVersion("3.6.0 on OS X", "3.6.0 [Build: 1686749764]", "OS X",
|
||||
"g8H7Op8eCrn0wHutuD/SlmUcZPmkSlFUBhvZpSgm9kuQl4dapzKJq2anI5jDwRs/ooBm/wrIlSiWI6HXuOPZDA=="),
|
||||
|
||||
new ClientVersion("3.?.? on Android", "3.?.? [Build: 5680278000]", "Android",
|
||||
"AWb948BY32Z7bpIyoAlQguSmxOGcmjESPceQe1DpW5IZ4+AW1KfTk2VUIYNfUPsxReDJMCtlhVKslzhR2lf0AA=="),
|
||||
new ClientVersion("3.8.7 on Android", "3.8.7 [Build: 1785186501]", "Android",
|
||||
"mbt0WPhXJ+YKJUix5b6t+ZOHTCfvRqhhhrpcS5koM9CUL0BAFQZRG5Di5rJrqipZdk+gEUtpAX9I5nkuL7V9Dw=="),
|
||||
new ClientVersion("3.8.6 on Android", "3.8.6 [Build: 1774892004]", "Android",
|
||||
"Hh0oEZupsq8f0BuUHhMKdkXsMmgZZ03dJL8qAwnlH2JAHuefaAZFS/buiH2MLwgdRkZ81ffYk5YOTfc/bJczDw=="),
|
||||
|
||||
new ClientVersion("3.?.? on iOS", "3.?.? [Build: 5680278000]", "iOS",
|
||||
"XrAf+Buq6Eb0ehEW/niFp06YX+nGGOS0Ke4MoUBzn+cX9q6G5C0A/d5XtgcNMe8r9jJgV/adIYVpsGS3pVlSAA=="),
|
||||
new ClientVersion("3.8.2 on iOS", "3.8.2 [Build: 1764028963]", "iOS",
|
||||
"Bc57xjumaioWkouTRqoS6XbSIXysrPH+gxynXgL4/nYr38l4OocUTpQaMMHevmgk/FZxJWFNEhU3Hpg5+M+MAA=="),
|
||||
new ClientVersion("3.8.1 on iOS", "3.8.1 [Build: 1763403218]", "iOS",
|
||||
"GM/7isPK6LDKTN3Hdelenbr99dnPyWTccLCOCFKK41321XMsdG02putu+NskCLddlDPu2CPYJ4YABRS2pJ88Ag=="));
|
||||
|
||||
/** The triple used when the user has not chosen one; matches the library's built-in. */
|
||||
public static final ClientVersion DEFAULT = PRESETS.get(0);
|
||||
|
||||
/** The preset carrying exactly these three values, or null when the user typed their own. */
|
||||
public static ClientVersion match(String version, String platform, String sign) {
|
||||
return PRESETS.stream()
|
||||
.filter(v -> v.version.equals(version) && v.platform.equals(platform) && v.sign.equals(sign))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,16 @@ public final class Settings {
|
||||
/** Id of the identity used when a server doesn't select one of its own. */
|
||||
public String defaultIdentityId = "";
|
||||
|
||||
// ---- reported client version ----
|
||||
/** Report {@link #clientVersion}/{@link #clientPlatform} instead of the built-in ones. */
|
||||
public boolean customVersion = false;
|
||||
/** Version string other clients see, e.g. "3.5.6 [Build: 1603963200]". */
|
||||
public String clientVersion = ClientVersion.DEFAULT.version();
|
||||
/** Operating system other clients see, e.g. "Windows". */
|
||||
public String clientPlatform = ClientVersion.DEFAULT.platform();
|
||||
/** TeamSpeak's signature over the version and platform; servers may verify it. */
|
||||
public String clientVersionSign = ClientVersion.DEFAULT.sign();
|
||||
|
||||
// ---- audio devices (mixer names; empty = system default) ----
|
||||
public String inputDevice = "";
|
||||
public String outputDevice = "";
|
||||
@@ -152,6 +162,10 @@ public final class Settings {
|
||||
serverPassword = props.getProperty("serverPassword", serverPassword);
|
||||
identityFile = props.getProperty("identityFile", identityFile);
|
||||
defaultIdentityId = props.getProperty("defaultIdentityId", defaultIdentityId);
|
||||
customVersion = parseB(props.getProperty("customVersion"), customVersion);
|
||||
clientVersion = props.getProperty("clientVersion", clientVersion);
|
||||
clientPlatform = props.getProperty("clientPlatform", clientPlatform);
|
||||
clientVersionSign = props.getProperty("clientVersionSign", clientVersionSign);
|
||||
inputDevice = props.getProperty("inputDevice", inputDevice);
|
||||
outputDevice = props.getProperty("outputDevice", outputDevice);
|
||||
inputMode = parseMode(props.getProperty("inputMode"), inputMode);
|
||||
@@ -186,6 +200,10 @@ public final class Settings {
|
||||
props.setProperty("serverPassword", serverPassword);
|
||||
props.setProperty("identityFile", identityFile);
|
||||
props.setProperty("defaultIdentityId", defaultIdentityId);
|
||||
props.setProperty("customVersion", Boolean.toString(customVersion));
|
||||
props.setProperty("clientVersion", clientVersion);
|
||||
props.setProperty("clientPlatform", clientPlatform);
|
||||
props.setProperty("clientVersionSign", clientVersionSign);
|
||||
props.setProperty("inputDevice", inputDevice);
|
||||
props.setProperty("outputDevice", outputDevice);
|
||||
props.setProperty("inputMode", inputMode.name());
|
||||
|
||||
@@ -226,6 +226,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
ui.onError("Network error: " + rootMessage(t));
|
||||
});
|
||||
|
||||
applyClientVersion();
|
||||
applyDefaultChannel(channel, channelPassword);
|
||||
|
||||
ui.onStatus("Connecting to " + address + ":" + port + "…");
|
||||
@@ -256,7 +257,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
applyMicrophoneState();
|
||||
} catch (Exception e) {
|
||||
connected = false;
|
||||
ui.onError("Connection failed: " + rootMessage(e));
|
||||
ui.onError("Connection failed: " + rootMessage(e) + versionHint(e));
|
||||
safeCleanup();
|
||||
// Report the failed attempt as a disconnect too, so the UI leaves the
|
||||
// "connecting" state and the tab becomes reusable.
|
||||
@@ -265,6 +266,38 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "client is modified" is what a server answers when it does not accept the reported
|
||||
* version: too old for its minimum, not a version it knows for that platform, or a
|
||||
* signature that does not belong to the two. Easy to run into after editing them by
|
||||
* hand, and the bare message does not point anywhere.
|
||||
*/
|
||||
private String versionHint(Exception e) {
|
||||
if (!settings.customVersion) return "";
|
||||
String message = rootMessage(e);
|
||||
return message != null && message.contains("client is modified")
|
||||
? " — the server did not accept the custom client version; pick a newer one"
|
||||
+ " of the listed versions in Options."
|
||||
: "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the version and operating system the user configured, which is what
|
||||
* other clients see in the info panel. Each field falls back to the library's
|
||||
* built-in one when left empty; the signature has to match the version and
|
||||
* platform it was issued for, or a server may refuse the connection.
|
||||
*/
|
||||
private void applyClientVersion() {
|
||||
if (!settings.customVersion) return;
|
||||
setIfPresent("client.version_string", settings.clientVersion);
|
||||
setIfPresent("client.version_platform", settings.clientPlatform);
|
||||
setIfPresent("client.version_sign", settings.clientVersionSign);
|
||||
}
|
||||
|
||||
private void setIfPresent(String option, String value) {
|
||||
if (value != null && !value.isBlank()) client.setOption(option, value.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the server, as part of {@code clientinit}, to place us in a specific
|
||||
* channel right away instead of the default one. This is the path a real client
|
||||
|
||||
Reference in New Issue
Block a user