Skip to content

Commit

Permalink
Add sasl, sasl2 and fast e2e tests (#1044)
Browse files Browse the repository at this point in the history
  • Loading branch information
sonnyp authored Jan 6, 2025
1 parent ff61892 commit b835091
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 39 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ If you want to disable either for some reason, pass `--no-verify` to `git push`

At that point you can make changes to the xmpp.js code and run tests with

```
```sh
make test
```

Expand All @@ -31,7 +31,7 @@ See [Jest CLI](https://jestjs.io/docs/cli).
When submitting a pull request, additional tests will be run on GitHub actions.
In most cases it shouldn't be necessary but if they fail, you can run them locally after installing prosody >= 0.12 with

```
```sh
make ci
```

Expand Down
6 changes: 3 additions & 3 deletions packages/client-core/src/bind2/bind2.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import xml from "@xmpp/xml";

const NS_BIND = "urn:xmpp:bind:0";
const NS = "urn:xmpp:bind:0";

export default function bind2({ sasl2, entity }, tag) {
const features = new Map();

sasl2.use(
NS_BIND,
NS,
async (element) => {
if (!element.is("bind", NS_BIND)) return;
if (!element.is("bind", NS)) return;

tag = typeof tag === "function" ? await tag() : tag;

Expand Down
12 changes: 2 additions & 10 deletions packages/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,13 @@ function client(options = {}) {
}).map(([k, v]) => ({ [k]: v(saslFactory) }));

// eslint-disable-next-line n/no-unsupported-features/node-builtins
const id = globalThis.crypto?.randomUUID?.();

let user_agent =
userAgent instanceof xml.Element
? userAgent
: xml("user-agent", { id: userAgent?.id || id }, [
userAgent?.software && xml("software", {}, userAgent.software),
userAgent?.device && xml("device", {}, userAgent.device),
]);
userAgent ??= xml("user-agent", { id: globalThis.crypto.randomUUID() });

// Stream features - order matters and define priority
const starttls = setupIfAvailable(_starttls, { streamFeatures });
const sasl2 = _sasl2(
{ streamFeatures, saslFactory },
createOnAuthenticate(credentials ?? { username, password }, user_agent),
createOnAuthenticate(credentials ?? { username, password }, userAgent),
);

const fast = setupIfAvailable(_fast, {
Expand Down
1 change: 0 additions & 1 deletion packages/connection/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,6 @@ class Connection extends EventEmitter {
*/
async stop() {
const el = await this._end();
this.jid = null;
this._status("offline", el);
return el;
}
Expand Down
10 changes: 0 additions & 10 deletions packages/connection/test/stop.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Connection from "../index.js";
import { JID } from "@xmpp/test";

test("resolves if socket property is undefined", async () => {
const conn = new Connection();
Expand Down Expand Up @@ -39,12 +38,3 @@ test("does not throw if connection is not established", async () => {
await conn.stop();
expect().pass();
});

test("resets jid", async () => {
const conn = new Connection();
conn.jid = new JID("foo@bar");

expect(conn.jid).not.toEqual(null);
await conn.stop();
expect(conn.jid).toEqual(null);
});
7 changes: 5 additions & 2 deletions packages/sasl2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Uses cases:
- Fetch credentials from a secure database

```js
import { xmpp } from "@xmpp/client";
import { xmpp, xml } from "@xmpp/client";

const client = xmpp({
credentials: authenticate,
Expand Down Expand Up @@ -60,7 +60,10 @@ async function getUserAgent() {
localStorage.set("user-agent-id", id);
}
// https://xmpp.org/extensions/xep-0388.html#initiation
return { id, software: "xmpp.js", device: "Sonny's Laptop" }; // You can also pass an xml.Element
return xml("user-agent", { id }, [
xml("software", {}, "xmpp.js"),
xml("device", {}, "Sonny's laptop"),
]);
}
```

Expand Down
15 changes: 12 additions & 3 deletions server/ctl.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import server from "./index.js";

const method = process.argv[2];
// eslint-disable-next-line unicorn/no-unreadable-array-destructuring
const [, , method, ...args] = process.argv;

const commands = {
start() {
Expand All @@ -22,10 +23,18 @@ const commands = {
console.log("stopped");
}
},
async enable(...args) {
await server.enableModules(...args);
await this.restart();
},
async disable(...args) {
await server.disableModules(...args);
await this.restart();
},
};

if (commands[method]) {
await commands[method]();
await commands[method](...args);
} else {
console.error("Valid commands are start/stop/restart/status.");
console.error("Valid commands are start/stop/restart/status/enable/disable.");
}
48 changes: 40 additions & 8 deletions server/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { promisify } from "util";
import path from "path";
import fs, { writeFileSync } from "fs";
import fs from "fs/promises";
import child_process from "child_process";
import net from "net";
// eslint-disable-next-line n/no-extraneous-import
Expand All @@ -10,18 +10,17 @@ import selfsigned from "selfsigned";
const __dirname = "./server";
// const __dirname = import.meta.dirname;

const readFile = promisify(fs.readFile);
const exec = promisify(child_process.exec);
const removeFile = promisify(fs.unlink);

const DATA_PATH = path.join(__dirname);
const PID_PATH = path.join(DATA_PATH, "prosody.pid");
const PROSODY_PORT = 5347;
const CFG_PATH = path.join(__dirname, "prosody.cfg.lua");

function clean() {
return Promise.all(
["prosody.err", "prosody.log", "prosody.pid"].map((file) =>
removeFile(path.join(__dirname, file)),
fs.unlink(path.join(__dirname, file)),
),
).catch(() => {});
}
Expand All @@ -47,12 +46,14 @@ async function waitPortOpen() {
return waitPortOpen();
}

function makeCertificate() {
async function makeCertificate() {
const attrs = [{ name: "commonName", value: "localhost" }];
const pems = selfsigned.generate(attrs, { days: 365, keySize: 2048 });

writeFileSync(path.join(__dirname, "certs/localhost.crt"), pems.cert);
writeFileSync(path.join(__dirname, "certs/localhost.key"), pems.private);
await Promise.all([
fs.writeFile(path.join(__dirname, "certs/localhost.crt"), pems.cert),
fs.writeFile(path.join(__dirname, "certs/localhost.key"), pems.private),
]);
}

async function waitPortClose() {
Expand All @@ -75,7 +76,7 @@ async function kill(signal = "SIGTERM") {

async function getPid() {
try {
return await readFile(PID_PATH, "utf8");
return await fs.readFile(PID_PATH, "utf8");
} catch (err) {
if (err.code !== "ENOENT") throw err;
return "";
Expand Down Expand Up @@ -119,6 +120,34 @@ async function restart(signal) {
return _start();
}

async function enableModules(mods) {
if (!Array.isArray(mods)) {
mods = [mods];
}

let prosody_cfg = await fs.readFile(CFG_PATH, "utf8");
for (const mod of mods) {
prosody_cfg = prosody_cfg.replace(`\n -- "${mod}";`, `\n "${mod}";`);
}
await fs.writeFile(CFG_PATH, prosody_cfg);
}

async function disableModules(mods) {
if (!Array.isArray(mods)) {
mods = [mods];
}

let prosody_cfg = await fs.readFile(CFG_PATH, "utf8");
for (const mod of mods) {
prosody_cfg = prosody_cfg.replace(`\n "${mod}";`, `\n -- "${mod}";`);
}
await fs.writeFile(CFG_PATH, prosody_cfg);
}

async function reset() {
await exec("git checkout server/prosody.cfg.lua");
}

export default {
isPortOpen,
waitPortClose,
Expand All @@ -128,4 +157,7 @@ export default {
stop,
restart,
kill,
enableModules,
disableModules,
reset,
};
125 changes: 125 additions & 0 deletions test/sasl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { client, jid } from "../packages/client/index.js";
import debug from "../packages/debug/index.js";
import server from "../server/index.js";

const NS_SASL = "urn:ietf:params:xml:ns:xmpp-sasl";
const NS_BIND = "urn:ietf:params:xml:ns:xmpp-bind";
const NS_SASL2 = "urn:xmpp:sasl:2";
const NS_BIND2 = "urn:xmpp:bind:0";
const NS_FAST = "urn:xmpp:fast:0";

const username = "client";
const password = "foobar";
const credentials = { username, password };
const domain = "localhost";
const JID = jid(username, domain).toString();

let xmpp;

afterEach(async () => {
await xmpp?.stop();
await server.reset();
});

test("client online with sasl and resource binding", async () => {
expect.assertions(6);

await server.disableModules([
"sasl2",
"sasl2_bind2",
"sasl2_sm",
"sasl2_fast",
]);
await server.enableModules(["saslauth"]);
await server.restart();

xmpp = client({ credentials, service: domain });
debug(xmpp);

xmpp.on("nonza", (element) => {
if (!element.is("features")) return;

expect(element.getChild("authentication", NS_SASL2)).toBe(undefined);
if (element.getChild("mechanisms", NS_SASL)) expect.pass();
});

xmpp.on("send", (el) => {
if (el.is("auth", NS_SASL)) expect().pass();
if (el.is("iq") && el.getChild("bind", NS_BIND)) expect().pass();
});

const address = await xmpp.start();
expect(address instanceof jid.JID).toBe(true);
expect(address.bare().toString()).toBe(JID);
});

test("client online with sasl2 and bind2", async () => {
expect.assertions(6);

await server.disableModules(["saslauth"]);
await server.enableModules(["sasl2", "sasl2_bind2"]);
await server.restart();

xmpp = client({ credentials, service: domain });
debug(xmpp);

xmpp.on("nonza", (element) => {
if (!element.is("features")) return;

expect(element.getChild("mechanisms", NS_SASL)).toBe(undefined);
if (element.getChild("authentication", NS_SASL2)) expect.pass();
});

xmpp.on("send", (el) => {
if (!el.is("authenticate", NS_SASL2)) return;
expect().pass();
if (el.getChild("bind", NS_BIND2)) expect().pass();
});

const address = await xmpp.start();
expect(address instanceof jid.JID).toBe(true);
expect(address.bare().toString()).toBe(JID);
});

test("client online with sasl2 and fast", async () => {
expect.assertions(3);

await server.disableModules(["saslauth"]);
await server.enableModules([
"sasl2",
"sasl2_bind2",
"sasl2_sm",
"sasl2_fast",
]);
await server.restart();

xmpp = client({
...credentials,
service: "ws://localhost:5280/xmpp-websocket",
});

// Get token
await xmpp.start();
await xmpp.stop();

debug(xmpp);

xmpp.on("nonza", (element) => {
if (!element.is("features")) return;

const authentication = element.getChild("authentication", NS_SASL2);
if (!authentication) return;
const inline = authentication.getChild("inline");
expect(inline.getChild("fast", NS_FAST)).not.toBe(undefined);
});

xmpp.on("send", (el) => {
const authenticate = el.is("authenticate", NS_SASL2);
if (!authenticate) return;

expect(el.attrs.mechanism).toBe("HT-SHA-256-NONE");
expect(el.getChild("fast", NS_FAST)).not.toBe(undefined);
});

await xmpp.start();
});

0 comments on commit b835091

Please sign in to comment.