diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index a6a16a82..47bab9f5 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -27,7 +27,7 @@ jobs:
echo deb http://packages.prosody.im/debian $(lsb_release -sc) main | sudo tee /etc/apt/sources.list.d/prosody.list
sudo wget https://prosody.im/files/prosody-debian-packages.key -O/etc/apt/trusted.gpg.d/prosody.gpg
sudo apt-get update
- sudo apt-get -y install prosody lua-bitop lua-sec
+ sudo apt-get -y install lua5.3 liblua5.3-dev prosody-trunk lua-bitop lua-sec luarocks
sudo service prosody stop
# - run: npm install -g npm
diff --git a/.gitignore b/.gitignore
index f10012a2..2e1fdeb4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@ server/certs/
server/prosody.err
server/prosody.log
server/prosody.pid
+server/modules
+server/.cache
!.gitkeep
!.editorconfig
diff --git a/Makefile b/Makefile
index 487ab4ea..30abfe93 100644
--- a/Makefile
+++ b/Makefile
@@ -28,10 +28,16 @@ ci:
make bundlesize
unit:
- npx jest
+ npm run test
e2e:
- NODE_TLS_REJECT_UNAUTHORIZED=0 npx jest --runInBand --config e2e.config.cjs
+ $(warning e2e tests require prosody-trunk and luarocks)
+ cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2 > /dev/null
+# https://github.com/xmppjs/xmpp.js/pull/1006
+# cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2_bind2 > /dev/null
+# cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2_fast > /dev/null
+# cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2_sm > /dev/null
+ npm run e2e
clean:
make stop
diff --git a/package-lock.json b/package-lock.json
index 3ffd095f..b5248f6a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4577,6 +4577,10 @@
"resolved": "packages/sasl-scram-sha-1",
"link": true
},
+ "node_modules/@xmpp/sasl2": {
+ "resolved": "packages/sasl2",
+ "link": true
+ },
"node_modules/@xmpp/session-establishment": {
"resolved": "packages/session-establishment",
"link": true
@@ -14561,6 +14565,7 @@
"@xmpp/sasl-anonymous": "^0.13.2",
"@xmpp/sasl-plain": "^0.13.2",
"@xmpp/sasl-scram-sha-1": "^0.13.2",
+ "@xmpp/sasl2": "^0.13.0",
"@xmpp/session-establishment": "^0.13.2",
"@xmpp/starttls": "^0.13.2",
"@xmpp/stream-features": "^0.13.2",
@@ -14773,6 +14778,18 @@
"node": ">= 20"
}
},
+ "packages/sasl-ht-sha-256-none": {
+ "name": "@xmpp/sasl-ht-sha-256-none",
+ "version": "0.13.0",
+ "extraneous": true,
+ "license": "ISC",
+ "dependencies": {
+ "create-hmac": "^1.1.7"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"packages/sasl-plain": {
"name": "@xmpp/sasl-plain",
"version": "0.13.2",
@@ -14795,6 +14812,21 @@
"node": ">= 20"
}
},
+ "packages/sasl2": {
+ "name": "@xmpp/sasl2",
+ "version": "0.13.0",
+ "license": "ISC",
+ "dependencies": {
+ "@xmpp/base64": "^0.13.0",
+ "@xmpp/error": "^0.13.0",
+ "@xmpp/jid": "^0.13.0",
+ "@xmpp/sasl": "^0.13.2",
+ "@xmpp/xml": "^0.13.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"packages/session-establishment": {
"name": "@xmpp/session-establishment",
"version": "0.13.2",
@@ -14949,6 +14981,7 @@
"@xmpp/sasl-anonymous": "^0.13.2",
"@xmpp/sasl-plain": "^0.13.2",
"@xmpp/sasl-scram-sha-1": "^0.13.2",
+ "@xmpp/sasl2": "^0.13.0",
"@xmpp/session-establishment": "^0.13.2",
"@xmpp/starttls": "^0.13.2",
"@xmpp/stream-features": "^0.13.2",
diff --git a/package.json b/package.json
index 8629245d..fdd16165 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,8 @@
"selfsigned": "^2.4.1"
},
"scripts": {
+ "test": "npx jest",
+ "e2e": "NODE_TLS_REJECT_UNAUTHORIZED=0 npx jest --runInBand --config e2e.config.cjs",
"preversion": "make bundle"
},
"engines": {
diff --git a/packages/client/README.md b/packages/client/README.md
index f0d62f0d..aeab2f58 100644
--- a/packages/client/README.md
+++ b/packages/client/README.md
@@ -63,6 +63,8 @@ xmpp.on("stanza", async (stanza) => {
});
xmpp.on("online", async (address) => {
+ console.log("online as", address.toString());
+
// Makes itself available
await xmpp.send(xml("presence"));
@@ -75,7 +77,7 @@ xmpp.on("online", async (address) => {
await xmpp.send(message);
});
-xmpp.start().catch(console.error);
+await xmpp.start();
```
## xml
diff --git a/packages/client/browser.js b/packages/client/browser.js
deleted file mode 100644
index b77c440f..00000000
--- a/packages/client/browser.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { xml, jid, Client } from "@xmpp/client-core";
-import getDomain from "./lib/getDomain.js";
-import createOnAuthenticate from "./lib/createOnAuthenticate.js";
-
-import _reconnect from "@xmpp/reconnect";
-import _websocket from "@xmpp/websocket";
-import _middleware from "@xmpp/middleware";
-import _streamFeatures from "@xmpp/stream-features";
-import _iqCaller from "@xmpp/iq/caller.js";
-import _iqCallee from "@xmpp/iq/callee.js";
-import _resolve from "@xmpp/resolve";
-
-import _sasl from "@xmpp/sasl";
-import _resourceBinding from "@xmpp/resource-binding";
-import _sessionEstablishment from "@xmpp/session-establishment";
-import _streamManagement from "@xmpp/stream-management";
-
-import SASLFactory from "saslmechanisms";
-import plain from "@xmpp/sasl-plain";
-import anonymous from "@xmpp/sasl-anonymous";
-
-function client(options = {}) {
- const { resource, credentials, username, password, ...params } = options;
-
- const { domain, service } = params;
- if (!domain && service) {
- params.domain = getDomain(service);
- }
-
- const entity = new Client(params);
-
- const reconnect = _reconnect({ entity });
- const websocket = _websocket({ entity });
-
- const middleware = _middleware({ entity });
- const streamFeatures = _streamFeatures({ middleware });
- const iqCaller = _iqCaller({ middleware, entity });
- const iqCallee = _iqCallee({ middleware, entity });
- const resolve = _resolve({ entity });
-
- // SASL mechanisms - order matters and define priority
- const saslFactory = new SASLFactory();
- const mechanisms = Object.entries({
- plain,
- anonymous,
- }).map(([k, v]) => ({ [k]: v(saslFactory) }));
-
- // Stream features - order matters and define priority
- const sasl = _sasl(
- { streamFeatures, saslFactory },
- createOnAuthenticate(credentials ?? { username, password }),
- );
- const streamManagement = _streamManagement({
- streamFeatures,
- entity,
- middleware,
- });
- const resourceBinding = _resourceBinding(
- { iqCaller, streamFeatures },
- resource,
- );
- const sessionEstablishment = _sessionEstablishment({
- iqCaller,
- streamFeatures,
- });
-
- return Object.assign(entity, {
- entity,
- reconnect,
- websocket,
- middleware,
- streamFeatures,
- iqCaller,
- iqCallee,
- resolve,
- sasl,
- resourceBinding,
- sessionEstablishment,
- streamManagement,
- mechanisms,
- saslFactory,
- });
-}
-
-export { xml, jid, client };
diff --git a/packages/client/example.js b/packages/client/example.js
index 7effe6cf..6bab1abc 100644
--- a/packages/client/example.js
+++ b/packages/client/example.js
@@ -1,5 +1,4 @@
import { client, xml } from "@xmpp/client";
-
// eslint-disable-next-line n/no-extraneous-import
import debug from "@xmpp/debug";
diff --git a/packages/client/index.js b/packages/client/index.js
index 24ac2a1e..aac728be 100644
--- a/packages/client/index.js
+++ b/packages/client/index.js
@@ -11,8 +11,8 @@ import _streamFeatures from "@xmpp/stream-features";
import _iqCaller from "@xmpp/iq/caller.js";
import _iqCallee from "@xmpp/iq/callee.js";
import _resolve from "@xmpp/resolve";
-
import _starttls from "@xmpp/starttls";
+import _sasl2 from "@xmpp/sasl2";
import _sasl from "@xmpp/sasl";
import _resourceBinding from "@xmpp/resource-binding";
import _sessionEstablishment from "@xmpp/session-establishment";
@@ -23,8 +23,20 @@ import scramsha1 from "@xmpp/sasl-scram-sha-1";
import plain from "@xmpp/sasl-plain";
import anonymous from "@xmpp/sasl-anonymous";
+// In browsers and react-native some packages are excluded
+// see package.json and https://metrobundler.dev/docs/configuration/#resolvermainfields
+// in which case the default import returns an empty object
+function setupIfAvailable(module, ...args) {
+ if (typeof module !== "function") {
+ return undefined;
+ }
+
+ return module(...args);
+}
+
function client(options = {}) {
const { resource, credentials, username, password, ...params } = options;
+ const { clientId, software, device } = params;
const { domain, service } = params;
if (!domain && service) {
@@ -35,8 +47,8 @@ function client(options = {}) {
const reconnect = _reconnect({ entity });
const websocket = _websocket({ entity });
- const tcp = _tcp({ entity });
- const tls = _tls({ entity });
+ const tcp = setupIfAvailable(_tcp, { entity });
+ const tls = setupIfAvailable(_tls, { entity });
const middleware = _middleware({ entity });
const streamFeatures = _streamFeatures({ middleware });
@@ -47,13 +59,18 @@ function client(options = {}) {
// SASL mechanisms - order matters and define priority
const saslFactory = new SASLFactory();
const mechanisms = Object.entries({
- scramsha1,
+ ...(typeof scramsha1 === "function" && { scramsha1 }),
plain,
anonymous,
}).map(([k, v]) => ({ [k]: v(saslFactory) }));
// Stream features - order matters and define priority
- const starttls = _starttls({ streamFeatures });
+ const starttls = setupIfAvailable(_starttls, { streamFeatures });
+ const sasl2 = _sasl2(
+ { streamFeatures, saslFactory },
+ createOnAuthenticate(credentials ?? { username, password }),
+ { clientId, software, device },
+ );
const sasl = _sasl(
{ streamFeatures, saslFactory },
createOnAuthenticate(credentials ?? { username, password }),
@@ -84,12 +101,13 @@ function client(options = {}) {
iqCallee,
resolve,
starttls,
+ saslFactory,
+ sasl2,
sasl,
resourceBinding,
sessionEstablishment,
streamManagement,
mechanisms,
- saslFactory,
});
}
diff --git a/packages/client/package.json b/packages/client/package.json
index 4bf39f21..1f446c7c 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -15,6 +15,7 @@
"@xmpp/resolve": "^0.13.2",
"@xmpp/resource-binding": "^0.13.2",
"@xmpp/sasl": "^0.13.2",
+ "@xmpp/sasl2": "^0.13.0",
"@xmpp/sasl-anonymous": "^0.13.2",
"@xmpp/sasl-plain": "^0.13.2",
"@xmpp/sasl-scram-sha-1": "^0.13.2",
@@ -27,8 +28,12 @@
"@xmpp/websocket": "^0.13.2",
"saslmechanisms": "^0.1.1"
},
- "browser": "browser.js",
- "react-native": "browser.js",
+ "browser": {
+ "@xmpp/tcp": false,
+ "@xmpp/tls": false,
+ "@xmpp/starttls": false,
+ "@xmpp/sasl-scram-sha-1": false
+ },
"engines": {
"node": ">= 20"
},
diff --git a/packages/component/README.md b/packages/component/README.md
index 5c18dd33..0a9c41e4 100644
--- a/packages/component/README.md
+++ b/packages/component/README.md
@@ -52,7 +52,7 @@ xmpp.on("online", async (address) => {
await xmpp.send(message);
});
-xmpp.start().catch(console.error);
+await xmpp.start();
```
## xml
diff --git a/packages/error/test.js b/packages/error/test.js
index da3b3867..5905200d 100644
--- a/packages/error/test.js
+++ b/packages/error/test.js
@@ -20,6 +20,7 @@ test("fromElement", () => {
const error = XMPPError.fromElement(nonza);
expect(error instanceof Error).toBe(true);
+ expect(error instanceof XMPPError).toBe(true);
expect(error.name).toBe("XMPPError");
expect(error.condition).toBe("some-condition");
expect(error.text).toBe("foo");
@@ -42,6 +43,7 @@ test("fromElement - whitespaces", () => {
const error = XMPPError.fromElement(nonza);
expect(error instanceof Error).toBe(true);
+ expect(error instanceof XMPPError).toBe(true);
expect(error.name).toBe("XMPPError");
expect(error.condition).toBe("some-condition");
expect(error.text).toBe("\n foo\n ");
diff --git a/packages/sasl/index.js b/packages/sasl/index.js
index 8a64e38c..78918517 100644
--- a/packages/sasl/index.js
+++ b/packages/sasl/index.js
@@ -6,8 +6,8 @@ import xml from "@xmpp/xml";
const NS = "urn:ietf:params:xml:ns:xmpp-sasl";
-function getMechanismNames(features) {
- return features
+function getMechanismNames(stanza) {
+ return stanza
.getChild("mechanisms", NS)
.getChildElements()
.map((el) => el.text());
diff --git a/packages/sasl/lib/SASLError.test.js b/packages/sasl/lib/SASLError.test.js
new file mode 100644
index 00000000..78b7b325
--- /dev/null
+++ b/packages/sasl/lib/SASLError.test.js
@@ -0,0 +1,71 @@
+import XMPPError from "@xmpp/error";
+import SASLError from "./SASLError.js";
+
+// https://xmpp.org/rfcs/rfc6120.html#rfc.section.6.4.5
+// https://xmpp.org/rfcs/rfc6120.html#rfc.section.A.4
+
+test("SASL", () => {
+ const nonza = (
+
+
+
+ );
+
+ const error = SASLError.fromElement(nonza);
+
+ expect(error instanceof Error).toBe(true);
+ expect(error instanceof XMPPError).toBe(true);
+ expect(error instanceof SASLError).toBe(true);
+ expect(error.name).toBe("SASLError");
+ expect(error.condition).toBe("not-authorized");
+ expect(error.text).toBe("");
+});
+
+test("SASL with text", () => {
+ const nonza = (
+
+
+ foo
+
+ );
+ expect(SASLError.fromElement(nonza).text).toBe("foo");
+});
+
+// https://xmpp.org/extensions/xep-0388.html#failure
+// https://github.com/xsf/xeps/pull/1411
+// https://xmpp.org/extensions/xep-0388.html#sect-idm46365286031040
+
+test("SASL2", () => {
+ const nonza = (
+
+
+
+ );
+
+ const error = SASLError.fromElement(nonza);
+
+ expect(error instanceof Error).toBe(true);
+ expect(error instanceof XMPPError).toBe(true);
+ expect(error instanceof SASLError).toBe(true);
+ expect(error.name).toBe("SASLError");
+ expect(error.condition).toBe("aborted");
+});
+
+test("SASL2 with text and application", () => {
+ const application = (
+
+ );
+
+ const nonza = (
+
+
+ This is a terrible example.
+ {application}
+
+ );
+
+ const error = SASLError.fromElement(nonza);
+
+ expect(error.text).toBe("This is a terrible example.");
+ expect(error.application).toBe(application);
+});
diff --git a/packages/sasl/test.js b/packages/sasl/test.js
index 4b8a32de..5147a99d 100644
--- a/packages/sasl/test.js
+++ b/packages/sasl/test.js
@@ -168,8 +168,8 @@ test("prefers SCRAM-SHA-1", async () => {
ANONYMOUS
- PLAIN
SCRAM-SHA-1
+ PLAIN
,
);
diff --git a/packages/sasl2/README.md b/packages/sasl2/README.md
new file mode 100644
index 00000000..30112b9f
--- /dev/null
+++ b/packages/sasl2/README.md
@@ -0,0 +1,64 @@
+# SASL2
+
+SASL2 Negotiation for `@xmpp/client`.
+
+Included and enabled in `@xmpp/client`.
+
+## Usage
+
+### object
+
+```js
+import { xmpp } from "@xmpp/client";
+
+const client = xmpp({
+ credentials: {
+ username: "foo",
+ password: "bar",
+ clientId: "Some UUID for this client/server pair (optional)",
+ software: "Name of this software (optional)",
+ device: "Description of this device (optional)",
+ },
+});
+```
+
+### function
+
+Instead, you can provide a function that will be called every time authentication occurs (every (re)connect).
+
+Uses cases:
+
+- Have the user enter the password every time
+- Do not ask for password before connection is made
+- Debug authentication
+- Using a SASL mechanism with specific requirements (such as FAST)
+- Perform an asynchronous operation to get credentials
+
+```js
+import { xmpp } from "@xmpp/client";
+
+const client = xmpp({
+ credentials: authenticate,
+ clientId: "Some UUID for this client/server pair (optional)",
+ software: "Name of this software (optional)",
+ device: "Description of this device (optional)",
+});
+
+async function authenticate(callback, mechanisms) {
+ const fast = mechanisms.find((mech) => mech.canFast)?.name;
+ const mech = mechanisms.find((mech) => mech.canOther)?.name;
+
+ return callback(
+ {
+ username: await prompt("enter username"),
+ password: await prompt("enter password"),
+ requestToken: fast,
+ },
+ mech,
+ );
+}
+```
+
+## References
+
+- [XEP-0388: Extensible SASL Profile](https://xmpp.org/extensions/xep-0388.html)
diff --git a/packages/sasl2/index.js b/packages/sasl2/index.js
new file mode 100644
index 00000000..1348f81a
--- /dev/null
+++ b/packages/sasl2/index.js
@@ -0,0 +1,141 @@
+import { encode, decode } from "@xmpp/base64";
+import SASLError from "@xmpp/sasl/lib/SASLError.js";
+import jid from "@xmpp/jid";
+import xml from "@xmpp/xml";
+
+// https://xmpp.org/extensions/xep-0388.html
+
+const NS = "urn:xmpp:sasl:2";
+
+function getMechanismNames(stanza) {
+ return stanza
+ .getChild("authentication", NS)
+ .getChildren("mechanism", NS)
+ .map((m) => m.text());
+}
+
+async function authenticate({
+ saslFactory,
+ entity,
+ mechanism,
+ credentials,
+ userAgent,
+}) {
+ const mech = saslFactory.create([mechanism]);
+ if (!mech) {
+ throw new Error(`SASL: Mechanism ${mechanism} not found.`);
+ }
+
+ const { domain } = entity.options;
+ const creds = {
+ username: null,
+ password: null,
+ server: domain,
+ host: domain,
+ realm: domain,
+ serviceType: "xmpp",
+ serviceName: domain,
+ ...credentials,
+ };
+
+ return new Promise((resolve, reject) => {
+ const handler = (element) => {
+ if (element.attrs.xmlns !== NS) {
+ return;
+ }
+
+ if (element.name === "challenge") {
+ mech.challenge(decode(element.text()));
+ const resp = mech.response(creds);
+ entity.send(
+ xml(
+ "response",
+ { xmlns: NS, mechanism: mech.name },
+ typeof resp === "string" ? encode(resp) : "",
+ ),
+ );
+ return;
+ }
+
+ if (element.name === "failure") {
+ reject(SASLError.fromElement(element));
+ return;
+ }
+
+ if (element.name === "continue") {
+ // No tasks supported yet
+ reject(new Error("continue is not supported yet"));
+ return;
+ }
+
+ if (element.name === "success") {
+ const additionalData = element.getChild("additional-data")?.text();
+ if (additionalData && mech.final) {
+ mech.final(decode(additionalData));
+ }
+ // This jid will be bare unless we do inline bind2 then it will be the bound full jid
+ const aid = element.getChild("authorization-identifier")?.text();
+ if (aid) {
+ if (!entity.jid?.resource) {
+ // No jid or bare jid, so update it
+ entity._jid(aid);
+ } else if (jid(aid).resource) {
+ // We have a full jid so use it
+ entity._jid(aid);
+ }
+ }
+ resolve(element);
+ return;
+ }
+
+ entity.removeListener("nonza", handler);
+ };
+
+ entity.send(
+ xml("authenticate", { xmlns: NS, mechanism: mech.name }, [
+ mech.clientFirst &&
+ xml("initial-response", {}, encode(mech.response(creds))),
+ (userAgent?.clientId || userAgent?.software || userAgent?.device) &&
+ xml(
+ "user-agent",
+ userAgent.clientId ? { id: userAgent.clientId } : {},
+ [
+ userAgent.software && xml("software", {}, userAgent.software),
+ userAgent.device && xml("device", {}, userAgent.device),
+ ],
+ ),
+ ]),
+ );
+
+ entity.on("nonza", handler);
+ });
+}
+
+export default function sasl2(
+ { streamFeatures, saslFactory },
+ onAuthenticate,
+ // userAgent,
+) {
+ streamFeatures.use("authentication", NS, async ({ stanza, entity }) => {
+ const offered = getMechanismNames(stanza);
+ const supported = saslFactory._mechs.map(({ name }) => name);
+ const intersection = supported.filter((mech) => offered.includes(mech));
+
+ if (intersection.length === 0) {
+ throw new SASLError("SASL: No compatible mechanism available.");
+ }
+
+ async function done(credentials, mechanism) {
+ await authenticate({
+ saslFactory,
+ entity,
+ mechanism,
+ credentials,
+ });
+ }
+
+ await onAuthenticate(done, intersection);
+
+ return true; // Not online yet, wait for next features
+ });
+}
diff --git a/packages/sasl2/package.json b/packages/sasl2/package.json
new file mode 100644
index 00000000..56cd9241
--- /dev/null
+++ b/packages/sasl2/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@xmpp/sasl2",
+ "description": "XMPP SASL2 for JavaScript",
+ "repository": "github:xmppjs/xmpp.js",
+ "homepage": "https://github.com/xmppjs/xmpp.js/tree/main/packages/sasl2",
+ "bugs": "http://github.com/xmppjs/xmpp.js/issues",
+ "version": "0.13.0",
+ "license": "ISC",
+ "type": "module",
+ "main": "index.js",
+ "keywords": [
+ "XMPP",
+ "sasl"
+ ],
+ "dependencies": {
+ "@xmpp/base64": "^0.13.0",
+ "@xmpp/error": "^0.13.0",
+ "@xmpp/sasl": "^0.13.2",
+ "@xmpp/jid": "^0.13.0",
+ "@xmpp/xml": "^0.13.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/sasl2/test.js b/packages/sasl2/test.js
new file mode 100644
index 00000000..43445f82
--- /dev/null
+++ b/packages/sasl2/test.js
@@ -0,0 +1,140 @@
+import { mockClient, promise } from "@xmpp/test";
+
+const username = "foo";
+const password = "bar";
+const credentials = { username, password };
+
+test("No compatible mechanism available", async () => {
+ const { entity } = mockClient({ username, password });
+
+ entity.mockInput(
+
+
+ FOO
+
+ ,
+ );
+
+ const error = await promise(entity, "error");
+ expect(error instanceof Error).toBe(true);
+ expect(error.message).toBe("SASL: No compatible mechanism available.");
+});
+
+test("with object credentials", async () => {
+ const { entity } = mockClient({ credentials });
+
+ entity.mockInput(
+
+
+ PLAIN
+
+ ,
+ );
+
+ expect(await promise(entity, "send")).toEqual(
+
+ AGZvbwBiYXI=
+ ,
+ );
+
+ entity.mockInput();
+ entity.mockInput();
+
+ await promise(entity, "online");
+});
+
+test("with function credentials", async () => {
+ const mech = "PLAIN";
+
+ function authenticate(auth, mechanisms) {
+ expect(mechanisms).toEqual([mech]);
+ return auth(credentials, mech);
+ }
+
+ const { entity } = mockClient({ credentials: authenticate });
+
+ entity.mockInput(
+
+
+ {mech}
+
+ ,
+ );
+
+ expect(await promise(entity, "send")).toEqual(
+
+ AGZvbwBiYXI=
+ ,
+ );
+
+ entity.mockInput();
+ entity.mockInput();
+
+ await promise(entity, "online");
+});
+
+test("failure", async () => {
+ const { entity } = mockClient({ credentials });
+
+ entity.mockInput(
+
+
+ PLAIN
+
+ ,
+ );
+
+ expect(await promise(entity, "send")).toEqual(
+
+ AGZvbwBiYXI=
+ ,
+ );
+
+ const failure = (
+
+
+
+ );
+
+ entity.mockInput(failure);
+
+ const error = await promise(entity, "error");
+ expect(error instanceof Error).toBe(true);
+ expect(error.name).toBe("SASLError");
+ expect(error.condition).toBe("some-condition");
+ expect(error.element).toBe(failure);
+});
+
+test("prefers SCRAM-SHA-1", async () => {
+ const { entity } = mockClient({ credentials });
+
+ entity.mockInput(
+
+
+ ANONYMOUS
+ SCRAM-SHA-1
+ PLAIN
+
+ ,
+ );
+
+ const result = await promise(entity, "send");
+ expect(result.attrs.mechanism).toEqual("SCRAM-SHA-1");
+});
+
+test.skip("use ANONYMOUS if username and password are not provided", async () => {
+ const { entity } = mockClient();
+
+ entity.mockInput(
+
+
+ ANONYMOUS
+ PLAIN
+ SCRAM-SHA-1
+
+ ,
+ );
+
+ const result = await promise(entity, "send");
+ expect(result.attrs.mechanism).toEqual("ANONYMOUS");
+});
diff --git a/packages/xmpp.js/package.json b/packages/xmpp.js/package.json
index a5655768..332423f5 100644
--- a/packages/xmpp.js/package.json
+++ b/packages/xmpp.js/package.json
@@ -7,7 +7,6 @@
"version": "0.13.2",
"license": "ISC",
"type": "module",
- "main": "index.js",
"keywords": [
"XMPP",
"jabber",
@@ -39,6 +38,7 @@
"@xmpp/sasl-anonymous": "^0.13.2",
"@xmpp/sasl-plain": "^0.13.2",
"@xmpp/sasl-scram-sha-1": "^0.13.2",
+ "@xmpp/sasl2": "^0.13.0",
"@xmpp/session-establishment": "^0.13.2",
"@xmpp/starttls": "^0.13.2",
"@xmpp/stream-features": "^0.13.2",
diff --git a/rollup.config.js b/rollup.config.js
index 2b81fd56..fd709d68 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -6,7 +6,7 @@ import terser from "@rollup/plugin-terser";
export default [
{
- input: "packages/client/browser.js",
+ input: "packages/client/index.js",
output: {
file: "packages/client/dist/xmpp.js",
format: "iife",
@@ -21,7 +21,7 @@ export default [
],
},
{
- input: "packages/client/browser.js",
+ input: "packages/client/index.js",
output: {
file: "packages/client/dist/xmpp.min.js",
format: "iife",
diff --git a/server/index.js b/server/index.js
index e9b405f9..2f9c4c22 100644
--- a/server/index.js
+++ b/server/index.js
@@ -87,7 +87,7 @@ async function _start() {
makeCertificate();
- await exec("prosody", {
+ await exec("prosody -D", {
cwd: DATA_PATH,
env: {
...process.env,
diff --git a/server/prosody.cfg.lua b/server/prosody.cfg.lua
index 480c0021..499cda1f 100644
--- a/server/prosody.cfg.lua
+++ b/server/prosody.cfg.lua
@@ -4,6 +4,10 @@
local lfs = require "lfs";
+plugin_paths = { "modules" }
+plugin_server = "https://modules.prosody.im/rocks/"
+installer_plugin_path = lfs.currentdir() .. "/modules";
+
modules_enabled = {
"roster";
"saslauth";
@@ -18,13 +22,17 @@ modules_enabled = {
"time";
"version";
"smacks";
+ "sasl2";
+ -- https://github.com/xmppjs/xmpp.js/pull/1006
+ -- "sasl2_bind2";
+ -- "sasl2_fast";
+ -- "sasl2_sm";
};
modules_disabled = {
"s2s";
}
-daemonize = true;
pidfile = lfs.currentdir() .. "/prosody.pid";
allow_registration = true;