All checks were successful
Testar e publicar imagem Docker / publish (push) Successful in 44s
187 lines
5.7 KiB
JavaScript
187 lines
5.7 KiB
JavaScript
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);
|
|
});
|