Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sasl, sasl2 and fast e2e tests #1044

Merged
merged 1 commit into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
});
Loading