import { test } from "node:test"; import assert from "node:assert/strict"; import { generateKeyPairSync } from "node:crypto"; import ssh2 from "ssh2"; import { createEventListener, parseQuery, escapeQuery, unescapeQuery, } from "./query-events.mjs"; test("Query escaping preserves pipes, newlines, Unicode and backslashes", () => { const text = "Olá Pessoa | texto\\p\nlinha"; assert.equal(unescapeQuery(escapeQuery(text)), text); assert.deepEqual( parseQuery( "clid=1 client_nickname=Daniel\\sCastro|clid=2 client_nickname=A\\pB", ), [ { clid: "1", client_nickname: "Daniel Castro" }, { clid: "2", client_nickname: "A|B" }, ], ); }); test("SSH listener authenticates, registers notifications and parses fragmented events", async () => { const privateKey = generateKeyPairSync("rsa", { modulusLength: 2048, }).privateKey.export({ type: "pkcs1", format: "pem" }); const commands = [], events = [], states = [], clients = new Set(); let channel, listener; const server = new ssh2.Server({ hostKeys: [privateKey] }, (client) => { clients.add(client); client.on("error", () => {}); client.on("close", () => clients.delete(client)); client.on("authentication", (ctx) => ctx.method === "password" && ctx.username === "query" && ctx.password === "ssh-secret" ? ctx.accept() : ctx.reject(), ); client.on("ready", () => client.on("session", (accept) => { const session = accept(); session.on("shell", (accept) => { channel = accept(); channel.write( "TS3\nWelcome to the TeamSpeak ServerQuery interface.\n", ); let buffer = ""; channel.on("data", (data) => { buffer += data.toString(); let index; while ((index = buffer.indexOf("\n")) >= 0) { const command = buffer.slice(0, index).trim(); buffer = buffer.slice(index + 1); commands.push(command); if (command === "whoami") channel.write("client_id=99 client_channel_id=1\n"); channel.write("error id=0 msg=ok\n"); } }); }); }), ); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { let connectedResolve; const connected = new Promise((resolve) => (connectedResolve = resolve)); listener = createEventListener( { url: "http://localhost:10080", serverId: 1, sshHost: "127.0.0.1", sshPort: server.address().port, sshUsername: "query", sshPassword: "ssh-secret", }, (name, data) => events.push({ name, data }), (state) => { states.push(state); if (state.status === "connected") connectedResolve(); }, ); await Promise.race([ connected, new Promise((_, reject) => { const timer = setTimeout( () => reject(new Error("SSH test timed out")), 8000, ); timer.unref(); }), ]); assert.equal(listener.ready, true); assert.match(states.at(-1).fingerprint, /^[a-f0-9]{64}$/); assert.ok(commands.includes("use sid=1")); assert.ok(commands.includes("servernotifyregister event=server")); assert.ok(commands.includes("servernotifyregister event=channel id=0")); assert.ok(commands.includes("servernotifyregister event=textprivate")); channel.write("notifycliententerview clid=4 client_nick"); await new Promise((r) => setTimeout(r, 10)); channel.write("name=Ol"); channel.write( "á\\sPessoa client_type=0\nnotifytextmessage invokerid=4 msg=!info\\steste\n", ); for (let i = 0; i < 50 && events.length < 2; i++) await new Promise((r) => setTimeout(r, 10)); assert.equal(events.length, 2); assert.equal(events[0].data.client_nickname, "Olá Pessoa"); assert.equal(events[1].data.msg, "!info teste"); assert.equal(events[1].data.__listenerChannel, "0"); listener.close(); listener = null; let readyResolve; const ready = new Promise((resolve) => (readyResolve = resolve)); listener = createEventListener( { url: "http://localhost:10080", serverId: 1, sshHost: "127.0.0.1", sshPort: server.address().port, sshUsername: "query", sshPassword: "ssh-secret", }, () => {}, (state) => { if (state.status === "connected") readyResolve(); }, { channelId: 21 }, ); await Promise.race([ ready, new Promise((_, reject) => { const timer = setTimeout( () => reject(new Error("Channel SSH test timed out")), 8000, ); timer.unref(); }), ]); assert.ok(commands.includes("clientmove clid=99 cid=21")); assert.ok(commands.includes("servernotifyregister event=textchannel")); } finally { listener?.close(); for (const client of clients) client.end(); await new Promise((resolve) => server.close(resolve)); } }); test("SSH listener rejects a changed fingerprint and exposes the error", async () => { let options, destroyed = false; class FakeClient { on() { return this; } connect(value) { options = value; } destroy() { destroyed = true; } } const states = [], listener = createEventListener( { url: "http://localhost", sshUsername: "query", sshPassword: "test", sshFingerprint: "a".repeat(64), }, () => {}, (state) => states.push(state), { Client: FakeClient }, ); assert.equal(options.hostVerifier("b".repeat(64)), false); assert.match(states.at(-1).error, /mudou/); listener.close(); assert.equal(destroyed, true); });