diff --git a/src/ts-protocol/client.ts b/src/ts-protocol/client.ts index acdc859..2436480 100644 --- a/src/ts-protocol/client.ts +++ b/src/ts-protocol/client.ts @@ -10,7 +10,6 @@ import { type Identity, type TextMessage, type ClientInfo, - type CommandMiddleware, } from "@honeybbq/teamspeak-client"; import type { Logger } from "../logger.js"; import { @@ -18,7 +17,7 @@ import { type ServerProtocol, } from "./protocol-detect.js"; import { TS6HttpQuery } from "./http-query.js"; -import { ts6VersionMiddleware } from "./ts6-compat.js"; +import { patchClientInitVersion } from "./ts6-compat.js"; export { CODEC_OPUS_MUSIC } from "./voice.js"; export type { ServerProtocol } from "./protocol-detect.js"; @@ -154,13 +153,6 @@ export class TS3Client extends EventEmitter { this.logger.warn(msg); }; - // Apply TS6 version middleware if connecting to a TS6 server - const commandMiddleware: CommandMiddleware[] = []; - if (this.detectedProtocol === "ts6") { - commandMiddleware.push(ts6VersionMiddleware("3.6.2")); - this.logger.info("Applying TS6 compatibility: upgrading client_version to 3.6.2"); - } - this.client = new TS3FullClient(this.identity, addr, this.options.nickname, { logger: { debug: (msg) => this.logger.debug(msg), @@ -168,9 +160,30 @@ export class TS3Client extends EventEmitter { warn: throttledWarn, error: (msg) => this.logger.error(msg), }, - commandMiddleware, }); + // Patch handler.sendPacket to intercept clientinit during handshake for TS6. + // The library's commandMiddleware only applies to post-connection commands + // (sendCommandNoWait/execCommand), but the handshake's clientinit is sent + // directly via handler.sendPacket, bypassing middleware entirely. + // This monkey-patch ensures the version is upgraded for TS6 servers. + if (this.detectedProtocol === "ts6") { + this.logger.info("Applying TS6 compatibility: upgrading client_version to 3.6.2"); + const origSendPacket = this.client.handler.sendPacket.bind(this.client.handler); + this.client.handler.sendPacket = (pType: number, data: Uint8Array, flags: number) => { + // PacketType.Command = 2 + if (pType === 2) { + const str = Buffer.from(data).toString("utf-8"); + if (str.startsWith("clientinit ")) { + const patched = patchClientInitVersion(str); + origSendPacket(pType, Buffer.from(patched), flags); + return; + } + } + origSendPacket(pType, data, flags); + }; + } + this.client.on("textMessage", (msg: TextMessage) => { const tsMsg: TS3TextMessage = { invokerName: msg.invokerName, diff --git a/src/ts-protocol/ts6-compat.test.ts b/src/ts-protocol/ts6-compat.test.ts index fd61412..3a6211c 100644 --- a/src/ts-protocol/ts6-compat.test.ts +++ b/src/ts-protocol/ts6-compat.test.ts @@ -1,45 +1,22 @@ import { describe, it, expect } from "vitest"; -import { ts6VersionMiddleware } from "./ts6-compat.js"; +import { patchClientInitVersion } from "./ts6-compat.js"; describe("ts6-compat", () => { - it("patches clientinit version to 3.6.2", async () => { - const middleware = ts6VersionMiddleware("3.6.2"); - let captured = ""; - const next = async (cmd: string) => { - captured = cmd; - }; - const handler = middleware(next); - + it("patches clientinit version to 3.6.2", () => { const original = "clientinit client_nickname=Bot client_version=3.5.3\\s[Build:\\s1587971024] " + "client_platform=Windows client_version_sign=Kvmj7qX6wJCPI5GVT71samfmhz/bvs7M+OTXWB/JWxdQbxDe17xda7dzUWLX7pjvdJTqZmbse1HBmTxThPKvAg== " + "client_key_offset=42 hwid=abc123"; - await handler(original); + const patched = patchClientInitVersion(original); - expect(captured).toContain("client_version=3.6.2\\s[Build:\\s1695203293]"); - expect(captured).toContain( + expect(patched).toContain("client_version=3.6.2\\s[Build:\\s1695203293]"); + expect(patched).toContain( "client_version_sign=4OH3unxjGNBYS5EN4RFNrEo3UJz2Jn5KW1JqMDh3Yy93mSd0IwPm3FBrv8hCJgLuv99y6yBSN7pOmOpFjDaCw==" ); // Other fields preserved - expect(captured).toContain("client_nickname=Bot"); - expect(captured).toContain("client_key_offset=42"); - expect(captured).toContain("hwid=abc123"); - }); - - it("does not modify non-clientinit commands", async () => { - const middleware = ts6VersionMiddleware("3.6.2"); - let captured = ""; - const handler = middleware(async (cmd) => { - captured = cmd; - }); - - const original = "sendtextmessage targetmode=2 msg=hello"; - await handler(original); - expect(captured).toBe(original); - }); - - it("throws for unknown target version", () => { - expect(() => ts6VersionMiddleware("9.9.9")).toThrow("Unknown version"); + expect(patched).toContain("client_nickname=Bot"); + expect(patched).toContain("client_key_offset=42"); + expect(patched).toContain("hwid=abc123"); }); }); diff --git a/src/ts-protocol/ts6-compat.ts b/src/ts-protocol/ts6-compat.ts index 5d24f09..4ff8bd9 100644 --- a/src/ts-protocol/ts6-compat.ts +++ b/src/ts-protocol/ts6-compat.ts @@ -1,57 +1,29 @@ -import type { CommandMiddleware } from "@honeybbq/teamspeak-client"; - /** - * Known-compatible client versions with their signatures. - * - * The version_sign is a P-256 ECDSA signature over the version string, - * verified by TeamSpeak servers. Only officially signed versions work. + * TS6 version used for compatibility. The version_sign is a P-256 ECDSA + * signature verified by TeamSpeak servers; only officially signed pairs work. */ -const KNOWN_VERSIONS: Record< - string, - { version: string; platform: string; sign: string } -> = { - // TS3 client 3.5.3 (default in @honeybbq/teamspeak-client) - "3.5.3": { - version: "3.5.3 [Build: 1587971024]", - platform: "Windows", - sign: "Kvmj7qX6wJCPI5GVT71samfmhz/bvs7M+OTXWB/JWxdQbxDe17xda7dzUWLX7pjvdJTqZmbse1HBmTxThPKvAg==", - }, - // TS3 client 3.6.2 (used by NeteaseTSBot, known to work with TS6 servers) - "3.6.2": { - version: "3.6.2 [Build: 1695203293]", - platform: "Windows", - sign: "4OH3unxjGNBYS5EN4RFNrEo3UJz2Jn5KW1JqMDh3Yy93mSd0IwPm3FBrv8hCJgLuv99y6yBSN7pOmOpFjDaCw==", - }, +const TS6_VERSION = { + version: "3.6.2 [Build: 1695203293]", + platform: "Windows", + sign: "4OH3unxjGNBYS5EN4RFNrEo3UJz2Jn5KW1JqMDh3Yy93mSd0IwPm3FBrv8hCJgLuv99y6yBSN7pOmOpFjDaCw==", }; /** - * Command middleware that upgrades the client_version in `clientinit` to - * a newer version known to work with TS6 servers. + * Patch a raw `clientinit` command string to use the 3.6.2 version. * - * TS6 servers may reject connections from older client versions. The default - * library sends version 3.5.3; this middleware patches it to 3.6.2 which is - * the version NeteaseTSBot uses successfully with TS6. + * The @honeybbq/teamspeak-client library hardcodes version 3.5.3 in the + * handshake and sends it directly via handler.sendPacket(), bypassing the + * commandMiddleware chain. TS6 servers silently reject 3.5.3 (they never + * respond with `initserver`), causing an idle timeout. * - * The version_sign must match the version string — it's a server-verified - * ECDSA signature, so we can only use known-valid pairs. + * This function is called from a monkey-patched handler.sendPacket() so it + * intercepts the actual handshake packets. */ -export function ts6VersionMiddleware( - targetVersion = "3.6.2", -): CommandMiddleware { - const vInfo = KNOWN_VERSIONS[targetVersion]; - if (!vInfo) { - throw new Error(`Unknown version ${targetVersion}. Known: ${Object.keys(KNOWN_VERSIONS).join(", ")}`); - } - - return (next) => async (cmd) => { - // Only intercept clientinit commands - if (cmd.startsWith("clientinit ")) { - cmd = replaceField(cmd, "client_version", vInfo.version); - cmd = replaceField(cmd, "client_platform", vInfo.platform); - cmd = replaceField(cmd, "client_version_sign", vInfo.sign); - } - return next(cmd); - }; +export function patchClientInitVersion(cmd: string): string { + cmd = replaceField(cmd, "client_version", TS6_VERSION.version); + cmd = replaceField(cmd, "client_platform", TS6_VERSION.platform); + cmd = replaceField(cmd, "client_version_sign", TS6_VERSION.sign); + return cmd; } /**