From f6c77edd5927f0d8ee9feafff1acbddfe9c39e75 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 22 Dec 2024 23:50:31 +0100 Subject: [PATCH] Implement SASL2 (#1030) Co-authored-by: Stephen Paul Weber --- .github/workflows/CI.yml | 2 +- .gitignore | 2 + Makefile | 10 +- package-lock.json | 33 +++++++ package.json | 2 + packages/client/README.md | 4 +- packages/client/browser.js | 85 ----------------- packages/client/example.js | 1 - packages/client/index.js | 30 ++++-- packages/client/package.json | 9 +- packages/component/README.md | 2 +- packages/error/test.js | 2 + packages/sasl/index.js | 4 +- packages/sasl/lib/SASLError.test.js | 71 ++++++++++++++ packages/sasl/test.js | 2 +- packages/sasl2/README.md | 64 +++++++++++++ packages/sasl2/index.js | 141 ++++++++++++++++++++++++++++ packages/sasl2/package.json | 28 ++++++ packages/sasl2/test.js | 140 +++++++++++++++++++++++++++ packages/xmpp.js/package.json | 2 +- rollup.config.js | 4 +- server/index.js | 2 +- server/prosody.cfg.lua | 10 +- 23 files changed, 543 insertions(+), 107 deletions(-) delete mode 100644 packages/client/browser.js create mode 100644 packages/sasl/lib/SASLError.test.js create mode 100644 packages/sasl2/README.md create mode 100644 packages/sasl2/index.js create mode 100644 packages/sasl2/package.json create mode 100644 packages/sasl2/test.js 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;