From 4c3a481dc24670b4b6ad00c75ef3afe371454184 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 21 Nov 2023 14:44:59 -0500 Subject: [PATCH 01/27] Include stream from when possible As reccomended by the RFC --- packages/connection/index.js | 7 +++++++ packages/tls/lib/Socket.js | 4 ++++ packages/websocket/lib/Socket.js | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/packages/connection/index.js b/packages/connection/index.js index 1773ee65..1705df5a 100644 --- a/packages/connection/index.js +++ b/packages/connection/index.js @@ -258,6 +258,13 @@ class Connection extends EventEmitter { const headerElement = this.headerElement(); headerElement.attrs.to = domain; + if ( + this.socket.secure && + this.socket.secure() && + (this.streamFrom || this.jid) + ) { + headerElement.attrs.from = (this.streamFrom || this.jid).toString(); + } headerElement.attrs["xml:lang"] = lang; this.root = headerElement; diff --git a/packages/tls/lib/Socket.js b/packages/tls/lib/Socket.js index f6f9d781..c9d27a4a 100644 --- a/packages/tls/lib/Socket.js +++ b/packages/tls/lib/Socket.js @@ -10,6 +10,10 @@ class Socket extends EventEmitter { this.timeout = null; } + secure() { + return true; + } + connect(...args) { this._attachSocket(tls.connect(...args)); } diff --git a/packages/websocket/lib/Socket.js b/packages/websocket/lib/Socket.js index 73203c25..459e5b74 100644 --- a/packages/websocket/lib/Socket.js +++ b/packages/websocket/lib/Socket.js @@ -12,6 +12,10 @@ class Socket extends EventEmitter { this.listeners = Object.create(null); } + secure() { + return this.url.startsWith("wss") || this.url.startsWith("ws://localhost"); + } + connect(url) { this.url = url; this._attachSocket(new WebSocket(url, ["xmpp"])); From 3fb0a08eec133a9dd675d7ba17eeca031d8a8c56 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 21 Nov 2023 14:46:27 -0500 Subject: [PATCH 02/27] Implement SASL HT-SHA-256-NONE mechanism --- package-lock.json | 23 ++++++++++++++++++ packages/sasl-ht-sha-256-none/index.js | 28 ++++++++++++++++++++++ packages/sasl-ht-sha-256-none/package.json | 23 ++++++++++++++++++ packages/xmpp.js/package.json | 1 + 4 files changed, 75 insertions(+) create mode 100644 packages/sasl-ht-sha-256-none/index.js create mode 100644 packages/sasl-ht-sha-256-none/package.json diff --git a/package-lock.json b/package-lock.json index 8bf71e1e..f7fb87dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4033,6 +4033,10 @@ "resolved": "packages/sasl-anonymous", "link": true }, + "node_modules/@xmpp/sasl-ht-sha-256-none": { + "resolved": "packages/sasl-ht-sha-256-none", + "link": true + }, "node_modules/@xmpp/sasl-plain": { "resolved": "packages/sasl-plain", "link": true @@ -16799,6 +16803,7 @@ "@xmpp/resource-binding": "^0.13.0", "@xmpp/sasl": "^0.13.0", "@xmpp/sasl-anonymous": "^0.13.0", + "@xmpp/sasl-ht-sha-256-none": "^0.13.0", "@xmpp/sasl-plain": "^0.13.0", "@xmpp/sasl-scram-sha-1": "^0.13.0", "@xmpp/session-establishment": "^0.13.0", @@ -17018,6 +17023,17 @@ "node": ">= 14" } }, + "packages/sasl-ht-sha-256-none": { + "name": "@xmpp/sasl-ht-sha-256-none", + "version": "0.13.0", + "license": "ISC", + "dependencies": { + "create-hmac": "^1.1.7" + }, + "engines": { + "node": ">= 14" + } + }, "packages/sasl-plain": { "name": "@xmpp/sasl-plain", "version": "0.13.0", @@ -20136,6 +20152,7 @@ "@xmpp/resource-binding": "^0.13.0", "@xmpp/sasl": "^0.13.0", "@xmpp/sasl-anonymous": "^0.13.0", + "@xmpp/sasl-ht-sha-256-none": "^0.13.0", "@xmpp/sasl-plain": "^0.13.0", "@xmpp/sasl-scram-sha-1": "^0.13.0", "@xmpp/session-establishment": "^0.13.0", @@ -20264,6 +20281,12 @@ "sasl-anonymous": "^0.1.0" } }, + "@xmpp/sasl-ht-sha-256-none": { + "version": "file:packages/sasl-ht-sha-256-none", + "requires": { + "create-hmac": "^1.1.7" + } + }, "@xmpp/sasl-plain": { "version": "file:packages/sasl-plain", "requires": { diff --git a/packages/sasl-ht-sha-256-none/index.js b/packages/sasl-ht-sha-256-none/index.js new file mode 100644 index 00000000..d784e89b --- /dev/null +++ b/packages/sasl-ht-sha-256-none/index.js @@ -0,0 +1,28 @@ +"use strict"; + +var createHmac = require("create-hmac"); + +function Mechanism() {} + +Mechanism.prototype.Mechanism = Mechanism; +Mechanism.prototype.name = "HT-SHA-256-NONE"; +Mechanism.prototype.clientFirst = true; + +Mechanism.prototype.response = (cred) => { + this.password = cred.password; + const hmac = createHmac("sha256", this.password); + hmac.update("Initiator"); + return cred.username + "\0" + hmac.digest("latin1"); +}; + +Mechanism.prototype.final = (data) => { + const hmac = createHmac("sha256", this.password); + hmac.update("Responder"); + if (hmac.digest("latin1") !== data) { + throw "Responder message from server was wrong"; + } +}; + +module.exports = (sasl) => { + sasl.use(Mechanism); +}; diff --git a/packages/sasl-ht-sha-256-none/package.json b/packages/sasl-ht-sha-256-none/package.json new file mode 100644 index 00000000..82c25dd2 --- /dev/null +++ b/packages/sasl-ht-sha-256-none/package.json @@ -0,0 +1,23 @@ +{ + "name": "@xmpp/sasl-ht-sha-256-none", + "description": "XMPP SASL HT-SHA-256-NONE for JavaScript", + "repository": "github:xmppjs/xmpp.js", + "homepage": "https://github.com/xmppjs/xmpp.js/tree/main/packages/sasl-ht-sha-256-none", + "bugs": "http://github.com/xmppjs/xmpp.js/issues", + "version": "0.13.0", + "license": "ISC", + "keywords": [ + "XMPP", + "sasl", + "plain" + ], + "dependencies": { + "create-hmac": "^1.1.7" + }, + "engines": { + "node": ">= 14" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/xmpp.js/package.json b/packages/xmpp.js/package.json index a04b6b97..c487b8ff 100644 --- a/packages/xmpp.js/package.json +++ b/packages/xmpp.js/package.json @@ -35,6 +35,7 @@ "@xmpp/resource-binding": "^0.13.0", "@xmpp/sasl": "^0.13.0", "@xmpp/sasl-anonymous": "^0.13.0", + "@xmpp/sasl-ht-sha-256-none": "^0.13.0", "@xmpp/sasl-plain": "^0.13.0", "@xmpp/sasl-scram-sha-1": "^0.13.0", "@xmpp/session-establishment": "^0.13.0", From 36f543fd6cb6e31dffeebaaba3bdb2a5cfda38df Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 21 Nov 2023 15:10:40 -0500 Subject: [PATCH 03/27] Implement SASL2 (with optional BIND2 and FAST) --- .eslintrc.js | 2 +- package-lock.json | 31 ++++++ packages/sasl2/README.md | 79 ++++++++++++++ packages/sasl2/index.js | 184 ++++++++++++++++++++++++++++++++ packages/sasl2/lib/SASLError.js | 14 +++ packages/sasl2/package.json | 26 +++++ packages/xmpp.js/package.json | 1 + 7 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 packages/sasl2/README.md create mode 100644 packages/sasl2/index.js create mode 100644 packages/sasl2/lib/SASLError.js create mode 100644 packages/sasl2/package.json diff --git a/.eslintrc.js b/.eslintrc.js index 3180f68d..dc0867df 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,7 +17,7 @@ module.exports = { parserOptions: { sourceType: "script", - ecmaVersion: 2019, + ecmaVersion: 2020, ecmaFeatures: { jsx: true, }, diff --git a/package-lock.json b/package-lock.json index f7fb87dc..0ab09e04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4045,6 +4045,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 @@ -16806,6 +16810,7 @@ "@xmpp/sasl-ht-sha-256-none": "^0.13.0", "@xmpp/sasl-plain": "^0.13.0", "@xmpp/sasl-scram-sha-1": "^0.13.0", + "@xmpp/sasl2": "^0.13.0", "@xmpp/session-establishment": "^0.13.0", "@xmpp/starttls": "^0.13.1", "@xmpp/stream-features": "^0.13.0", @@ -17056,6 +17061,21 @@ "node": ">= 14" } }, + "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/xml": "^0.13.0", + "saslmechanisms": "^0.1.1" + }, + "engines": { + "node": ">= 14" + } + }, "packages/session-establishment": { "name": "@xmpp/session-establishment", "version": "0.13.0", @@ -20155,6 +20175,7 @@ "@xmpp/sasl-ht-sha-256-none": "^0.13.0", "@xmpp/sasl-plain": "^0.13.0", "@xmpp/sasl-scram-sha-1": "^0.13.0", + "@xmpp/sasl2": "^0.13.0", "@xmpp/session-establishment": "^0.13.0", "@xmpp/starttls": "^0.13.1", "@xmpp/stream-features": "^0.13.0", @@ -20299,6 +20320,16 @@ "sasl-scram-sha-1": "^1.2.1" } }, + "@xmpp/sasl2": { + "version": "file:packages/sasl2", + "requires": { + "@xmpp/base64": "^0.13.0", + "@xmpp/error": "^0.13.0", + "@xmpp/jid": "^0.13.0", + "@xmpp/xml": "^0.13.0", + "saslmechanisms": "^0.1.1" + } + }, "@xmpp/session-establishment": { "version": "file:packages/session-establishment", "requires": { diff --git a/packages/sasl2/README.md b/packages/sasl2/README.md new file mode 100644 index 00000000..a2d27b96 --- /dev/null +++ b/packages/sasl2/README.md @@ -0,0 +1,79 @@ +# SASL + +SASL2 Negotiation for `@xmpp/client` (including optional BIND2 and FAST). + +Note that if you set clientId then BIND2 will be used so you will not get offline messages (and are expected to do a MAM sync instead if you want that). + +Included and enabled in `@xmpp/client`. + +## Usage + +### object + +```js +const {xmpp} = require('@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 +const { xmpp } = require("@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; + + if (fast) { + const [token, count] = await db.lookupFast(clientId); + if (token) { + await db.incrementFastCount(clientId); + return callback( + { + username: await prompt("enter username"), + password: token, + fastCount: count, + }, + fast, + ); + } + } + + return callback( + { + username: await prompt("enter username"), + password: await prompt("enter password"), + requestToken: fast, + }, + mech, + ); +} +``` + +## References + +[SASL2](https://xmpp.org/extensions/xep-0388.html) +[BIND2](https://xmpp.org/extensions/xep-0386.html) +[FAST](https://xmpp.org/extensions/inbox/xep-fast.html) diff --git a/packages/sasl2/index.js b/packages/sasl2/index.js new file mode 100644 index 00000000..6c3a68c2 --- /dev/null +++ b/packages/sasl2/index.js @@ -0,0 +1,184 @@ +"use strict"; + +const { encode, decode } = require("@xmpp/base64"); +const SASLError = require("./lib/SASLError"); +var jid = require("@xmpp/jid"); +const xml = require("@xmpp/xml"); +const SASLFactory = require("saslmechanisms"); + +// https://xmpp.org/rfcs/rfc6120.html#sasl + +const NS = "urn:xmpp:sasl:2"; +const BIND2_NS = "urn:xmpp:bind:0"; +const FAST_NS = "urn:xmpp:fast:0"; + +async function authenticate( + SASL, + entity, + mechname, + credentials, + userAgent, + features, +) { + const mech = SASL.create([mechname]); + if (!mech) { + throw new Error("No compatible mechanism"); + } + + 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; + } + + switch (element.name) { + case "failure": + reject(SASLError.fromElement(element)); + break; + case "continue": + // No tasks supported yet + reject(); + break; + case "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); + } + } + const token = element.getChild("token", FAST_NS); + if (token) { + entity.emit("fast-token", token); + } + resolve(); + break; + } + } + + entity.removeListener("nonza", handler); + }; + + entity.on("nonza", handler); + + const bind2 = features + .getChild("authentication", NS) + .getChild("inline") + ?.getChild("bind", BIND2_NS); + + 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), + ], + ), + bind2 != null && + userAgent?.clientId && + xml("bind", { xmlns: BIND2_NS }, [ + userAgent?.software && xml("tag", {}, userAgent.software), + ]), + credentials.requestToken && + xml( + "request-token", + { xmlns: FAST_NS, mechanism: credentials.requestToken }, + [], + ), + (credentials.fastCount || credentials.fastCount === 0) && + xml("fast", { xmlns: FAST_NS, count: credentials.fastCount }, []), + ]), + ); + }); +} + +module.exports = function sasl({ streamFeatures }, credentials, userAgent) { + const SASL = new SASLFactory(); + + streamFeatures.use("authentication", NS, async ({ stanza, entity }) => { + const offered = new Set( + stanza + .getChild("authentication", NS) + .getChildren("mechanism", NS) + .map((m) => m.text()), + ); + const fast = new Set( + stanza + .getChild("authentication", NS) + .getChild("inline") + ?.getChild("fast", FAST_NS) + ?.getChildren("mechanism", FAST_NS) + ?.map((m) => m.text()) || [], + ); + const supported = SASL._mechs.map(({ name }) => name); + // eslint-disable-next-line unicorn/prefer-array-find + const intersection = supported + .map((mech) => ({ + name: mech, + canFast: fast.has(mech), + canOther: offered.has(mech), + })) + .filter((mech) => mech.canFast || mech.canOther); + + if (typeof credentials === "function") { + await credentials( + (creds, mech) => + authenticate(SASL, entity, mech, creds, userAgent, stanza), + intersection, + ); + } else { + let mech = intersection[0]?.name; + if (!credentials.username && !credentials.password) { + mech = "ANONYMOUS"; + } + + await authenticate(SASL, entity, mech, credentials, userAgent, stanza); + } + + return true; // Not online yet, wait for next features + }); + + return { + use(...args) { + return SASL.use(...args); + }, + }; +}; diff --git a/packages/sasl2/lib/SASLError.js b/packages/sasl2/lib/SASLError.js new file mode 100644 index 00000000..d7f17efb --- /dev/null +++ b/packages/sasl2/lib/SASLError.js @@ -0,0 +1,14 @@ +"use strict"; + +const XMPPError = require("@xmpp/error"); + +// https://xmpp.org/rfcs/rfc6120.html#sasl-errors + +class SASLError extends XMPPError { + constructor(...args) { + super(...args); + this.name = "SASLError"; + } +} + +module.exports = SASLError; diff --git a/packages/sasl2/package.json b/packages/sasl2/package.json new file mode 100644 index 00000000..e51f9756 --- /dev/null +++ b/packages/sasl2/package.json @@ -0,0 +1,26 @@ +{ + "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", + "keywords": [ + "XMPP", + "sasl" + ], + "dependencies": { + "@xmpp/base64": "^0.13.0", + "@xmpp/error": "^0.13.0", + "@xmpp/jid": "^0.13.0", + "@xmpp/xml": "^0.13.0", + "saslmechanisms": "^0.1.1" + }, + "engines": { + "node": ">= 14" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/xmpp.js/package.json b/packages/xmpp.js/package.json index c487b8ff..45ca9138 100644 --- a/packages/xmpp.js/package.json +++ b/packages/xmpp.js/package.json @@ -38,6 +38,7 @@ "@xmpp/sasl-ht-sha-256-none": "^0.13.0", "@xmpp/sasl-plain": "^0.13.0", "@xmpp/sasl-scram-sha-1": "^0.13.0", + "@xmpp/sasl2": "^0.13.0", "@xmpp/session-establishment": "^0.13.0", "@xmpp/starttls": "^0.13.1", "@xmpp/stream-features": "^0.13.0", From 2d3bde98dff69a5ea96a54fe54edbb21259532c8 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 21 Nov 2023 15:15:27 -0500 Subject: [PATCH 04/27] Use SASL2 from client --- packages/client/browser.js | 17 ++++++++++++++--- packages/client/index.js | 12 +++++++++++- packages/client/package.json | 2 ++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/client/browser.js b/packages/client/browser.js index 8751aea7..adf76d3b 100644 --- a/packages/client/browser.js +++ b/packages/client/browser.js @@ -12,6 +12,7 @@ const _iqCallee = require("@xmpp/iq/callee"); const _resolve = require("@xmpp/resolve"); // Stream features - order matters and define priority +const _sasl2 = require("@xmpp/sasl2"); const _sasl = require("@xmpp/sasl"); const _resourceBinding = require("@xmpp/resource-binding"); const _sessionEstablishment = require("@xmpp/session-establishment"); @@ -19,10 +20,12 @@ const _streamManagement = require("@xmpp/stream-management"); // SASL mechanisms - order matters and define priority const anonymous = require("@xmpp/sasl-anonymous"); +const htsha256 = require("@xmpp/sasl-ht-sha-256-none"); const plain = require("@xmpp/sasl-plain"); function client(options = {}) { const { resource, credentials, username, password, ...params } = options; + const { clientId, software, device } = params; const { domain, service } = params; if (!domain && service) { @@ -40,6 +43,11 @@ function client(options = {}) { const iqCallee = _iqCallee({ middleware, entity }); const resolve = _resolve({ entity }); // Stream features - order matters and define priority + const sasl2 = _sasl2( + { streamFeatures }, + credentials || { username, password }, + { clientId, software, device }, + ); const sasl = _sasl({ streamFeatures }, credentials || { username, password }); const streamManagement = _streamManagement({ streamFeatures, @@ -55,9 +63,11 @@ function client(options = {}) { streamFeatures, }); // SASL mechanisms - order matters and define priority - const mechanisms = Object.entries({ plain, anonymous }).map(([k, v]) => ({ - [k]: v(sasl), - })); + const mechanisms = Object.entries({ + htsha256, + plain, + anonymous, + }).map(([k, v]) => ({ [k]: [v(sasl2), v(sasl)] })); return Object.assign(entity, { entity, @@ -68,6 +78,7 @@ function client(options = {}) { iqCaller, iqCallee, resolve, + sasl2, sasl, resourceBinding, sessionEstablishment, diff --git a/packages/client/index.js b/packages/client/index.js index 1f1d46b0..5c80566d 100644 --- a/packages/client/index.js +++ b/packages/client/index.js @@ -15,6 +15,7 @@ const _resolve = require("@xmpp/resolve"); // Stream features - order matters and define priority const _starttls = require("@xmpp/starttls/client"); +const _sasl2 = require("@xmpp/sasl2"); const _sasl = require("@xmpp/sasl"); const _resourceBinding = require("@xmpp/resource-binding"); const _sessionEstablishment = require("@xmpp/session-establishment"); @@ -22,11 +23,13 @@ const _streamManagement = require("@xmpp/stream-management"); // SASL mechanisms - order matters and define priority const scramsha1 = require("@xmpp/sasl-scram-sha-1"); +const htsha256 = require("@xmpp/sasl-ht-sha-256-none"); const plain = require("@xmpp/sasl-plain"); const anonymous = require("@xmpp/sasl-anonymous"); function client(options = {}) { const { resource, credentials, username, password, ...params } = options; + const { clientId, software, device } = params; const { domain, service } = params; if (!domain && service) { @@ -47,6 +50,11 @@ function client(options = {}) { const resolve = _resolve({ entity }); // Stream features - order matters and define priority const starttls = _starttls({ streamFeatures }); + const sasl2 = _sasl2( + { streamFeatures }, + credentials || { username, password }, + { clientId, software, device }, + ); const sasl = _sasl({ streamFeatures }, credentials || { username, password }); const streamManagement = _streamManagement({ streamFeatures, @@ -64,9 +72,10 @@ function client(options = {}) { // SASL mechanisms - order matters and define priority const mechanisms = Object.entries({ scramsha1, + htsha256, plain, anonymous, - }).map(([k, v]) => ({ [k]: v(sasl) })); + }).map(([k, v]) => ({ [k]: [v(sasl2), v(sasl)] })); return Object.assign(entity, { entity, @@ -80,6 +89,7 @@ function client(options = {}) { iqCallee, resolve, starttls, + sasl2, sasl, resourceBinding, sessionEstablishment, diff --git a/packages/client/package.json b/packages/client/package.json index 8bc2a4a8..d9c94e2b 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -16,10 +16,12 @@ "@xmpp/reconnect": "^0.13.0", "@xmpp/resolve": "^0.13.1", "@xmpp/resource-binding": "^0.13.0", + "@xmpp/sasl2": "^0.13.0", "@xmpp/sasl": "^0.13.0", "@xmpp/sasl-anonymous": "^0.13.0", "@xmpp/sasl-plain": "^0.13.0", "@xmpp/sasl-scram-sha-1": "^0.13.0", + "@xmpp/sasl-ht-sha-256-none": "^0.13.0", "@xmpp/session-establishment": "^0.13.0", "@xmpp/starttls": "^0.13.1", "@xmpp/stream-features": "^0.13.0", From 46f3ee6b2f648b44a7994dff3c1a212a1cdc0c7e Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 22 Nov 2023 15:42:53 -0500 Subject: [PATCH 05/27] Allow other modules to inline into sasl2/bind2 --- packages/sasl2/index.js | 131 ++++++++++++++++++++++++++++------------ 1 file changed, 94 insertions(+), 37 deletions(-) diff --git a/packages/sasl2/index.js b/packages/sasl2/index.js index 6c3a68c2..0085b39c 100644 --- a/packages/sasl2/index.js +++ b/packages/sasl2/index.js @@ -14,6 +14,8 @@ const FAST_NS = "urn:xmpp:fast:0"; async function authenticate( SASL, + inlineHandlers, + bindInlineHandlers, entity, mechname, credentials, @@ -37,7 +39,7 @@ async function authenticate( ...credentials, }; - return new Promise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { const handler = (element) => { if (element.attrs.xmlns !== NS) { return; @@ -84,7 +86,7 @@ async function authenticate( if (token) { entity.emit("fast-token", token); } - resolve(); + resolve(element); break; } } @@ -93,45 +95,76 @@ async function authenticate( }; entity.on("nonza", handler); + }); - const bind2 = features - .getChild("authentication", NS) - .getChild("inline") - ?.getChild("bind", BIND2_NS); + const sendInline = []; + const hPromises = []; + const inline = features.getChild("authentication", NS).getChild("inline"); + for (const el of inline?.children || []) { + const h = inlineHandlers["{" + el.attrs.xmlns + "}" + el.name]; + if (h) { + hPromises.push( + h(el, (addEl) => { + sendInline.push(addEl); + return promise; + }), + ); + } + } - 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), - ], - ), - bind2 != null && - userAgent?.clientId && - xml("bind", { xmlns: BIND2_NS }, [ - userAgent?.software && xml("tag", {}, userAgent.software), - ]), - credentials.requestToken && - xml( - "request-token", - { xmlns: FAST_NS, mechanism: credentials.requestToken }, - [], - ), - (credentials.fastCount || credentials.fastCount === 0) && - xml("fast", { xmlns: FAST_NS, count: credentials.fastCount }, []), - ]), - ); - }); + const bindInline = []; + const bind2 = inline?.getChild("bind", BIND2_NS); + for (const el of bind2?.getChild("inline")?.getChildren("feature") || []) { + const h = bindInlineHandlers[el.attrs.var]; + if (h) { + hPromises.push( + h((addEl) => { + bindInline.push(addEl); + return promise; + }), + ); + } + } + + 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), + ], + ), + bind2 != null && + userAgent?.clientId && + xml("bind", { xmlns: BIND2_NS }, [ + userAgent?.software && xml("tag", {}, userAgent.software), + ...bindInline, + ]), + credentials.requestToken && + xml( + "request-token", + { xmlns: FAST_NS, mechanism: credentials.requestToken }, + [], + ), + (credentials.fastCount || credentials.fastCount === 0) && + xml("fast", { xmlns: FAST_NS, count: credentials.fastCount }, []), + ...sendInline, + ]), + ); + + await promise; + await Promise.all(hPromises); } module.exports = function sasl({ streamFeatures }, credentials, userAgent) { const SASL = new SASLFactory(); + const handlers = {}; + const bindHandlers = {}; streamFeatures.use("authentication", NS, async ({ stanza, entity }) => { const offered = new Set( @@ -161,7 +194,16 @@ module.exports = function sasl({ streamFeatures }, credentials, userAgent) { if (typeof credentials === "function") { await credentials( (creds, mech) => - authenticate(SASL, entity, mech, creds, userAgent, stanza), + authenticate( + SASL, + handlers, + bindHandlers, + entity, + mech, + creds, + userAgent, + stanza, + ), intersection, ); } else { @@ -170,7 +212,16 @@ module.exports = function sasl({ streamFeatures }, credentials, userAgent) { mech = "ANONYMOUS"; } - await authenticate(SASL, entity, mech, credentials, userAgent, stanza); + await authenticate( + SASL, + handlers, + bindHandlers, + entity, + mech, + credentials, + userAgent, + stanza, + ); } return true; // Not online yet, wait for next features @@ -180,5 +231,11 @@ module.exports = function sasl({ streamFeatures }, credentials, userAgent) { use(...args) { return SASL.use(...args); }, + inline(name, xmlns, handler) { + handlers["{" + xmlns + "}" + name] = handler; + }, + bindInline(feature, handler) { + bindHandlers[feature] = handler; + }, }; }; From 917f30b43607dc0b8f835285c06f16b2977ce013 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 22 Nov 2023 15:45:40 -0500 Subject: [PATCH 06/27] Inline stream management into sasl2/bind2 --- packages/client/browser.js | 1 + packages/client/index.js | 1 + packages/stream-features/route.js | 4 +- packages/stream-management/index.js | 64 +++++++++++++++++++++++++---- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/packages/client/browser.js b/packages/client/browser.js index adf76d3b..5ca42e44 100644 --- a/packages/client/browser.js +++ b/packages/client/browser.js @@ -53,6 +53,7 @@ function client(options = {}) { streamFeatures, entity, middleware, + sasl2, }); const resourceBinding = _resourceBinding( { iqCaller, streamFeatures }, diff --git a/packages/client/index.js b/packages/client/index.js index 5c80566d..f582612e 100644 --- a/packages/client/index.js +++ b/packages/client/index.js @@ -60,6 +60,7 @@ function client(options = {}) { streamFeatures, entity, middleware, + sasl2, }); const resourceBinding = _resourceBinding( { iqCaller, streamFeatures }, diff --git a/packages/stream-features/route.js b/packages/stream-features/route.js index bd6d64b5..48e3dd3c 100644 --- a/packages/stream-features/route.js +++ b/packages/stream-features/route.js @@ -6,6 +6,8 @@ module.exports = function route() { return next(); const prevent = await next(); - if (!prevent && entity.jid) entity._status("online", entity.jid); + if (!prevent && entity.jid && entity.status !== "online") { + entity._status("online", entity.jid); + } }; }; diff --git a/packages/stream-management/index.js b/packages/stream-management/index.js index c66dd140..af416744 100644 --- a/packages/stream-management/index.js +++ b/packages/stream-management/index.js @@ -44,6 +44,7 @@ module.exports = function streamManagement({ streamFeatures, entity, middleware, + sasl2, }) { let address = null; @@ -88,24 +89,30 @@ module.exports = function streamManagement({ // https://xmpp.org/extensions/xep-0198.html#enable // For client-to-server connections, the client MUST NOT attempt to enable stream management until after it has completed Resource Binding unless it is resuming a previous session + const resumeSuccess = () => { + sm.enabled = true; + if (address) entity.jid = address; + entity.status = "online"; + }; + + const resumeFailed = () => { + sm.id = ""; + sm.enabled = false; + sm.outbound = 0; + }; + streamFeatures.use("sm", NS, async (context, next) => { // Resuming if (sm.id) { try { - await resume(entity, sm.inbound, sm.id); - sm.enabled = true; - entity.jid = address; - entity.status = "online"; + resumeSuccess(await resume(entity, sm.inbound, sm.id)); return true; // If resumption fails, continue with session establishment // eslint-disable-next-line no-unused-vars } catch { - sm.id = ""; - sm.enabled = false; - sm.outbound = 0; + resumeFailed(); } } - // Enabling // Resource binding first @@ -129,5 +136,46 @@ module.exports = function streamManagement({ sm.inbound = 0; }); + sasl2?.inline("sm", NS, async (_, addInline) => { + if (sm.id) { + const success = await addInline( + xml("resume", { xmlns: NS, h: sm.inbound, previd: sm.id }), + ); + const resumed = success.getChild("resumed", NS); + if (resumed) { + resumeSuccess(resumed); + } else { + resumeFailed(); + } + } + }); + + sasl2?.bindInline(NS, async (addInline) => { + const success = await addInline( + xml("enable", { + xmlns: NS, + max: sm.preferredMaximum, + resume: sm.allowResume ? "true" : undefined, + }), + ); + const bound = success.getChild("bound", "urn:xmpp:bind:0"); + if (!bound) return; // Did a resume or something, don't need this + + const enabled = bound?.getChild("enabled", NS); + if (enabled) { + if (sm.outbound_q.length > 0) { + throw "Stream Management assertion failure, queue should be empty after enable"; + } + sm.outbound = 0; + sm.enabled = true; + sm.id = enabled.attrs.id; + sm.max = enabled.attrs.max; + } else { + sm.enabled = false; + } + + sm.inbound = 0; + }); + return sm; }; From d9b0711da494ea045565f4c86987b2e0450d463e Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 3 Jul 2024 21:57:52 -0500 Subject: [PATCH 07/27] Add SASL2/BIND2/FAST tests --- packages/sasl2/test.js | 288 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 packages/sasl2/test.js diff --git a/packages/sasl2/test.js b/packages/sasl2/test.js new file mode 100644 index 00000000..be2afd59 --- /dev/null +++ b/packages/sasl2/test.js @@ -0,0 +1,288 @@ +"use strict"; + +const test = require("ava"); +const { mockClient, promise } = require("@xmpp/test"); +const parse = require("@xmpp/xml/lib/parse.js"); + +const username = "foo"; +const password = "bar"; +const credentials = { username, password }; + +test("no compatibles mechanisms", async (t) => { + const { entity } = mockClient({ username, password }); + + entity.mockInput( + + + FOO + + , + ); + + const error = await promise(entity, "error"); + t.true(error instanceof Error); + t.is(error.message, "No compatible mechanism"); +}); + +test("with object credentials", async (t) => { + const { entity } = mockClient({ credentials }); + + entity.mockInput( + + + PLAIN + + , + ); + + t.deepEqual( + await promise(entity, "send"), + + AGZvbwBiYXI= + , + ); + + entity.mockInput(); + entity.mockInput(); + + await promise(entity, "online"); +}); + +test("with function credentials", async (t) => { + const mech = "PLAIN"; + + function authenticate(auth, mechanisms) { + t.deepEqual(mechanisms, [{ name: mech, canFast: false, canOther: true }]); + return auth(credentials, mech); + } + + const { entity } = mockClient({ credentials: authenticate }); + + entity.mockInput( + + + {mech} + + , + ); + + t.deepEqual( + await promise(entity, "send"), + + AGZvbwBiYXI= + , + ); + + entity.mockInput(); + entity.mockInput(); + + await promise(entity, "online"); +}); + +test("failure", async (t) => { + const { entity } = mockClient({ credentials }); + + entity.mockInput( + + + PLAIN + + , + ); + + t.deepEqual( + await promise(entity, "send"), + + AGZvbwBiYXI= + , + ); + + const failure = ( + + + + ); + + entity.mockInput(failure); + + const error = await promise(entity, "error"); + t.true(error instanceof Error); + t.is(error.name, "SASLError"); + t.is(error.condition, "some-condition"); + t.is(error.element, failure); +}); + +test("prefers SCRAM-SHA-1", async (t) => { + const { entity } = mockClient({ credentials }); + + entity.mockInput( + + + ANONYMOUS + PLAIN + SCRAM-SHA-1 + + , + ); + + const result = await promise(entity, "send"); + t.deepEqual(result.attrs.mechanism, "SCRAM-SHA-1"); +}); + +test("use ANONYMOUS if username and password are not provided", async (t) => { + const { entity } = mockClient(); + + entity.mockInput( + + + ANONYMOUS + PLAIN + SCRAM-SHA-1 + + , + ); + + const result = await promise(entity, "send"); + t.deepEqual(result.attrs.mechanism, "ANONYMOUS"); +}); + +test("with whitespaces", async (t) => { + const { entity } = mockClient(); + + entity.mockInput( + parse( + ` + + + ANONYMOUS + PLAIN + SCRAM-SHA-1 + + + `.trim(), + ), + ); + + const result = await promise(entity, "send"); + t.deepEqual(result.attrs.mechanism, "ANONYMOUS"); +}); + +test("with bind2", async (t) => { + const { entity } = mockClient({ + credentials, + clientId: "uniqueid", + software: "xmpp.js", + device: "script", + }); + + entity.mockInput( + + + PLAIN + + + + + , + ); + + t.deepEqual( + await promise(entity, "send"), + + AGZvbwBiYXI= + + xmpp.js + script + + + xmpp.js + + , + ); + + entity.mockInput(); + entity.mockInput(); + + await promise(entity, "online"); +}); + +test("with FAST", async (t) => { + const { entity } = mockClient({ + credentials: (callback, mechanisms) => { + t.deepEqual(mechanisms, [ + { canFast: true, canOther: false, name: "HT-SHA-256-NONE" }, + { canFast: false, canOther: true, name: "PLAIN" }, + ]); + callback( + { ...credentials, requestToken: mechanisms[0].name }, + mechanisms[1].name, + ); + }, + }); + + entity.mockInput( + + + PLAIN + + + HT-SHA-256-NONE + + + + , + ); + + t.deepEqual( + await promise(entity, "send"), + + AGZvbwBiYXI= + + , + ); + + entity.mockInput(); + entity.mockInput(); + + await promise(entity, "online"); +}); + +test("with FAST token", async (t) => { + const { entity } = mockClient({ + credentials: (callback, mechanisms) => { + t.deepEqual(mechanisms, [ + { canFast: true, canOther: false, name: "HT-SHA-256-NONE" }, + { canFast: false, canOther: true, name: "PLAIN" }, + ]); + callback({ password: "TOKEN", fastCount: 2 }, mechanisms[0].name); + }, + }); + + entity.mockInput( + + + PLAIN + + + HT-SHA-256-NONE + + + + , + ); + + t.deepEqual( + await promise(entity, "send"), + + + bnVsbAAAXAywUfR/w4Mr9SUDUtNAgPDajNI073fqfiZLMYcmfA== + + + , + ); + + entity.mockInput(); + entity.mockInput(); + + await promise(entity, "online"); +}); From 42cccdb8ec6d262209f8062d74bc15abe12573e2 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Thu, 12 Dec 2024 14:25:23 +0100 Subject: [PATCH 08/27] fix eslint --- package-lock.json | 2 ++ packages/sasl-ht-sha-256-none/index.js | 2 +- packages/sasl2/index.js | 8 +++++--- packages/xmpp.js/package.json | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5a09e789..6a5886c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15997,6 +15997,7 @@ } }, "packages/sasl-ht-sha-256-none": { + "name": "@xmpp/sasl-ht-sha-256-none", "version": "0.13.0", "license": "ISC", "dependencies": { @@ -16029,6 +16030,7 @@ } }, "packages/sasl2": { + "name": "@xmpp/sasl2", "version": "0.13.0", "license": "ISC", "dependencies": { diff --git a/packages/sasl-ht-sha-256-none/index.js b/packages/sasl-ht-sha-256-none/index.js index d784e89b..2a3c3d65 100644 --- a/packages/sasl-ht-sha-256-none/index.js +++ b/packages/sasl-ht-sha-256-none/index.js @@ -23,6 +23,6 @@ Mechanism.prototype.final = (data) => { } }; -module.exports = (sasl) => { +module.exports = function sasl2(sasl) { sasl.use(Mechanism); }; diff --git a/packages/sasl2/index.js b/packages/sasl2/index.js index 0085b39c..7d09e1e2 100644 --- a/packages/sasl2/index.js +++ b/packages/sasl2/index.js @@ -59,13 +59,15 @@ async function authenticate( } switch (element.name) { - case "failure": + case "failure": { reject(SASLError.fromElement(element)); break; - case "continue": + } + case "continue": { // No tasks supported yet reject(); break; + } case "success": { const additionalData = element.getChild("additional-data")?.text(); if (additionalData && mech.final) { @@ -182,7 +184,7 @@ module.exports = function sasl({ streamFeatures }, credentials, userAgent) { ?.map((m) => m.text()) || [], ); const supported = SASL._mechs.map(({ name }) => name); - // eslint-disable-next-line unicorn/prefer-array-find + const intersection = supported .map((mech) => ({ name: mech, diff --git a/packages/xmpp.js/package.json b/packages/xmpp.js/package.json index 371ebab2..1cb558bd 100644 --- a/packages/xmpp.js/package.json +++ b/packages/xmpp.js/package.json @@ -33,12 +33,12 @@ "@xmpp/reconnect": "^0.13.2", "@xmpp/resolve": "^0.13.2", "@xmpp/resource-binding": "^0.13.2", - "@xmpp/sasl2": "^0.13.0", "@xmpp/sasl": "^0.13.2", "@xmpp/sasl-anonymous": "^0.13.2", + "@xmpp/sasl-ht-sha-256-none": "^0.13.0", "@xmpp/sasl-plain": "^0.13.2", "@xmpp/sasl-scram-sha-1": "^0.13.2", - "@xmpp/sasl-ht-sha-256-none": "^0.13.0", + "@xmpp/sasl2": "^0.13.0", "@xmpp/session-establishment": "^0.13.2", "@xmpp/starttls": "^0.13.2", "@xmpp/stream-features": "^0.13.2", From 296000d6c7aec3223d2465e9ca2c495eb62b5183 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Thu, 12 Dec 2024 14:27:33 +0100 Subject: [PATCH 09/27] Remove sasl-ht-sha-256-none from @xmpp/client browser FAIL ./packages/client/dist/xmpp.min.js: 45.01KB > maxSize 16KB (gzip) --- package.json | 2 +- packages/client/browser.js | 2 -- packages/sasl2/index.js | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index ff5909d3..bb992683 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "bundlesize": [ { "path": "./packages/client/dist/xmpp.min.js", - "maxSize": "16 KB" + "maxSize": "17 KB" } ], "lint-staged": { diff --git a/packages/client/browser.js b/packages/client/browser.js index 5ca42e44..cece2dc5 100644 --- a/packages/client/browser.js +++ b/packages/client/browser.js @@ -20,7 +20,6 @@ const _streamManagement = require("@xmpp/stream-management"); // SASL mechanisms - order matters and define priority const anonymous = require("@xmpp/sasl-anonymous"); -const htsha256 = require("@xmpp/sasl-ht-sha-256-none"); const plain = require("@xmpp/sasl-plain"); function client(options = {}) { @@ -65,7 +64,6 @@ function client(options = {}) { }); // SASL mechanisms - order matters and define priority const mechanisms = Object.entries({ - htsha256, plain, anonymous, }).map(([k, v]) => ({ [k]: [v(sasl2), v(sasl)] })); diff --git a/packages/sasl2/index.js b/packages/sasl2/index.js index 7d09e1e2..2b2fe037 100644 --- a/packages/sasl2/index.js +++ b/packages/sasl2/index.js @@ -2,7 +2,7 @@ const { encode, decode } = require("@xmpp/base64"); const SASLError = require("./lib/SASLError"); -var jid = require("@xmpp/jid"); +const jid = require("@xmpp/jid"); const xml = require("@xmpp/xml"); const SASLFactory = require("saslmechanisms"); @@ -163,7 +163,7 @@ async function authenticate( await Promise.all(hPromises); } -module.exports = function sasl({ streamFeatures }, credentials, userAgent) { +module.exports = function sasl2({ streamFeatures }, credentials, userAgent) { const SASL = new SASLFactory(); const handlers = {}; const bindHandlers = {}; From b8e0fac1c7c6086ed0c3d4280a0f74e2288c9851 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 18 Dec 2024 12:31:21 -0500 Subject: [PATCH 10/27] Add doc comment to sasl-h2-sha-256-none --- packages/sasl-ht-sha-256-none/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sasl-ht-sha-256-none/index.js b/packages/sasl-ht-sha-256-none/index.js index 2a3c3d65..07a37531 100644 --- a/packages/sasl-ht-sha-256-none/index.js +++ b/packages/sasl-ht-sha-256-none/index.js @@ -1,6 +1,7 @@ "use strict"; -var createHmac = require("create-hmac"); +// https://datatracker.ietf.org/doc/draft-schmaus-kitten-sasl-ht/ +const createHmac = require("create-hmac"); function Mechanism() {} From 9f6ad2f96b81a26c6b2346ca99170567efd7b1ee Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 18 Dec 2024 12:34:50 -0500 Subject: [PATCH 11/27] Update sasl2 doc link comment --- packages/sasl2/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sasl2/index.js b/packages/sasl2/index.js index 2b2fe037..c2d3da14 100644 --- a/packages/sasl2/index.js +++ b/packages/sasl2/index.js @@ -6,7 +6,9 @@ const jid = require("@xmpp/jid"); const xml = require("@xmpp/xml"); const SASLFactory = require("saslmechanisms"); -// https://xmpp.org/rfcs/rfc6120.html#sasl +// https://xmpp.org/extensions/xep-0388.html +// https://xmpp.org/extensions/xep-0386.html +// https://xmpp.org/extensions/xep-0484.html const NS = "urn:xmpp:sasl:2"; const BIND2_NS = "urn:xmpp:bind:0"; From 675aab9f576ad79b96b419918ab1b6fce48f3366 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 18 Dec 2024 12:38:37 -0500 Subject: [PATCH 12/27] Add explanatory comment about bind2 inline setting to online --- packages/stream-features/route.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream-features/route.js b/packages/stream-features/route.js index 48e3dd3c..b785d9b5 100644 --- a/packages/stream-features/route.js +++ b/packages/stream-features/route.js @@ -6,6 +6,7 @@ module.exports = function route() { return next(); const prevent = await next(); + // BIND2 inline handler may have already set to online, eg inline SM resume if (!prevent && entity.jid && entity.status !== "online") { entity._status("online", entity.jid); } From 30ca0a7a4e6815d54b8c4b0167fa2dcd539b21dd Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 18 Dec 2024 12:40:05 -0500 Subject: [PATCH 13/27] Explanation of setting stream from --- packages/connection/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/connection/index.js b/packages/connection/index.js index 1705df5a..b5a8b189 100644 --- a/packages/connection/index.js +++ b/packages/connection/index.js @@ -263,6 +263,9 @@ class Connection extends EventEmitter { this.socket.secure() && (this.streamFrom || this.jid) ) { + // When the stream is secure there is no leak to setting the stream from + // This is suggested in general and in required for FAST implementations + // in particular headerElement.attrs.from = (this.streamFrom || this.jid).toString(); } headerElement.attrs["xml:lang"] = lang; From f2e29facb2c6a7d108340a7eb223b57c7982b2bc Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 18 Dec 2024 14:11:42 -0500 Subject: [PATCH 14/27] Workaround for bug in babel-plugin-transform-async-to-promises --- packages/sasl2/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/sasl2/index.js b/packages/sasl2/index.js index c2d3da14..a026f9d6 100644 --- a/packages/sasl2/index.js +++ b/packages/sasl2/index.js @@ -161,8 +161,7 @@ async function authenticate( ]), ); - await promise; - await Promise.all(hPromises); + await Promise.all([promise, ...hPromises]); } module.exports = function sasl2({ streamFeatures }, credentials, userAgent) { From 2fd700317c866d18ef3c1f5b9f3891555d23130a Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 18 Dec 2024 14:12:31 -0500 Subject: [PATCH 15/27] Enable SASL2 for test server --- server/modules/mod_sasl2.lua | 228 ++++++++++++++++++++++++ server/modules/mod_sasl2_bind2.lua | 110 ++++++++++++ server/modules/mod_sasl2_fast.lua | 271 +++++++++++++++++++++++++++++ server/modules/mod_sasl2_sm.lua | 90 ++++++++++ server/prosody.cfg.lua | 6 + 5 files changed, 705 insertions(+) create mode 100644 server/modules/mod_sasl2.lua create mode 100644 server/modules/mod_sasl2_bind2.lua create mode 100644 server/modules/mod_sasl2_fast.lua create mode 100644 server/modules/mod_sasl2_sm.lua diff --git a/server/modules/mod_sasl2.lua b/server/modules/mod_sasl2.lua new file mode 100644 index 00000000..9eeff4b1 --- /dev/null +++ b/server/modules/mod_sasl2.lua @@ -0,0 +1,228 @@ +-- Prosody IM +-- Copyright (C) 2019 Kim Alvefur +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +-- XEP-0388: Extensible SASL Profile +-- + +local st = require "util.stanza"; +local errors = require "util.error"; +local base64 = require "util.encodings".base64; +local jid_join = require "util.jid".join; +local set = require "util.set"; + +local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; +local sm_make_authenticated = require "core.sessionmanager".make_authenticated; + +local xmlns_sasl2 = "urn:xmpp:sasl:2"; + +local secure_auth_only = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", true)); +local allow_unencrypted_plain_auth = module:get_option_boolean("allow_unencrypted_plain_auth", false) +local insecure_mechanisms = module:get_option_set("insecure_sasl_mechanisms", allow_unencrypted_plain_auth and {} or {"PLAIN", "LOGIN"}); +local disabled_mechanisms = module:get_option_set("disable_sasl_mechanisms", { "DIGEST-MD5" }); + +local host = module.host; + +local function tls_unique(self) + return self.userdata["tls-unique"]:ssl_peerfinished(); +end + +local function tls_exporter(conn) + if not conn.ssl_exportkeyingmaterial then return end + return conn:ssl_exportkeyingmaterial("EXPORTER-Channel-Binding", 32, ""); +end + +local function sasl_tls_exporter(self) + return tls_exporter(self.userdata["tls-exporter"]); +end + +module:hook("stream-features", function(event) + local origin, features = event.origin, event.features; + local log = origin.log or module._log; + + if origin.type ~= "c2s_unauthed" then + log("debug", "Already authenticated"); + return + elseif secure_auth_only and not origin.secure then + log("debug", "Not offering authentication on insecure connection"); + return; + end + + local sasl_handler = usermanager_get_sasl_handler(host, origin) + origin.sasl_handler = sasl_handler; + + local channel_bindings = set.new() + if origin.encrypted then + -- check whether LuaSec has the nifty binding to the function needed for tls-unique + -- FIXME: would be nice to have this check only once and not for every socket + if sasl_handler.add_cb_handler then + local info = origin.conn:ssl_info(); + if info and info.protocol == "TLSv1.3" then + log("debug", "Channel binding 'tls-unique' undefined in context of TLS 1.3"); + if tls_exporter(origin.conn) then + log("debug", "Channel binding 'tls-exporter' supported"); + sasl_handler:add_cb_handler("tls-exporter", sasl_tls_exporter); + channel_bindings:add("tls-exporter"); + else + log("debug", "Channel binding 'tls-exporter' not supported"); + end + elseif origin.conn.ssl_peerfinished and origin.conn:ssl_peerfinished() then + log("debug", "Channel binding 'tls-unique' supported"); + sasl_handler:add_cb_handler("tls-unique", tls_unique); + channel_bindings:add("tls-unique"); + else + log("debug", "Channel binding 'tls-unique' not supported (by LuaSec?)"); + end + sasl_handler["userdata"] = { + ["tls-unique"] = origin.conn; + ["tls-exporter"] = origin.conn; + }; + else + log("debug", "Channel binding not supported by SASL handler"); + end + end + + local mechanisms = st.stanza("authentication", { xmlns = xmlns_sasl2 }); + + local available_mechanisms = sasl_handler:mechanisms() + for mechanism in pairs(available_mechanisms) do + if disabled_mechanisms:contains(mechanism) then + log("debug", "Not offering disabled mechanism %s", mechanism); + elseif not origin.secure and insecure_mechanisms:contains(mechanism) then + log("debug", "Not offering mechanism %s on insecure connection", mechanism); + else + log("debug", "Offering mechanism %s", mechanism); + mechanisms:text_tag("mechanism", mechanism); + end + end + + features:add_direct_child(mechanisms); + + local inline = st.stanza("inline"); + module:fire_event("advertise-sasl-features", { origin = origin, features = inline, stream = event.stream }); + mechanisms:add_direct_child(inline); +end, 1); + +local function handle_status(session, status, ret, err_msg) + local err = nil; + if status == "error" then + ret, err = nil, ret; + if not errors.is_err(err) then + err = errors.new({ condition = err, text = err_msg }, { session = session }); + end + end + + return module:fire_event("sasl2/"..session.base_type.."/"..status, { + session = session, + message = ret; + error = err; + error_text = err_msg; + }); +end + +module:hook("sasl2/c2s/failure", function (event) + module:fire_event("authentication-failure", event); + local session, condition, text = event.session, event.message, event.error_text; + local failure = st.stanza("failure", { xmlns = xmlns_sasl2 }) + :tag(condition, { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl" }):up(); + if text then + failure:text_tag("text", text); + end + session.send(failure); + return true; +end); + +module:hook("sasl2/c2s/error", function (event) + local session = event.session + session.send(st.stanza("failure", { xmlns = xmlns_sasl2 }) + :tag(event.error and event.error.condition)); + return true; +end); + +module:hook("sasl2/c2s/challenge", function (event) + local session = event.session; + session.send(st.stanza("challenge", { xmlns = xmlns_sasl2 }) + :text(base64.encode(event.message))); + return true; +end); + +module:hook("sasl2/c2s/success", function (event) + local session = event.session + local ok, err = sm_make_authenticated(session, session.sasl_handler.username); + if not ok then + handle_status(session, "failure", err); + return true; + end + event.success = st.stanza("success", { xmlns = xmlns_sasl2 }); + if event.message then + event.success:text_tag("additional-data", base64.encode(event.message)); + end +end, 1000); + +module:hook("sasl2/c2s/success", function (event) + local session = event.session + event.success:text_tag("authorization-identifier", jid_join(session.username, session.host, session.resource)); + session.send(event.success); +end, -1000); + +module:hook("sasl2/c2s/success", function (event) + module:fire_event("authentication-success", event); + local session = event.session; + local features = st.stanza("stream:features"); + module:fire_event("stream-features", { origin = session, features = features }); + session.send(features); +end, -1500); + +-- The gap here is to allow modules to do stuff to the stream after the stanza +-- is sent, but before we proceed with anything else. This is expected to be +-- a common pattern with SASL2, which allows atomic negotiation of a bunch of +-- stream features. +module:hook("sasl2/c2s/success", function (event) --luacheck: ignore 212/event + event.session.sasl_handler = nil; + return true; +end, -2000); + +local function process_cdata(session, cdata) + if cdata then + cdata = base64.decode(cdata); + if not cdata then + return handle_status(session, "failure", "incorrect-encoding"); + end + end + return handle_status(session, session.sasl_handler:process(cdata)); +end + +module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth) + if secure_auth_only and not session.secure then + return handle_status(session, "failure", "encryption-required"); + end + local sasl_handler = session.sasl_handler; + if not sasl_handler then + sasl_handler = usermanager_get_sasl_handler(host, session); + session.sasl_handler = sasl_handler; + end + local mechanism = assert(auth.attr.mechanism); + if not sasl_handler:select(mechanism) then + return handle_status(session, "failure", "invalid-mechanism"); + end + local user_agent = auth:get_child("user-agent"); + if user_agent then + session.client_id = user_agent.attr.id; + sasl_handler.user_agent = { + software = user_agent:get_child_text("software"); + device = user_agent:get_child_text("device"); + }; + end + local initial = auth:get_child_text("initial-response"); + return process_cdata(session, initial); +end); + +module:hook_tag(xmlns_sasl2, "response", function (session, response) + local sasl_handler = session.sasl_handler; + if not sasl_handler or not sasl_handler.selected then + return handle_status(session, "failure", "invalid-mechanism"); + end + return process_cdata(session, response:get_text()); +end); diff --git a/server/modules/mod_sasl2_bind2.lua b/server/modules/mod_sasl2_bind2.lua new file mode 100644 index 00000000..fb02ffd5 --- /dev/null +++ b/server/modules/mod_sasl2_bind2.lua @@ -0,0 +1,110 @@ +local base64 = require "util.encodings".base64; +local id = require "util.id"; +local sha1 = require "util.hashes".sha1; +local st = require "util.stanza"; + +local sm_bind_resource = require "core.sessionmanager".bind_resource; + +local xmlns_bind2 = "urn:xmpp:bind:0"; +local xmlns_sasl2 = "urn:xmpp:sasl:2"; + +module:depends("sasl2"); + +-- Advertise what we can do + +module:hook("advertise-sasl-features", function(event) + local bind = st.stanza("bind", { xmlns = xmlns_bind2 }); + local inline = st.stanza("inline"); + module:fire_event("advertise-bind-features", { origin = event.origin, features = inline }); + bind:add_direct_child(inline); + + event.features:add_direct_child(bind); +end, 1); + +-- Helper to actually bind a resource to a session + +local function do_bind(session, bind_request) + local resource = session.sasl_handler.resource; + + if not resource then + local client_name_tag = bind_request:get_child_text("tag"); + if client_name_tag then + local client_id = session.client_id; + local tag_suffix = client_id and base64.encode(sha1(client_id):sub(1, 9)) or id.medium(); + resource = ("%s~%s"):format(client_name_tag, tag_suffix); + end + end + + local success, err_type, err, err_msg = sm_bind_resource(session, resource); + if not success then + session.log("debug", "Resource bind failed: %s", err_msg or err); + return nil, { type = err_type, condition = err, text = err_msg }; + end + + session.log("debug", "Resource bound: %s", session.full_jid); + return st.stanza("bound", { xmlns = xmlns_bind2 }); +end + +-- Enable inline features requested by the client + +local function enable_features(session, bind_request, bind_result) + module:fire_event("enable-bind-features", { + session = session; + request = bind_request; + result = bind_result; + }); +end + +-- SASL 2 integration + +module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth) + -- Cache action for future processing (after auth success) + session.sasl2_bind_request = auth:child_with_ns(xmlns_bind2); +end, 100); + +module:hook("sasl2/c2s/success", function (event) + local session = event.session; + + local bind_request = session.sasl2_bind_request; + if not bind_request then return; end -- No bind requested + session.sasl2_bind_request = nil; + + local sm_success = session.sasl2_sm_success; + if sm_success and sm_success.type == "resumed" then + return; -- No need to bind a resource + end + + local bind_result, err = do_bind(session, bind_request); + if not bind_result then + bind_result = st.stanza("failed", { xmlns = xmlns_bind2 }) + :add_error(err); + else + enable_features(session, bind_request, bind_result); + end + + event.success:add_child(bind_result); +end, 100); + +-- Inline features + +module:hook("advertise-bind-features", function (event) + local features = event.features; + features:tag("feature", { var = "urn:xmpp:carbons:2" }):up(); + features:tag("feature", { var = "urn:xmpp:csi:0" }):up(); +end); + +module:hook("enable-bind-features", function (event) + local session, request = event.session, event.request; + + -- Carbons + if request:get_child("enable", "urn:xmpp:carbons:2") then + session.want_carbons = true; + event.result:tag("enabled", { xmlns = "urn:xmpp:carbons:2" }):up(); + end + + -- CSI + local csi_state_tag = request:child_with_ns("urn:xmpp:csi:0"); + if csi_state_tag then + session.state = csi_state_tag.name; + end +end, 10); diff --git a/server/modules/mod_sasl2_fast.lua b/server/modules/mod_sasl2_fast.lua new file mode 100644 index 00000000..4a4c4475 --- /dev/null +++ b/server/modules/mod_sasl2_fast.lua @@ -0,0 +1,271 @@ +local usermanager = require "core.usermanager"; + +local sasl = require "util.sasl"; +local dt = require "util.datetime"; +local id = require "util.id"; +local jid = require "util.jid"; +local st = require "util.stanza"; +local now = require "util.time".now; +local hash = require "util.hashes"; + +local sasl_mt = getmetatable(sasl.new("", { mechanisms = {} })); +local function is_util_sasl(sasl_handler) + return getmetatable(sasl_handler) == sasl_mt; +end + +module:depends("sasl2"); + +-- Tokens expire after 21 days by default +local fast_token_ttl = module:get_option_number("sasl2_fast_token_ttl", 86400*21); +-- Tokens are automatically rotated daily +local fast_token_min_ttl = module:get_option_number("sasl2_fast_token_min_ttl", 86400); + +local xmlns_fast = "urn:xmpp:fast:0"; +local xmlns_sasl2 = "urn:xmpp:sasl:2"; + +local token_store = module:open_store("fast_tokens", "map"); + +local log = module._log; + +local function make_token(username, client_id, mechanism) + local new_token = "secret-token:fast-"..id.long(); + local key = hash.sha256(client_id, true).."-new"; + local issued_at = now(); + local token_info = { + mechanism = mechanism; + secret = new_token; + issued_at = issued_at; + expires_at = issued_at + fast_token_ttl; + }; + if not token_store:set(username, key, token_info) then + return nil; + end + return token_info; +end + +local function new_token_tester(hmac_f) + return function (mechanism, username, client_id, token_hash, cb_data, invalidate) + local account_info = usermanager.get_account_info(username, module.host); + local last_password_change = account_info and account_info.password_updated; + local tried_current_token = false; + local key = hash.sha256(client_id, true).."-new"; + local token; + repeat + log("debug", "Looking for %s token %s/%s", mechanism, username, key); + token = token_store:get(username, key); + if token and token.mechanism == mechanism then + local expected_hash = hmac_f(token.secret, "Initiator"..(cb_data or "")); + if hash.equals(expected_hash, token_hash) then + local current_time = now(); + if token.expires_at < current_time then + log("debug", "Token found, but it has expired (%ds ago). Cleaning up...", current_time - token.expires_at); + token_store:set(username, key, nil); + return nil, "credentials-expired"; + elseif last_password_change and token.issued_at < last_password_change then + log("debug", "Token found, but issued prior to password change (%ds ago). Cleaning up...", + current_time - last_password_change + ); + token_store:set(username, key, nil); + return nil, "credentials-expired"; + end + if not tried_current_token and not invalidate then + -- The new token is becoming the current token + token_store:set_keys(username, { + [key] = token_store.remove; + [key:sub(1, -5).."-cur"] = token; + }); + end + local rotation_needed; + if invalidate then + token_store:set(username, key, nil); + elseif current_time - token.issued_at > fast_token_min_ttl then + log("debug", "FAST token due for rotation (age: %d)", current_time - token.issued_at); + rotation_needed = true; + end + return true, username, hmac_f(token.secret, "Responder"..(cb_data or "")), rotation_needed; + end + end + if not tried_current_token then + log("debug", "Trying next token..."); + -- Try again with the current token instead + tried_current_token = true; + key = key:sub(1, -5).."-cur"; + else + log("debug", "No matching %s token found for %s/%s", mechanism, username, key); + return nil; + end + until false; + end +end + +function get_sasl_handler() + local token_auth_profile = { + ht_sha_256 = new_token_tester(hash.hmac_sha256); + }; + local handler = sasl.new(module.host, token_auth_profile); + handler.fast = true; + return handler; +end + +-- Advertise FAST to connecting clients +module:hook("advertise-sasl-features", function (event) + local session = event.origin; + local username = session.username; + if not username then + username = jid.node(event.stream.from); + if not username then return; end + end + local sasl_handler = get_sasl_handler(username); + if not sasl_handler then return; end + sasl_handler.fast_auth = true; -- For informational purposes + -- Copy channel binding info from primary SASL handler if it's compatible + if is_util_sasl(session.sasl_handler) then + sasl_handler.profile.cb = session.sasl_handler.profile.cb; + sasl_handler.userdata = session.sasl_handler.userdata; + end + -- Store this handler, in case we later want to use it for authenticating + session.fast_sasl_handler = sasl_handler; + local fast = st.stanza("fast", { xmlns = xmlns_fast }); + for mech in pairs(sasl_handler:mechanisms()) do + fast:text_tag("mechanism", mech); + end + event.features:add_child(fast); +end); + +-- Process any FAST elements in +module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth) + -- Cache action for future processing (after auth success) + local fast_auth = auth:get_child("fast", xmlns_fast); + if fast_auth then + -- Client says it is using FAST auth, so set our SASL handler + local fast_sasl_handler = session.fast_sasl_handler; + local client_id = auth:get_child_attr("user-agent", nil, "id"); + if fast_sasl_handler and client_id then + session.log("debug", "Client is authenticating using FAST"); + fast_sasl_handler.client_id = client_id; + fast_sasl_handler.profile.cb = session.sasl_handler.profile.cb; + fast_sasl_handler.userdata = session.sasl_handler.userdata; + local invalidate = fast_auth.attr.invalidate; + fast_sasl_handler.invalidate = invalidate == "1" or invalidate == "true"; + -- Set our SASL handler as the session's SASL handler + session.sasl_handler = fast_sasl_handler; + else + session.log("warn", "Client asked to auth via FAST, but SASL handler or client id missing"); + local failure = st.stanza("failure", { xmlns = xmlns_sasl2 }) + :tag("malformed-request"):up() + :text_tag("text", "FAST is not available on this stream"); + session.send(failure); + return true; + end + end + session.fast_sasl_handler = nil; + local fast_token_request = auth:get_child("request-token", xmlns_fast); + if fast_token_request then + local mech = fast_token_request.attr.mechanism; + session.log("debug", "Client requested new FAST token for %s", mech); + session.fast_token_request = { + mechanism = mech; + }; + end +end, 100); + +-- Process post-success (new token generation, etc.) +module:hook("sasl2/c2s/success", function (event) + local session = event.session; + + local token_request = session.fast_token_request; + local client_id = session.client_id; + local sasl_handler = session.sasl_handler; + if token_request or (sasl_handler.fast and sasl_handler.rotation_needed) then + if not client_id then + session.log("warn", "FAST token requested, but missing client id"); + return; + end + local mechanism = token_request and token_request.mechanism or session.sasl_handler.selected; + local token_info = make_token(session.username, client_id, mechanism) + if token_info then + session.log("debug", "Provided new FAST token to client"); + event.success:tag("token", { + xmlns = xmlns_fast; + expiry = dt.datetime(token_info.expires_at); + token = token_info.secret; + }):up(); + end + end +end, 75); + +-- HT-* mechanisms + +local function new_ht_mechanism(mechanism_name, backend_profile_name, cb_name) + return function (sasl_handler, message) + local backend = sasl_handler.profile[backend_profile_name]; + local authc_username, token_hash = message:match("^([^%z]+)%z(.+)$"); + if not authc_username then + return "failure", "malformed-request"; + end + local cb_data; + if cb_name then + if not sasl_handler.profile.cb then + module:log("warn", "Attempt to use channel binding %s with SASL profile that does not support any channel binding (FAST: %s)", cb_name, sasl_handler.fast); + return "failure", "malformed-request"; + elseif not sasl_handler.profile.cb[cb_name] then + module:log("warn", "SASL profile does not support %s channel binding (FAST: %s)", cb_name, sasl_handler.fast); + return "failure", "malformed-request"; + end + cb_data = sasl_handler.profile.cb[cb_name](sasl_handler) or ""; + end + local ok, authz_username, response, rotation_needed = backend( + mechanism_name, + authc_username, + sasl_handler.client_id, + token_hash, + cb_data, + sasl_handler.invalidate + ); + if not ok then + -- authz_username is error condition + return "failure", authz_username or "not-authorized"; + end + sasl_handler.username = authz_username; + sasl_handler.rotation_needed = rotation_needed; + return "success", response; + end +end + +local function register_ht_mechanism(name, backend_profile_name, cb_name) + return sasl.registerMechanism(name, { backend_profile_name }, new_ht_mechanism( + name, + backend_profile_name, + cb_name + ), + cb_name and { cb_name } or nil); +end + +register_ht_mechanism("HT-SHA-256-NONE", "ht_sha_256", nil); +register_ht_mechanism("HT-SHA-256-UNIQ", "ht_sha_256", "tls-unique"); +register_ht_mechanism("HT-SHA-256-ENDP", "ht_sha_256", "tls-server-end-point"); +register_ht_mechanism("HT-SHA-256-EXPR", "ht_sha_256", "tls-exporter"); + +-- Public API + +--luacheck: ignore 131 +function is_client_fast(username, client_id, last_password_change) + local client_id_hash = hash.sha256(client_id, true); + local curr_time = now(); + local cur = token_store:get(username, client_id_hash.."-cur"); + if cur and cur.expires_at >= curr_time and (not last_password_change or last_password_change < cur.issued_at) then + return true; + end + local new = token_store:get(username, client_id_hash.."-new"); + if new and new.expires_at >= curr_time and (not last_password_change or last_password_change < new.issued_at) then + return true; + end + return false; +end + +function revoke_fast_tokens(username, client_id) + local client_id_hash = hash.sha256(client_id, true); + local cur_ok = token_store:set(username, client_id_hash.."-cur", nil); + local new_ok = token_store:set(username, client_id_hash.."-new", nil); + return cur_ok and new_ok; +end diff --git a/server/modules/mod_sasl2_sm.lua b/server/modules/mod_sasl2_sm.lua new file mode 100644 index 00000000..4ed0fb1a --- /dev/null +++ b/server/modules/mod_sasl2_sm.lua @@ -0,0 +1,90 @@ +local st = require "util.stanza"; + +local mod_smacks = module:depends("smacks"); + +local xmlns_sasl2 = "urn:xmpp:sasl:2"; +local xmlns_sm = "urn:xmpp:sm:3"; + +module:depends("sasl2"); + +-- Advertise what we can do + +module:hook("advertise-sasl-features", function (event) + local features = event.features; + features:tag("sm", { xmlns = xmlns_sm }):up(); +end); + +module:hook("advertise-bind-features", function (event) + local features = event.features; + features:tag("feature", { var = xmlns_sm }):up(); +end); + +module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth) + -- Cache action for future processing (after auth success) + session.sasl2_sm_request = auth:child_with_ns(xmlns_sm); +end, 100); + +-- SASL 2 integration (for resume) + +module:hook("sasl2/c2s/success", function (event) + local session = event.session; + local sm_request = session.sasl2_sm_request; + if not sm_request then return; end + session.sasl2_sm_request = nil; + local sm_result; + if sm_request.name ~= "resume" then return; end + + local resumed, err = mod_smacks.do_resume(session, sm_request); + if not resumed then + local h = err.context and err.context.h; + sm_result = st.stanza("failed", { xmlns = xmlns_sm, h = h and ("%d"):format(h) or nil }) + :add_error(err); + else + event.session = resumed.session; -- Update to resumed session + event.session.sasl2_sm_success = resumed; -- To be called after sending final SASL response + sm_result = st.stanza("resumed", { xmlns = xmlns_sm, + h = ("%d"):format(event.session.handled_stanza_count); + previd = resumed.id; }); + end + + if sm_result then + event.success:add_child(sm_result); + end +end, 110); + +-- Bind 2 integration (for enable) + +module:hook("enable-bind-features", function (event) + local sm_enable = event.request:get_child("enable", xmlns_sm); + if not sm_enable then return; end + + local sm_result; + local enabled, err = mod_smacks.do_enable(event.session, sm_enable); + if not enabled then + sm_result = st.stanza("failed", { xmlns = xmlns_sm }) + :add_error(err); + else + event.session.sasl2_sm_success = enabled; -- To be called after sending final SASL response + sm_result = st.stanza("enabled", { + xmlns = xmlns_sm; + id = enabled.id; + resume = enabled.id and "1" or nil; + max = enabled.resume_max; + }); + end + event.result:add_child(sm_result); +end, 100); + +-- Finish and/or clean up after SASL 2 completed + +module:hook("sasl2/c2s/success", function (event) + -- The authenticate response has already been sent at this point + local success = event.session.sasl2_sm_success; + if success then + success.finish(); -- Finish enable/resume and sync stanzas + end +end, -1100); + +module:hook("sasl2/c2s/failure", function (event) + event.session.sasl2_sm_request = nil; +end); diff --git a/server/prosody.cfg.lua b/server/prosody.cfg.lua index 480c0021..1474579f 100644 --- a/server/prosody.cfg.lua +++ b/server/prosody.cfg.lua @@ -4,6 +4,8 @@ local lfs = require "lfs"; +plugin_paths = { "modules" } + modules_enabled = { "roster"; "saslauth"; @@ -18,6 +20,10 @@ modules_enabled = { "time"; "version"; "smacks"; + "sasl2"; + "sasl2_bind2"; + "sasl2_fast"; + "sasl2_sm"; }; modules_disabled = { From 96d50c8e283cffc67ddf195986340a7ffc531bfa Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 18 Dec 2024 14:46:45 -0500 Subject: [PATCH 16/27] e2e tests for bind2 and fast --- packages/stream-management/index.js | 1 + test/client.js | 69 +++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/packages/stream-management/index.js b/packages/stream-management/index.js index f61b6e12..1e46c293 100644 --- a/packages/stream-management/index.js +++ b/packages/stream-management/index.js @@ -56,6 +56,7 @@ module.exports = function streamManagement({ outbound: 0, inbound: 0, max: null, + outbound_q: [], }; entity.on("online", (jid) => { diff --git a/test/client.js b/test/client.js index a4790f45..53be3e1d 100644 --- a/test/client.js +++ b/test/client.js @@ -48,6 +48,75 @@ test.serial("client", async (t) => { t.is(address.bare().toString(), JID); }); +test.serial("bind2", async (t) => { + t.plan(6); + + const xmpp = client({ + credentials, + service: domain, + clientId: "75b2d490-3e3b-4d96-bca1-624b30ea6f82", + software: "xmpp.js tests", + device: "Test Device", + }); + t.context.xmpp = xmpp; + debug(xmpp); + + xmpp.on("connect", () => { + t.pass(); + }); + + xmpp.once("open", (el) => { + t.true(el instanceof xml.Element); + }); + + xmpp.on("online", (address) => { + t.true(address instanceof jid.JID); + t.is(address.bare().toString(), JID); + }); + + const address = await xmpp.start(); + t.true(address instanceof jid.JID); + t.is(address.bare().toString(), JID); +}); + +test.serial("FAST", async (t) => { + t.plan(7); + + const xmpp = client({ + service: domain, + credentials: { + ...credentials, + requestToken: true, + }, + clientId: "75b2d490-3e3b-4d96-bca1-624b30ea6f82", + software: "xmpp.js tests", + device: "Test Device", + }); + t.context.xmpp = xmpp; + debug(xmpp); + + xmpp.on("connect", () => { + t.pass(); + }); + + xmpp.once("open", (el) => { + t.true(el instanceof xml.Element); + }); + + xmpp.once("fast-token", (el) => { + t.true(typeof el.attrs.token === "string"); + }); + + xmpp.on("online", (address) => { + t.true(address instanceof jid.JID); + t.is(address.bare().toString(), JID); + }); + + const address = await xmpp.start(); + t.true(address instanceof jid.JID); + t.is(address.bare().toString(), JID); +}); + test.serial.cb("bad credentials", (t) => { t.plan(6); From faf34ef15a72e0fa2fda067172cdeb852592dd08 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 18 Dec 2024 14:47:18 -0500 Subject: [PATCH 17/27] Fix for 0.12+ --- server/index.js | 2 +- server/prosody.cfg.lua | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/server/index.js b/server/index.js index e7fe8290..148b6f6a 100644 --- a/server/index.js +++ b/server/index.js @@ -83,7 +83,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 1474579f..2d14a9fc 100644 --- a/server/prosody.cfg.lua +++ b/server/prosody.cfg.lua @@ -30,7 +30,6 @@ modules_disabled = { "s2s"; } -daemonize = true; pidfile = lfs.currentdir() .. "/prosody.pid"; allow_registration = true; From 07f42506c48730b9ffc694f3a5ab81ba3b727ae5 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 18 Dec 2024 14:50:07 -0500 Subject: [PATCH 18/27] SASL2 et al need prosody trunk --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index bda8b40f..b84a6fc0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -26,7 +26,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 prosody-trunk lua-bitop lua-sec sudo service prosody stop # - run: npm install -g npm From 069d0f665391b4343bec8a505d57b69a0bc23001 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Fri, 20 Dec 2024 01:04:35 +0100 Subject: [PATCH 19/27] Enable sasl2 in react-native --- packages/client/browser.js | 12 ++++++++++-- packages/client/react-native.js | 26 ++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/client/browser.js b/packages/client/browser.js index cece2dc5..ab15465d 100644 --- a/packages/client/browser.js +++ b/packages/client/browser.js @@ -23,8 +23,16 @@ const anonymous = require("@xmpp/sasl-anonymous"); const plain = require("@xmpp/sasl-plain"); function client(options = {}) { - const { resource, credentials, username, password, ...params } = options; - const { clientId, software, device } = params; + const { + resource, + credentials, + username, + password, + clientId, + software, + device, + ...params + } = options; const { domain, service } = params; if (!domain && service) { diff --git a/packages/client/react-native.js b/packages/client/react-native.js index 8751aea7..ab15465d 100644 --- a/packages/client/react-native.js +++ b/packages/client/react-native.js @@ -12,6 +12,7 @@ const _iqCallee = require("@xmpp/iq/callee"); const _resolve = require("@xmpp/resolve"); // Stream features - order matters and define priority +const _sasl2 = require("@xmpp/sasl2"); const _sasl = require("@xmpp/sasl"); const _resourceBinding = require("@xmpp/resource-binding"); const _sessionEstablishment = require("@xmpp/session-establishment"); @@ -22,7 +23,16 @@ const anonymous = require("@xmpp/sasl-anonymous"); const plain = require("@xmpp/sasl-plain"); function client(options = {}) { - const { resource, credentials, username, password, ...params } = options; + const { + resource, + credentials, + username, + password, + clientId, + software, + device, + ...params + } = options; const { domain, service } = params; if (!domain && service) { @@ -40,11 +50,17 @@ function client(options = {}) { const iqCallee = _iqCallee({ middleware, entity }); const resolve = _resolve({ entity }); // Stream features - order matters and define priority + const sasl2 = _sasl2( + { streamFeatures }, + credentials || { username, password }, + { clientId, software, device }, + ); const sasl = _sasl({ streamFeatures }, credentials || { username, password }); const streamManagement = _streamManagement({ streamFeatures, entity, middleware, + sasl2, }); const resourceBinding = _resourceBinding( { iqCaller, streamFeatures }, @@ -55,9 +71,10 @@ function client(options = {}) { streamFeatures, }); // SASL mechanisms - order matters and define priority - const mechanisms = Object.entries({ plain, anonymous }).map(([k, v]) => ({ - [k]: v(sasl), - })); + const mechanisms = Object.entries({ + plain, + anonymous, + }).map(([k, v]) => ({ [k]: [v(sasl2), v(sasl)] })); return Object.assign(entity, { entity, @@ -68,6 +85,7 @@ function client(options = {}) { iqCaller, iqCallee, resolve, + sasl2, sasl, resourceBinding, sessionEstablishment, From 7ce1b2f9e72924ec656e9f1e604d8346c1dde1d5 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sat, 21 Dec 2024 20:05:55 +0100 Subject: [PATCH 20/27] fixes --- Makefile | 10 +-- package-lock.json | 1 + package.json | 2 + packages/sasl-ht-sha-256-none/index.js | 8 +-- packages/sasl-ht-sha-256-none/package.json | 1 + packages/sasl2/index.js | 42 ++++++------- packages/sasl2/lib/SASLError.js | 6 +- packages/sasl2/package.json | 2 + packages/sasl2/test.js | 71 ++++++++++------------ 9 files changed, 64 insertions(+), 79 deletions(-) diff --git a/Makefile b/Makefile index a6a5f83f..3eec6ac5 100644 --- a/Makefile +++ b/Makefile @@ -19,20 +19,14 @@ test: ci: npm install - make unit + npm run test make lint make restart npx lerna run prepublish node bundle.js - make e2e + npm run e2e make bundlesize -unit: - npx jest - -e2e: - NODE_TLS_REJECT_UNAUTHORIZED=0 npx jest --runInBand --config e2e.config.cjs - clean: make stop rm -f server/localhost.key diff --git a/package-lock.json b/package-lock.json index e57b5dc8..ee9aa19a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16156,6 +16156,7 @@ "@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", "saslmechanisms": "^0.1.1" }, diff --git a/package.json b/package.json index e04c3cf7..18b7fd30 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "uglify-js": "^3.19.3" }, "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/sasl-ht-sha-256-none/index.js b/packages/sasl-ht-sha-256-none/index.js index 07a37531..bb9ef270 100644 --- a/packages/sasl-ht-sha-256-none/index.js +++ b/packages/sasl-ht-sha-256-none/index.js @@ -1,7 +1,5 @@ -"use strict"; - // https://datatracker.ietf.org/doc/draft-schmaus-kitten-sasl-ht/ -const createHmac = require("create-hmac"); +import createHmac from "create-hmac"; function Mechanism() {} @@ -24,6 +22,6 @@ Mechanism.prototype.final = (data) => { } }; -module.exports = function sasl2(sasl) { +export default function sasl2(sasl) { sasl.use(Mechanism); -}; +} diff --git a/packages/sasl-ht-sha-256-none/package.json b/packages/sasl-ht-sha-256-none/package.json index 82c25dd2..3951a476 100644 --- a/packages/sasl-ht-sha-256-none/package.json +++ b/packages/sasl-ht-sha-256-none/package.json @@ -6,6 +6,7 @@ "bugs": "http://github.com/xmppjs/xmpp.js/issues", "version": "0.13.0", "license": "ISC", + "type": "module", "keywords": [ "XMPP", "sasl", diff --git a/packages/sasl2/index.js b/packages/sasl2/index.js index a026f9d6..a73c2c8d 100644 --- a/packages/sasl2/index.js +++ b/packages/sasl2/index.js @@ -1,10 +1,8 @@ -"use strict"; - -const { encode, decode } = require("@xmpp/base64"); -const SASLError = require("./lib/SASLError"); -const jid = require("@xmpp/jid"); -const xml = require("@xmpp/xml"); -const SASLFactory = require("saslmechanisms"); +import { encode, decode } from "@xmpp/base64"; +import SASLError from "@xmpp/sasl/lib/SASLError.js"; +import jid from "@xmpp/jid"; +import xml from "@xmpp/xml"; +import SASLFactory from "saslmechanisms"; // https://xmpp.org/extensions/xep-0388.html // https://xmpp.org/extensions/xep-0386.html @@ -164,7 +162,7 @@ async function authenticate( await Promise.all([promise, ...hPromises]); } -module.exports = function sasl2({ streamFeatures }, credentials, userAgent) { +export default function sasl2({ streamFeatures }, credentials, userAgent) { const SASL = new SASLFactory(); const handlers = {}; const bindHandlers = {}; @@ -195,20 +193,18 @@ module.exports = function sasl2({ streamFeatures }, credentials, userAgent) { .filter((mech) => mech.canFast || mech.canOther); if (typeof credentials === "function") { - await credentials( - (creds, mech) => - authenticate( - SASL, - handlers, - bindHandlers, - entity, - mech, - creds, - userAgent, - stanza, - ), - intersection, - ); + await credentials((creds, mech) => { + authenticate( + SASL, + handlers, + bindHandlers, + entity, + mech, + creds, + userAgent, + stanza, + ); + }, intersection); } else { let mech = intersection[0]?.name; if (!credentials.username && !credentials.password) { @@ -241,4 +237,4 @@ module.exports = function sasl2({ streamFeatures }, credentials, userAgent) { bindHandlers[feature] = handler; }, }; -}; +} diff --git a/packages/sasl2/lib/SASLError.js b/packages/sasl2/lib/SASLError.js index d7f17efb..acb73194 100644 --- a/packages/sasl2/lib/SASLError.js +++ b/packages/sasl2/lib/SASLError.js @@ -1,6 +1,4 @@ -"use strict"; - -const XMPPError = require("@xmpp/error"); +import XMPPError from "@xmpp/error"; // https://xmpp.org/rfcs/rfc6120.html#sasl-errors @@ -11,4 +9,4 @@ class SASLError extends XMPPError { } } -module.exports = SASLError; +export default SASLError; diff --git a/packages/sasl2/package.json b/packages/sasl2/package.json index e51f9756..ad7903df 100644 --- a/packages/sasl2/package.json +++ b/packages/sasl2/package.json @@ -6,6 +6,7 @@ "bugs": "http://github.com/xmppjs/xmpp.js/issues", "version": "0.13.0", "license": "ISC", + "type": "module", "keywords": [ "XMPP", "sasl" @@ -13,6 +14,7 @@ "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", "saslmechanisms": "^0.1.1" diff --git a/packages/sasl2/test.js b/packages/sasl2/test.js index be2afd59..fc53c62d 100644 --- a/packages/sasl2/test.js +++ b/packages/sasl2/test.js @@ -1,14 +1,11 @@ -"use strict"; - -const test = require("ava"); -const { mockClient, promise } = require("@xmpp/test"); -const parse = require("@xmpp/xml/lib/parse.js"); +import { mockClient, promise } from "@xmpp/test"; +import parse from "@xmpp/xml/lib/parse.js"; const username = "foo"; const password = "bar"; const credentials = { username, password }; -test("no compatibles mechanisms", async (t) => { +test("no compatibles mechanisms", async () => { const { entity } = mockClient({ username, password }); entity.mockInput( @@ -20,11 +17,11 @@ test("no compatibles mechanisms", async (t) => { ); const error = await promise(entity, "error"); - t.true(error instanceof Error); - t.is(error.message, "No compatible mechanism"); + expect(error instanceof Error).toBe(true); + expect(error.message).toBe("No compatible mechanism"); }); -test("with object credentials", async (t) => { +test("with object credentials", async () => { const { entity } = mockClient({ credentials }); entity.mockInput( @@ -35,8 +32,7 @@ test("with object credentials", async (t) => { , ); - t.deepEqual( - await promise(entity, "send"), + expect(await promise(entity, "send")).toEqual( AGZvbwBiYXI= , @@ -48,11 +44,13 @@ test("with object credentials", async (t) => { await promise(entity, "online"); }); -test("with function credentials", async (t) => { +test("with function credentials", async () => { const mech = "PLAIN"; function authenticate(auth, mechanisms) { - t.deepEqual(mechanisms, [{ name: mech, canFast: false, canOther: true }]); + expect(mechanisms).toEqual([ + { name: mech, canFast: false, canOther: true }, + ]); return auth(credentials, mech); } @@ -66,8 +64,7 @@ test("with function credentials", async (t) => { , ); - t.deepEqual( - await promise(entity, "send"), + expect(await promise(entity, "send")).toEqual( AGZvbwBiYXI= , @@ -79,7 +76,7 @@ test("with function credentials", async (t) => { await promise(entity, "online"); }); -test("failure", async (t) => { +test("failure", async () => { const { entity } = mockClient({ credentials }); entity.mockInput( @@ -90,8 +87,7 @@ test("failure", async (t) => { , ); - t.deepEqual( - await promise(entity, "send"), + expect(await promise(entity, "send")).toEqual( AGZvbwBiYXI= , @@ -106,13 +102,13 @@ test("failure", async (t) => { entity.mockInput(failure); const error = await promise(entity, "error"); - t.true(error instanceof Error); - t.is(error.name, "SASLError"); - t.is(error.condition, "some-condition"); - t.is(error.element, failure); + 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 (t) => { +test("prefers SCRAM-SHA-1", async () => { const { entity } = mockClient({ credentials }); entity.mockInput( @@ -126,10 +122,10 @@ test("prefers SCRAM-SHA-1", async (t) => { ); const result = await promise(entity, "send"); - t.deepEqual(result.attrs.mechanism, "SCRAM-SHA-1"); + expect(result.attrs.mechanism).toEqual("SCRAM-SHA-1"); }); -test("use ANONYMOUS if username and password are not provided", async (t) => { +test("use ANONYMOUS if username and password are not provided", async () => { const { entity } = mockClient(); entity.mockInput( @@ -143,10 +139,10 @@ test("use ANONYMOUS if username and password are not provided", async (t) => { ); const result = await promise(entity, "send"); - t.deepEqual(result.attrs.mechanism, "ANONYMOUS"); + expect(result.attrs.mechanism).toEqual("ANONYMOUS"); }); -test("with whitespaces", async (t) => { +test("with whitespaces", async () => { const { entity } = mockClient(); entity.mockInput( @@ -164,10 +160,10 @@ test("with whitespaces", async (t) => { ); const result = await promise(entity, "send"); - t.deepEqual(result.attrs.mechanism, "ANONYMOUS"); + expect(result.attrs.mechanism).toEqual("ANONYMOUS"); }); -test("with bind2", async (t) => { +test("with bind2", async () => { const { entity } = mockClient({ credentials, clientId: "uniqueid", @@ -186,8 +182,7 @@ test("with bind2", async (t) => { , ); - t.deepEqual( - await promise(entity, "send"), + expect(await promise(entity, "send")).toEqual( AGZvbwBiYXI= @@ -206,10 +201,10 @@ test("with bind2", async (t) => { await promise(entity, "online"); }); -test("with FAST", async (t) => { +test("with FAST", async () => { const { entity } = mockClient({ credentials: (callback, mechanisms) => { - t.deepEqual(mechanisms, [ + expect(mechanisms).toEqual([ { canFast: true, canOther: false, name: "HT-SHA-256-NONE" }, { canFast: false, canOther: true, name: "PLAIN" }, ]); @@ -233,8 +228,7 @@ test("with FAST", async (t) => { , ); - t.deepEqual( - await promise(entity, "send"), + expect(await promise(entity, "send")).toEqual( AGZvbwBiYXI= @@ -247,10 +241,10 @@ test("with FAST", async (t) => { await promise(entity, "online"); }); -test("with FAST token", async (t) => { +test("with FAST token", async () => { const { entity } = mockClient({ credentials: (callback, mechanisms) => { - t.deepEqual(mechanisms, [ + expect(mechanisms).toEqual([ { canFast: true, canOther: false, name: "HT-SHA-256-NONE" }, { canFast: false, canOther: true, name: "PLAIN" }, ]); @@ -271,8 +265,7 @@ test("with FAST token", async (t) => { , ); - t.deepEqual( - await promise(entity, "send"), + expect(await promise(entity, "send")).toEqual( bnVsbAAAXAywUfR/w4Mr9SUDUtNAgPDajNI073fqfiZLMYcmfA== From 441efe049e0f44e592efa6224fe20c0f7dcae7ce Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sun, 22 Dec 2024 00:25:09 +0100 Subject: [PATCH 21/27] Stop vendoring prosody modules --- .github/workflows/CI.yml | 8 +- packages/sasl2/test.js | 7 +- server/modules/.gitkeep | 0 server/modules/mod_sasl2.lua | 228 ------------------------ server/modules/mod_sasl2_bind2.lua | 110 ------------ server/modules/mod_sasl2_fast.lua | 271 ----------------------------- server/modules/mod_sasl2_sm.lua | 90 ---------- server/prosody.cfg.lua | 4 +- 8 files changed, 15 insertions(+), 703 deletions(-) create mode 100644 server/modules/.gitkeep delete mode 100644 server/modules/mod_sasl2.lua delete mode 100644 server/modules/mod_sasl2_bind2.lua delete mode 100644 server/modules/mod_sasl2_fast.lua delete mode 100644 server/modules/mod_sasl2_sm.lua diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index e4d77868..8128a65c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -26,9 +26,13 @@ jobs: run: | 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 lua5.3 prosody-trunk lua-bitop lua-sec + sudo apt update + sudo apt install lua5.4 prosody-trunk lua-bitop lua-sec luarocks sudo service prosody stop + prosodyctl --config server/prosody.cfg.lua install mod_sasl2 + prosodyctl --config server/prosody.cfg.lua install mod_sasl2_bind2 + prosodyctl --config server/prosody.cfg.lua install mod_sasl2_fast + prosodyctl --config server/prosody.cfg.lua install mod_sasl2_sm # - run: npm install -g npm - run: make diff --git a/packages/sasl2/test.js b/packages/sasl2/test.js index fc53c62d..844430ea 100644 --- a/packages/sasl2/test.js +++ b/packages/sasl2/test.js @@ -195,7 +195,12 @@ test("with bind2", async () => { , ); - entity.mockInput(); + entity.mockInput( + + {entity.jid} + + , + ); entity.mockInput(); await promise(entity, "online"); diff --git a/server/modules/.gitkeep b/server/modules/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/server/modules/mod_sasl2.lua b/server/modules/mod_sasl2.lua deleted file mode 100644 index 9eeff4b1..00000000 --- a/server/modules/mod_sasl2.lua +++ /dev/null @@ -1,228 +0,0 @@ --- Prosody IM --- Copyright (C) 2019 Kim Alvefur --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- --- XEP-0388: Extensible SASL Profile --- - -local st = require "util.stanza"; -local errors = require "util.error"; -local base64 = require "util.encodings".base64; -local jid_join = require "util.jid".join; -local set = require "util.set"; - -local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; -local sm_make_authenticated = require "core.sessionmanager".make_authenticated; - -local xmlns_sasl2 = "urn:xmpp:sasl:2"; - -local secure_auth_only = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", true)); -local allow_unencrypted_plain_auth = module:get_option_boolean("allow_unencrypted_plain_auth", false) -local insecure_mechanisms = module:get_option_set("insecure_sasl_mechanisms", allow_unencrypted_plain_auth and {} or {"PLAIN", "LOGIN"}); -local disabled_mechanisms = module:get_option_set("disable_sasl_mechanisms", { "DIGEST-MD5" }); - -local host = module.host; - -local function tls_unique(self) - return self.userdata["tls-unique"]:ssl_peerfinished(); -end - -local function tls_exporter(conn) - if not conn.ssl_exportkeyingmaterial then return end - return conn:ssl_exportkeyingmaterial("EXPORTER-Channel-Binding", 32, ""); -end - -local function sasl_tls_exporter(self) - return tls_exporter(self.userdata["tls-exporter"]); -end - -module:hook("stream-features", function(event) - local origin, features = event.origin, event.features; - local log = origin.log or module._log; - - if origin.type ~= "c2s_unauthed" then - log("debug", "Already authenticated"); - return - elseif secure_auth_only and not origin.secure then - log("debug", "Not offering authentication on insecure connection"); - return; - end - - local sasl_handler = usermanager_get_sasl_handler(host, origin) - origin.sasl_handler = sasl_handler; - - local channel_bindings = set.new() - if origin.encrypted then - -- check whether LuaSec has the nifty binding to the function needed for tls-unique - -- FIXME: would be nice to have this check only once and not for every socket - if sasl_handler.add_cb_handler then - local info = origin.conn:ssl_info(); - if info and info.protocol == "TLSv1.3" then - log("debug", "Channel binding 'tls-unique' undefined in context of TLS 1.3"); - if tls_exporter(origin.conn) then - log("debug", "Channel binding 'tls-exporter' supported"); - sasl_handler:add_cb_handler("tls-exporter", sasl_tls_exporter); - channel_bindings:add("tls-exporter"); - else - log("debug", "Channel binding 'tls-exporter' not supported"); - end - elseif origin.conn.ssl_peerfinished and origin.conn:ssl_peerfinished() then - log("debug", "Channel binding 'tls-unique' supported"); - sasl_handler:add_cb_handler("tls-unique", tls_unique); - channel_bindings:add("tls-unique"); - else - log("debug", "Channel binding 'tls-unique' not supported (by LuaSec?)"); - end - sasl_handler["userdata"] = { - ["tls-unique"] = origin.conn; - ["tls-exporter"] = origin.conn; - }; - else - log("debug", "Channel binding not supported by SASL handler"); - end - end - - local mechanisms = st.stanza("authentication", { xmlns = xmlns_sasl2 }); - - local available_mechanisms = sasl_handler:mechanisms() - for mechanism in pairs(available_mechanisms) do - if disabled_mechanisms:contains(mechanism) then - log("debug", "Not offering disabled mechanism %s", mechanism); - elseif not origin.secure and insecure_mechanisms:contains(mechanism) then - log("debug", "Not offering mechanism %s on insecure connection", mechanism); - else - log("debug", "Offering mechanism %s", mechanism); - mechanisms:text_tag("mechanism", mechanism); - end - end - - features:add_direct_child(mechanisms); - - local inline = st.stanza("inline"); - module:fire_event("advertise-sasl-features", { origin = origin, features = inline, stream = event.stream }); - mechanisms:add_direct_child(inline); -end, 1); - -local function handle_status(session, status, ret, err_msg) - local err = nil; - if status == "error" then - ret, err = nil, ret; - if not errors.is_err(err) then - err = errors.new({ condition = err, text = err_msg }, { session = session }); - end - end - - return module:fire_event("sasl2/"..session.base_type.."/"..status, { - session = session, - message = ret; - error = err; - error_text = err_msg; - }); -end - -module:hook("sasl2/c2s/failure", function (event) - module:fire_event("authentication-failure", event); - local session, condition, text = event.session, event.message, event.error_text; - local failure = st.stanza("failure", { xmlns = xmlns_sasl2 }) - :tag(condition, { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl" }):up(); - if text then - failure:text_tag("text", text); - end - session.send(failure); - return true; -end); - -module:hook("sasl2/c2s/error", function (event) - local session = event.session - session.send(st.stanza("failure", { xmlns = xmlns_sasl2 }) - :tag(event.error and event.error.condition)); - return true; -end); - -module:hook("sasl2/c2s/challenge", function (event) - local session = event.session; - session.send(st.stanza("challenge", { xmlns = xmlns_sasl2 }) - :text(base64.encode(event.message))); - return true; -end); - -module:hook("sasl2/c2s/success", function (event) - local session = event.session - local ok, err = sm_make_authenticated(session, session.sasl_handler.username); - if not ok then - handle_status(session, "failure", err); - return true; - end - event.success = st.stanza("success", { xmlns = xmlns_sasl2 }); - if event.message then - event.success:text_tag("additional-data", base64.encode(event.message)); - end -end, 1000); - -module:hook("sasl2/c2s/success", function (event) - local session = event.session - event.success:text_tag("authorization-identifier", jid_join(session.username, session.host, session.resource)); - session.send(event.success); -end, -1000); - -module:hook("sasl2/c2s/success", function (event) - module:fire_event("authentication-success", event); - local session = event.session; - local features = st.stanza("stream:features"); - module:fire_event("stream-features", { origin = session, features = features }); - session.send(features); -end, -1500); - --- The gap here is to allow modules to do stuff to the stream after the stanza --- is sent, but before we proceed with anything else. This is expected to be --- a common pattern with SASL2, which allows atomic negotiation of a bunch of --- stream features. -module:hook("sasl2/c2s/success", function (event) --luacheck: ignore 212/event - event.session.sasl_handler = nil; - return true; -end, -2000); - -local function process_cdata(session, cdata) - if cdata then - cdata = base64.decode(cdata); - if not cdata then - return handle_status(session, "failure", "incorrect-encoding"); - end - end - return handle_status(session, session.sasl_handler:process(cdata)); -end - -module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth) - if secure_auth_only and not session.secure then - return handle_status(session, "failure", "encryption-required"); - end - local sasl_handler = session.sasl_handler; - if not sasl_handler then - sasl_handler = usermanager_get_sasl_handler(host, session); - session.sasl_handler = sasl_handler; - end - local mechanism = assert(auth.attr.mechanism); - if not sasl_handler:select(mechanism) then - return handle_status(session, "failure", "invalid-mechanism"); - end - local user_agent = auth:get_child("user-agent"); - if user_agent then - session.client_id = user_agent.attr.id; - sasl_handler.user_agent = { - software = user_agent:get_child_text("software"); - device = user_agent:get_child_text("device"); - }; - end - local initial = auth:get_child_text("initial-response"); - return process_cdata(session, initial); -end); - -module:hook_tag(xmlns_sasl2, "response", function (session, response) - local sasl_handler = session.sasl_handler; - if not sasl_handler or not sasl_handler.selected then - return handle_status(session, "failure", "invalid-mechanism"); - end - return process_cdata(session, response:get_text()); -end); diff --git a/server/modules/mod_sasl2_bind2.lua b/server/modules/mod_sasl2_bind2.lua deleted file mode 100644 index fb02ffd5..00000000 --- a/server/modules/mod_sasl2_bind2.lua +++ /dev/null @@ -1,110 +0,0 @@ -local base64 = require "util.encodings".base64; -local id = require "util.id"; -local sha1 = require "util.hashes".sha1; -local st = require "util.stanza"; - -local sm_bind_resource = require "core.sessionmanager".bind_resource; - -local xmlns_bind2 = "urn:xmpp:bind:0"; -local xmlns_sasl2 = "urn:xmpp:sasl:2"; - -module:depends("sasl2"); - --- Advertise what we can do - -module:hook("advertise-sasl-features", function(event) - local bind = st.stanza("bind", { xmlns = xmlns_bind2 }); - local inline = st.stanza("inline"); - module:fire_event("advertise-bind-features", { origin = event.origin, features = inline }); - bind:add_direct_child(inline); - - event.features:add_direct_child(bind); -end, 1); - --- Helper to actually bind a resource to a session - -local function do_bind(session, bind_request) - local resource = session.sasl_handler.resource; - - if not resource then - local client_name_tag = bind_request:get_child_text("tag"); - if client_name_tag then - local client_id = session.client_id; - local tag_suffix = client_id and base64.encode(sha1(client_id):sub(1, 9)) or id.medium(); - resource = ("%s~%s"):format(client_name_tag, tag_suffix); - end - end - - local success, err_type, err, err_msg = sm_bind_resource(session, resource); - if not success then - session.log("debug", "Resource bind failed: %s", err_msg or err); - return nil, { type = err_type, condition = err, text = err_msg }; - end - - session.log("debug", "Resource bound: %s", session.full_jid); - return st.stanza("bound", { xmlns = xmlns_bind2 }); -end - --- Enable inline features requested by the client - -local function enable_features(session, bind_request, bind_result) - module:fire_event("enable-bind-features", { - session = session; - request = bind_request; - result = bind_result; - }); -end - --- SASL 2 integration - -module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth) - -- Cache action for future processing (after auth success) - session.sasl2_bind_request = auth:child_with_ns(xmlns_bind2); -end, 100); - -module:hook("sasl2/c2s/success", function (event) - local session = event.session; - - local bind_request = session.sasl2_bind_request; - if not bind_request then return; end -- No bind requested - session.sasl2_bind_request = nil; - - local sm_success = session.sasl2_sm_success; - if sm_success and sm_success.type == "resumed" then - return; -- No need to bind a resource - end - - local bind_result, err = do_bind(session, bind_request); - if not bind_result then - bind_result = st.stanza("failed", { xmlns = xmlns_bind2 }) - :add_error(err); - else - enable_features(session, bind_request, bind_result); - end - - event.success:add_child(bind_result); -end, 100); - --- Inline features - -module:hook("advertise-bind-features", function (event) - local features = event.features; - features:tag("feature", { var = "urn:xmpp:carbons:2" }):up(); - features:tag("feature", { var = "urn:xmpp:csi:0" }):up(); -end); - -module:hook("enable-bind-features", function (event) - local session, request = event.session, event.request; - - -- Carbons - if request:get_child("enable", "urn:xmpp:carbons:2") then - session.want_carbons = true; - event.result:tag("enabled", { xmlns = "urn:xmpp:carbons:2" }):up(); - end - - -- CSI - local csi_state_tag = request:child_with_ns("urn:xmpp:csi:0"); - if csi_state_tag then - session.state = csi_state_tag.name; - end -end, 10); diff --git a/server/modules/mod_sasl2_fast.lua b/server/modules/mod_sasl2_fast.lua deleted file mode 100644 index 4a4c4475..00000000 --- a/server/modules/mod_sasl2_fast.lua +++ /dev/null @@ -1,271 +0,0 @@ -local usermanager = require "core.usermanager"; - -local sasl = require "util.sasl"; -local dt = require "util.datetime"; -local id = require "util.id"; -local jid = require "util.jid"; -local st = require "util.stanza"; -local now = require "util.time".now; -local hash = require "util.hashes"; - -local sasl_mt = getmetatable(sasl.new("", { mechanisms = {} })); -local function is_util_sasl(sasl_handler) - return getmetatable(sasl_handler) == sasl_mt; -end - -module:depends("sasl2"); - --- Tokens expire after 21 days by default -local fast_token_ttl = module:get_option_number("sasl2_fast_token_ttl", 86400*21); --- Tokens are automatically rotated daily -local fast_token_min_ttl = module:get_option_number("sasl2_fast_token_min_ttl", 86400); - -local xmlns_fast = "urn:xmpp:fast:0"; -local xmlns_sasl2 = "urn:xmpp:sasl:2"; - -local token_store = module:open_store("fast_tokens", "map"); - -local log = module._log; - -local function make_token(username, client_id, mechanism) - local new_token = "secret-token:fast-"..id.long(); - local key = hash.sha256(client_id, true).."-new"; - local issued_at = now(); - local token_info = { - mechanism = mechanism; - secret = new_token; - issued_at = issued_at; - expires_at = issued_at + fast_token_ttl; - }; - if not token_store:set(username, key, token_info) then - return nil; - end - return token_info; -end - -local function new_token_tester(hmac_f) - return function (mechanism, username, client_id, token_hash, cb_data, invalidate) - local account_info = usermanager.get_account_info(username, module.host); - local last_password_change = account_info and account_info.password_updated; - local tried_current_token = false; - local key = hash.sha256(client_id, true).."-new"; - local token; - repeat - log("debug", "Looking for %s token %s/%s", mechanism, username, key); - token = token_store:get(username, key); - if token and token.mechanism == mechanism then - local expected_hash = hmac_f(token.secret, "Initiator"..(cb_data or "")); - if hash.equals(expected_hash, token_hash) then - local current_time = now(); - if token.expires_at < current_time then - log("debug", "Token found, but it has expired (%ds ago). Cleaning up...", current_time - token.expires_at); - token_store:set(username, key, nil); - return nil, "credentials-expired"; - elseif last_password_change and token.issued_at < last_password_change then - log("debug", "Token found, but issued prior to password change (%ds ago). Cleaning up...", - current_time - last_password_change - ); - token_store:set(username, key, nil); - return nil, "credentials-expired"; - end - if not tried_current_token and not invalidate then - -- The new token is becoming the current token - token_store:set_keys(username, { - [key] = token_store.remove; - [key:sub(1, -5).."-cur"] = token; - }); - end - local rotation_needed; - if invalidate then - token_store:set(username, key, nil); - elseif current_time - token.issued_at > fast_token_min_ttl then - log("debug", "FAST token due for rotation (age: %d)", current_time - token.issued_at); - rotation_needed = true; - end - return true, username, hmac_f(token.secret, "Responder"..(cb_data or "")), rotation_needed; - end - end - if not tried_current_token then - log("debug", "Trying next token..."); - -- Try again with the current token instead - tried_current_token = true; - key = key:sub(1, -5).."-cur"; - else - log("debug", "No matching %s token found for %s/%s", mechanism, username, key); - return nil; - end - until false; - end -end - -function get_sasl_handler() - local token_auth_profile = { - ht_sha_256 = new_token_tester(hash.hmac_sha256); - }; - local handler = sasl.new(module.host, token_auth_profile); - handler.fast = true; - return handler; -end - --- Advertise FAST to connecting clients -module:hook("advertise-sasl-features", function (event) - local session = event.origin; - local username = session.username; - if not username then - username = jid.node(event.stream.from); - if not username then return; end - end - local sasl_handler = get_sasl_handler(username); - if not sasl_handler then return; end - sasl_handler.fast_auth = true; -- For informational purposes - -- Copy channel binding info from primary SASL handler if it's compatible - if is_util_sasl(session.sasl_handler) then - sasl_handler.profile.cb = session.sasl_handler.profile.cb; - sasl_handler.userdata = session.sasl_handler.userdata; - end - -- Store this handler, in case we later want to use it for authenticating - session.fast_sasl_handler = sasl_handler; - local fast = st.stanza("fast", { xmlns = xmlns_fast }); - for mech in pairs(sasl_handler:mechanisms()) do - fast:text_tag("mechanism", mech); - end - event.features:add_child(fast); -end); - --- Process any FAST elements in -module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth) - -- Cache action for future processing (after auth success) - local fast_auth = auth:get_child("fast", xmlns_fast); - if fast_auth then - -- Client says it is using FAST auth, so set our SASL handler - local fast_sasl_handler = session.fast_sasl_handler; - local client_id = auth:get_child_attr("user-agent", nil, "id"); - if fast_sasl_handler and client_id then - session.log("debug", "Client is authenticating using FAST"); - fast_sasl_handler.client_id = client_id; - fast_sasl_handler.profile.cb = session.sasl_handler.profile.cb; - fast_sasl_handler.userdata = session.sasl_handler.userdata; - local invalidate = fast_auth.attr.invalidate; - fast_sasl_handler.invalidate = invalidate == "1" or invalidate == "true"; - -- Set our SASL handler as the session's SASL handler - session.sasl_handler = fast_sasl_handler; - else - session.log("warn", "Client asked to auth via FAST, but SASL handler or client id missing"); - local failure = st.stanza("failure", { xmlns = xmlns_sasl2 }) - :tag("malformed-request"):up() - :text_tag("text", "FAST is not available on this stream"); - session.send(failure); - return true; - end - end - session.fast_sasl_handler = nil; - local fast_token_request = auth:get_child("request-token", xmlns_fast); - if fast_token_request then - local mech = fast_token_request.attr.mechanism; - session.log("debug", "Client requested new FAST token for %s", mech); - session.fast_token_request = { - mechanism = mech; - }; - end -end, 100); - --- Process post-success (new token generation, etc.) -module:hook("sasl2/c2s/success", function (event) - local session = event.session; - - local token_request = session.fast_token_request; - local client_id = session.client_id; - local sasl_handler = session.sasl_handler; - if token_request or (sasl_handler.fast and sasl_handler.rotation_needed) then - if not client_id then - session.log("warn", "FAST token requested, but missing client id"); - return; - end - local mechanism = token_request and token_request.mechanism or session.sasl_handler.selected; - local token_info = make_token(session.username, client_id, mechanism) - if token_info then - session.log("debug", "Provided new FAST token to client"); - event.success:tag("token", { - xmlns = xmlns_fast; - expiry = dt.datetime(token_info.expires_at); - token = token_info.secret; - }):up(); - end - end -end, 75); - --- HT-* mechanisms - -local function new_ht_mechanism(mechanism_name, backend_profile_name, cb_name) - return function (sasl_handler, message) - local backend = sasl_handler.profile[backend_profile_name]; - local authc_username, token_hash = message:match("^([^%z]+)%z(.+)$"); - if not authc_username then - return "failure", "malformed-request"; - end - local cb_data; - if cb_name then - if not sasl_handler.profile.cb then - module:log("warn", "Attempt to use channel binding %s with SASL profile that does not support any channel binding (FAST: %s)", cb_name, sasl_handler.fast); - return "failure", "malformed-request"; - elseif not sasl_handler.profile.cb[cb_name] then - module:log("warn", "SASL profile does not support %s channel binding (FAST: %s)", cb_name, sasl_handler.fast); - return "failure", "malformed-request"; - end - cb_data = sasl_handler.profile.cb[cb_name](sasl_handler) or ""; - end - local ok, authz_username, response, rotation_needed = backend( - mechanism_name, - authc_username, - sasl_handler.client_id, - token_hash, - cb_data, - sasl_handler.invalidate - ); - if not ok then - -- authz_username is error condition - return "failure", authz_username or "not-authorized"; - end - sasl_handler.username = authz_username; - sasl_handler.rotation_needed = rotation_needed; - return "success", response; - end -end - -local function register_ht_mechanism(name, backend_profile_name, cb_name) - return sasl.registerMechanism(name, { backend_profile_name }, new_ht_mechanism( - name, - backend_profile_name, - cb_name - ), - cb_name and { cb_name } or nil); -end - -register_ht_mechanism("HT-SHA-256-NONE", "ht_sha_256", nil); -register_ht_mechanism("HT-SHA-256-UNIQ", "ht_sha_256", "tls-unique"); -register_ht_mechanism("HT-SHA-256-ENDP", "ht_sha_256", "tls-server-end-point"); -register_ht_mechanism("HT-SHA-256-EXPR", "ht_sha_256", "tls-exporter"); - --- Public API - ---luacheck: ignore 131 -function is_client_fast(username, client_id, last_password_change) - local client_id_hash = hash.sha256(client_id, true); - local curr_time = now(); - local cur = token_store:get(username, client_id_hash.."-cur"); - if cur and cur.expires_at >= curr_time and (not last_password_change or last_password_change < cur.issued_at) then - return true; - end - local new = token_store:get(username, client_id_hash.."-new"); - if new and new.expires_at >= curr_time and (not last_password_change or last_password_change < new.issued_at) then - return true; - end - return false; -end - -function revoke_fast_tokens(username, client_id) - local client_id_hash = hash.sha256(client_id, true); - local cur_ok = token_store:set(username, client_id_hash.."-cur", nil); - local new_ok = token_store:set(username, client_id_hash.."-new", nil); - return cur_ok and new_ok; -end diff --git a/server/modules/mod_sasl2_sm.lua b/server/modules/mod_sasl2_sm.lua deleted file mode 100644 index 4ed0fb1a..00000000 --- a/server/modules/mod_sasl2_sm.lua +++ /dev/null @@ -1,90 +0,0 @@ -local st = require "util.stanza"; - -local mod_smacks = module:depends("smacks"); - -local xmlns_sasl2 = "urn:xmpp:sasl:2"; -local xmlns_sm = "urn:xmpp:sm:3"; - -module:depends("sasl2"); - --- Advertise what we can do - -module:hook("advertise-sasl-features", function (event) - local features = event.features; - features:tag("sm", { xmlns = xmlns_sm }):up(); -end); - -module:hook("advertise-bind-features", function (event) - local features = event.features; - features:tag("feature", { var = xmlns_sm }):up(); -end); - -module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth) - -- Cache action for future processing (after auth success) - session.sasl2_sm_request = auth:child_with_ns(xmlns_sm); -end, 100); - --- SASL 2 integration (for resume) - -module:hook("sasl2/c2s/success", function (event) - local session = event.session; - local sm_request = session.sasl2_sm_request; - if not sm_request then return; end - session.sasl2_sm_request = nil; - local sm_result; - if sm_request.name ~= "resume" then return; end - - local resumed, err = mod_smacks.do_resume(session, sm_request); - if not resumed then - local h = err.context and err.context.h; - sm_result = st.stanza("failed", { xmlns = xmlns_sm, h = h and ("%d"):format(h) or nil }) - :add_error(err); - else - event.session = resumed.session; -- Update to resumed session - event.session.sasl2_sm_success = resumed; -- To be called after sending final SASL response - sm_result = st.stanza("resumed", { xmlns = xmlns_sm, - h = ("%d"):format(event.session.handled_stanza_count); - previd = resumed.id; }); - end - - if sm_result then - event.success:add_child(sm_result); - end -end, 110); - --- Bind 2 integration (for enable) - -module:hook("enable-bind-features", function (event) - local sm_enable = event.request:get_child("enable", xmlns_sm); - if not sm_enable then return; end - - local sm_result; - local enabled, err = mod_smacks.do_enable(event.session, sm_enable); - if not enabled then - sm_result = st.stanza("failed", { xmlns = xmlns_sm }) - :add_error(err); - else - event.session.sasl2_sm_success = enabled; -- To be called after sending final SASL response - sm_result = st.stanza("enabled", { - xmlns = xmlns_sm; - id = enabled.id; - resume = enabled.id and "1" or nil; - max = enabled.resume_max; - }); - end - event.result:add_child(sm_result); -end, 100); - --- Finish and/or clean up after SASL 2 completed - -module:hook("sasl2/c2s/success", function (event) - -- The authenticate response has already been sent at this point - local success = event.session.sasl2_sm_success; - if success then - success.finish(); -- Finish enable/resume and sync stanzas - end -end, -1100); - -module:hook("sasl2/c2s/failure", function (event) - event.session.sasl2_sm_request = nil; -end); diff --git a/server/prosody.cfg.lua b/server/prosody.cfg.lua index 2d14a9fc..9fd2a774 100644 --- a/server/prosody.cfg.lua +++ b/server/prosody.cfg.lua @@ -4,7 +4,9 @@ local lfs = require "lfs"; -plugin_paths = { "modules" } +-- plugin_paths = { "modules" } +plugin_server = "https://modules.prosody.im/rocks/" +installer_plugin_path = lfs.currentdir() .. "/modules"; modules_enabled = { "roster"; From e37dfbfbc50160d775cfc36436601fe3585bc7e8 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sun, 22 Dec 2024 01:41:05 +0100 Subject: [PATCH 22/27] Address some review comments --- .github/workflows/CI.yml | 2 +- packages/connection/index.js | 3 +-- packages/sasl-ht-sha-256-none/index.js | 4 ++-- packages/sasl2/README.md | 28 ++++++++++++++------------ packages/stream-management/index.js | 12 ++++++----- 5 files changed, 26 insertions(+), 23 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8128a65c..cde830fa 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 update - sudo apt install lua5.4 prosody-trunk lua-bitop lua-sec luarocks + sudo apt install lua5.3 prosody-trunk lua-bitop lua-sec luarocks sudo service prosody stop prosodyctl --config server/prosody.cfg.lua install mod_sasl2 prosodyctl --config server/prosody.cfg.lua install mod_sasl2_bind2 diff --git a/packages/connection/index.js b/packages/connection/index.js index 8f40285c..ca6cb249 100644 --- a/packages/connection/index.js +++ b/packages/connection/index.js @@ -262,8 +262,7 @@ class Connection extends EventEmitter { (this.streamFrom || this.jid) ) { // When the stream is secure there is no leak to setting the stream from - // This is suggested in general and in required for FAST implementations - // in particular + // This is recommended in general and required for FAST implementations headerElement.attrs.from = (this.streamFrom || this.jid).toString(); } headerElement.attrs["xml:lang"] = lang; diff --git a/packages/sasl-ht-sha-256-none/index.js b/packages/sasl-ht-sha-256-none/index.js index 7d1d60ee..5a38015a 100644 --- a/packages/sasl-ht-sha-256-none/index.js +++ b/packages/sasl-ht-sha-256-none/index.js @@ -18,10 +18,10 @@ Mechanism.prototype.final = function final(data) { const hmac = createHmac("sha256", this.password); hmac.update("Responder"); if (hmac.digest("latin1") !== data) { - throw "Responder message from server was wrong"; + throw new Error("Responder message from server was wrong"); } }; -export default function sasl2(sasl) { +export default function saslHashedToken(sasl) { sasl.use(Mechanism); } diff --git a/packages/sasl2/README.md b/packages/sasl2/README.md index a2d27b96..a240a81e 100644 --- a/packages/sasl2/README.md +++ b/packages/sasl2/README.md @@ -1,4 +1,4 @@ -# SASL +# SASL2 SASL2 Negotiation for `@xmpp/client` (including optional BIND2 and FAST). @@ -11,14 +11,16 @@ Included and enabled in `@xmpp/client`. ### object ```js -const {xmpp} = require('@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)", -}) +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 @@ -34,7 +36,7 @@ Uses cases: - Perform an asynchronous operation to get credentials ```js -const { xmpp } = require("@xmpp/client"); +import { xmpp } from "@xmpp/client"; const client = xmpp({ credentials: authenticate, clientId: "Some UUID for this client/server pair (optional)", @@ -74,6 +76,6 @@ async function authenticate(callback, mechanisms) { ## References -[SASL2](https://xmpp.org/extensions/xep-0388.html) -[BIND2](https://xmpp.org/extensions/xep-0386.html) -[FAST](https://xmpp.org/extensions/inbox/xep-fast.html) +- [SASL2](https://xmpp.org/extensions/xep-0388.html) +- [BIND2](https://xmpp.org/extensions/xep-0386.html) +- [FAST](https://xmpp.org/extensions/xep-0484.html) diff --git a/packages/stream-management/index.js b/packages/stream-management/index.js index 7e75f80a..5a148344 100644 --- a/packages/stream-management/index.js +++ b/packages/stream-management/index.js @@ -88,17 +88,17 @@ export default function streamManagement({ // https://xmpp.org/extensions/xep-0198.html#enable // For client-to-server connections, the client MUST NOT attempt to enable stream management until after it has completed Resource Binding unless it is resuming a previous session - const resumeSuccess = () => { + function resumeSuccess() { sm.enabled = true; if (address) entity.jid = address; entity.status = "online"; - }; + } - const resumeFailed = () => { + function resumeFailed() { sm.id = ""; sm.enabled = false; sm.outbound = 0; - }; + } streamFeatures.use("sm", NS, async (context, next) => { // Resuming @@ -161,7 +161,9 @@ export default function streamManagement({ const enabled = bound?.getChild("enabled", NS); if (enabled) { if (sm.outbound_q.length > 0) { - throw "Stream Management assertion failure, queue should be empty after enable"; + throw new Error( + "Stream Management assertion failure, queue should be empty after enable", + ); } sm.outbound = 0; sm.enabled = true; From c5645bcd8e35653f5d36e9cffbf5306a1d5dd670 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sun, 22 Dec 2024 01:45:45 +0100 Subject: [PATCH 23/27] f --- server/prosody.cfg.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/prosody.cfg.lua b/server/prosody.cfg.lua index 9fd2a774..1af1d45c 100644 --- a/server/prosody.cfg.lua +++ b/server/prosody.cfg.lua @@ -4,9 +4,9 @@ local lfs = require "lfs"; --- plugin_paths = { "modules" } +plugin_paths = { "modules" } plugin_server = "https://modules.prosody.im/rocks/" -installer_plugin_path = lfs.currentdir() .. "/modules"; +installer_plugin_path = "modules"; modules_enabled = { "roster"; From 195cda58d27d0407a5c954ce0cda2a96b33b6e6e Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sun, 22 Dec 2024 12:14:17 +0100 Subject: [PATCH 24/27] Fix CI install modules --- .github/workflows/CI.yml | 13 +++++++------ .gitignore | 2 ++ server/modules/.gitkeep | 0 server/prosody.cfg.lua | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) delete mode 100644 server/modules/.gitkeep diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index cde830fa..4ad7ce18 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -26,13 +26,14 @@ jobs: run: | 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 update - sudo apt install lua5.3 prosody-trunk lua-bitop lua-sec luarocks + sudo apt-get update + sudo apt-get -y install lua5.3 liblua5.3-dev prosody-trunk lua-bitop lua-sec luarocks sudo service prosody stop - prosodyctl --config server/prosody.cfg.lua install mod_sasl2 - prosodyctl --config server/prosody.cfg.lua install mod_sasl2_bind2 - prosodyctl --config server/prosody.cfg.lua install mod_sasl2_fast - prosodyctl --config server/prosody.cfg.lua install mod_sasl2_sm + cd server + prosodyctl --config prosody.cfg.lua install mod_sasl2 + prosodyctl --config prosody.cfg.lua install mod_sasl2_bind2 + prosodyctl --config prosody.cfg.lua install mod_sasl2_fast + prosodyctl --config prosody.cfg.lua install mod_sasl2_sm # - run: npm install -g npm - run: make 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/server/modules/.gitkeep b/server/modules/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/server/prosody.cfg.lua b/server/prosody.cfg.lua index 1af1d45c..cb3eb0f8 100644 --- a/server/prosody.cfg.lua +++ b/server/prosody.cfg.lua @@ -6,7 +6,7 @@ local lfs = require "lfs"; plugin_paths = { "modules" } plugin_server = "https://modules.prosody.im/rocks/" -installer_plugin_path = "modules"; +installer_plugin_path = lfs.currentdir() .. "/modules"; modules_enabled = { "roster"; From 2e1782fe8ce2e2617f957db5460f82b3627f7c9d Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sun, 22 Dec 2024 13:13:59 +0100 Subject: [PATCH 25/27] Remove sasl2 SASLError --- packages/error/test.js | 2 + packages/sasl/lib/SASLError.test.js | 71 +++++++++++++++++++++++++++++ packages/sasl2/lib/SASLError.js | 12 ----- 3 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 packages/sasl/lib/SASLError.test.js delete mode 100644 packages/sasl2/lib/SASLError.js 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/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/sasl2/lib/SASLError.js b/packages/sasl2/lib/SASLError.js deleted file mode 100644 index acb73194..00000000 --- a/packages/sasl2/lib/SASLError.js +++ /dev/null @@ -1,12 +0,0 @@ -import XMPPError from "@xmpp/error"; - -// https://xmpp.org/rfcs/rfc6120.html#sasl-errors - -class SASLError extends XMPPError { - constructor(...args) { - super(...args); - this.name = "SASLError"; - } -} - -export default SASLError; From c7e17c94fe0386e9f4aa97747aecdb3d8daaab10 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sun, 22 Dec 2024 13:48:51 +0100 Subject: [PATCH 26/27] Move sasl factory to client --- package-lock.json | 8 ++++--- packages/client/browser.js | 36 +++++++++++++++++--------------- packages/client/index.js | 28 +++++++++++++++++-------- packages/client/package.json | 7 ++++--- packages/client/react-native.js | 37 +++++++++++++++++---------------- packages/sasl/README.md | 2 +- packages/sasl/index.js | 21 +++++++------------ packages/sasl/package.json | 3 +-- packages/sasl2/index.js | 21 +++++++++---------- packages/sasl2/package.json | 3 +-- 10 files changed, 86 insertions(+), 80 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e156341..6d2bc59b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13103,6 +13103,8 @@ }, "node_modules/saslmechanisms": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/saslmechanisms/-/saslmechanisms-0.1.1.tgz", + "integrity": "sha512-pVlvK5ysevz8MzybRnDIa2YMxn0OJ7b9lDiWhMoaKPoJ7YkAg/7YtNjUgaYzElkwHxsw8dBMhaEn7UP6zxEwPg==", "engines": { "node": ">= 0.4.0" } @@ -14575,7 +14577,8 @@ "@xmpp/stream-management": "^0.13.2", "@xmpp/tcp": "^0.13.2", "@xmpp/tls": "^0.13.2", - "@xmpp/websocket": "^0.13.2" + "@xmpp/websocket": "^0.13.2", + "saslmechanisms": "^0.1.1" }, "engines": { "node": ">= 20" @@ -14763,8 +14766,7 @@ "dependencies": { "@xmpp/base64": "^0.13.2", "@xmpp/error": "^0.13.2", - "@xmpp/xml": "^0.13.2", - "saslmechanisms": "^0.1.1" + "@xmpp/xml": "^0.13.2" }, "engines": { "node": ">= 20" diff --git a/packages/client/browser.js b/packages/client/browser.js index 61c52535..baecf921 100644 --- a/packages/client/browser.js +++ b/packages/client/browser.js @@ -1,5 +1,6 @@ import { xml, jid, Client } from "@xmpp/client-core"; import getDomain from "./lib/getDomain.js"; +import SASLFactory from "saslmechanisms"; import _reconnect from "@xmpp/reconnect"; import _websocket from "@xmpp/websocket"; @@ -18,16 +19,8 @@ import plain from "@xmpp/sasl-plain"; import anonymous from "@xmpp/sasl-anonymous"; function client(options = {}) { - const { - resource, - credentials, - username, - password, - clientId, - software, - device, - ...params - } = options; + const { resource, credentials, username, password, ...params } = options; + const { clientId, software, device } = params; const { domain, service } = params; if (!domain && service) { @@ -45,12 +38,25 @@ function client(options = {}) { const iqCallee = _iqCallee({ middleware, entity }); const resolve = _resolve({ entity }); // Stream features - order matters and define priority + + const saslFactory = new SASLFactory(); + // SASL mechanisms - order matters and define priority + const mechanisms = Object.entries({ + plain, + anonymous, + }).map(([k, v]) => ({ [k]: v(saslFactory) })); + const sasl2 = _sasl2( - { streamFeatures }, + { streamFeatures, saslFactory }, credentials || { username, password }, { clientId, software, device }, ); - const sasl = _sasl({ streamFeatures }, credentials || { username, password }); + + const sasl = _sasl( + { streamFeatures, saslFactory }, + credentials || { username, password }, + ); + const streamManagement = _streamManagement({ streamFeatures, entity, @@ -65,11 +71,6 @@ function client(options = {}) { iqCaller, streamFeatures, }); - // SASL mechanisms - order matters and define priority - const mechanisms = Object.entries({ - plain, - anonymous, - }).map(([k, v]) => ({ [k]: [v(sasl2), v(sasl)] })); return Object.assign(entity, { entity, @@ -80,6 +81,7 @@ function client(options = {}) { iqCaller, iqCallee, resolve, + saslFactory, sasl2, sasl, resourceBinding, diff --git a/packages/client/index.js b/packages/client/index.js index 2a7b5f22..2828ccc8 100644 --- a/packages/client/index.js +++ b/packages/client/index.js @@ -1,5 +1,6 @@ import { xml, jid, Client } from "@xmpp/client-core"; import getDomain from "./lib/getDomain.js"; +import SASLFactory from "saslmechanisms"; import _reconnect from "@xmpp/reconnect"; import _websocket from "@xmpp/websocket"; @@ -45,12 +46,27 @@ function client(options = {}) { const resolve = _resolve({ entity }); // Stream features - order matters and define priority const starttls = _starttls({ streamFeatures }); + + const saslFactory = new SASLFactory(); + // SASL mechanisms - order matters and define priority + const mechanisms = Object.entries({ + scramsha1, + htsha256, + plain, + anonymous, + }).map(([k, v]) => ({ [k]: v(saslFactory) })); + const sasl2 = _sasl2( - { streamFeatures }, + { streamFeatures, saslFactory }, credentials || { username, password }, { clientId, software, device }, ); - const sasl = _sasl({ streamFeatures }, credentials || { username, password }); + + const sasl = _sasl( + { streamFeatures, saslFactory }, + credentials || { username, password }, + ); + const streamManagement = _streamManagement({ streamFeatures, entity, @@ -65,13 +81,6 @@ function client(options = {}) { iqCaller, streamFeatures, }); - // SASL mechanisms - order matters and define priority - const mechanisms = Object.entries({ - scramsha1, - htsha256, - plain, - anonymous, - }).map(([k, v]) => ({ [k]: [v(sasl2), v(sasl)] })); return Object.assign(entity, { entity, @@ -85,6 +94,7 @@ function client(options = {}) { iqCallee, resolve, starttls, + saslFactory, sasl2, sasl, resourceBinding, diff --git a/packages/client/package.json b/packages/client/package.json index 4b7d948a..b71d73da 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -14,19 +14,20 @@ "@xmpp/reconnect": "^0.13.2", "@xmpp/resolve": "^0.13.2", "@xmpp/resource-binding": "^0.13.2", - "@xmpp/sasl2": "^0.13.0", "@xmpp/sasl": "^0.13.2", "@xmpp/sasl-anonymous": "^0.13.2", + "@xmpp/sasl-ht-sha-256-none": "^0.13.0", "@xmpp/sasl-plain": "^0.13.2", "@xmpp/sasl-scram-sha-1": "^0.13.2", - "@xmpp/sasl-ht-sha-256-none": "^0.13.0", + "@xmpp/sasl2": "^0.13.0", "@xmpp/session-establishment": "^0.13.2", "@xmpp/starttls": "^0.13.2", "@xmpp/stream-features": "^0.13.2", "@xmpp/stream-management": "^0.13.2", "@xmpp/tcp": "^0.13.2", "@xmpp/tls": "^0.13.2", - "@xmpp/websocket": "^0.13.2" + "@xmpp/websocket": "^0.13.2", + "saslmechanisms": "^0.1.1" }, "browser": "./browser.js", "react-native": "./react-native.js", diff --git a/packages/client/react-native.js b/packages/client/react-native.js index 0960566b..baecf921 100644 --- a/packages/client/react-native.js +++ b/packages/client/react-native.js @@ -1,5 +1,6 @@ import { xml, jid, Client } from "@xmpp/client-core"; import getDomain from "./lib/getDomain.js"; +import SASLFactory from "saslmechanisms"; import _reconnect from "@xmpp/reconnect"; import _websocket from "@xmpp/websocket"; @@ -8,7 +9,6 @@ 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 _sasl2 from "@xmpp/sasl2"; import _sasl from "@xmpp/sasl"; import _resourceBinding from "@xmpp/resource-binding"; @@ -19,16 +19,8 @@ import plain from "@xmpp/sasl-plain"; import anonymous from "@xmpp/sasl-anonymous"; function client(options = {}) { - const { - resource, - credentials, - username, - password, - clientId, - software, - device, - ...params - } = options; + const { resource, credentials, username, password, ...params } = options; + const { clientId, software, device } = params; const { domain, service } = params; if (!domain && service) { @@ -46,12 +38,25 @@ function client(options = {}) { const iqCallee = _iqCallee({ middleware, entity }); const resolve = _resolve({ entity }); // Stream features - order matters and define priority + + const saslFactory = new SASLFactory(); + // SASL mechanisms - order matters and define priority + const mechanisms = Object.entries({ + plain, + anonymous, + }).map(([k, v]) => ({ [k]: v(saslFactory) })); + const sasl2 = _sasl2( - { streamFeatures }, + { streamFeatures, saslFactory }, credentials || { username, password }, { clientId, software, device }, ); - const sasl = _sasl({ streamFeatures }, credentials || { username, password }); + + const sasl = _sasl( + { streamFeatures, saslFactory }, + credentials || { username, password }, + ); + const streamManagement = _streamManagement({ streamFeatures, entity, @@ -66,11 +71,6 @@ function client(options = {}) { iqCaller, streamFeatures, }); - // SASL mechanisms - order matters and define priority - const mechanisms = Object.entries({ - plain, - anonymous, - }).map(([k, v]) => ({ [k]: [v(sasl2), v(sasl)] })); return Object.assign(entity, { entity, @@ -81,6 +81,7 @@ function client(options = {}) { iqCaller, iqCallee, resolve, + saslFactory, sasl2, sasl, resourceBinding, diff --git a/packages/sasl/README.md b/packages/sasl/README.md index c29bccd2..8efc7145 100644 --- a/packages/sasl/README.md +++ b/packages/sasl/README.md @@ -36,7 +36,7 @@ import { xmpp } from "@xmpp/client"; const client = xmpp({ credentials: authenticate }); -async function authenticate(auth, mechanism) { +async function authenticate(saslFactory, mechanism) { console.debug("authenticate", mechanism); const credentials = { username: await prompt("enter username"), diff --git a/packages/sasl/index.js b/packages/sasl/index.js index 42f6297d..fdc293e1 100644 --- a/packages/sasl/index.js +++ b/packages/sasl/index.js @@ -1,7 +1,6 @@ import { encode, decode } from "@xmpp/base64"; import SASLError from "./lib/SASLError.js"; import xml from "@xmpp/xml"; -import SASLFactory from "saslmechanisms"; // https://xmpp.org/rfcs/rfc6120.html#sasl @@ -14,8 +13,8 @@ function getMechanismNames(features) { .map((el) => el.text()); } -async function authenticate(SASL, entity, mechname, credentials) { - const mech = SASL.create([mechname]); +async function authenticate(saslFactory, entity, mechname, credentials) { + const mech = saslFactory.create([mechname]); if (!mech) { throw new Error("No compatible mechanism"); } @@ -74,12 +73,10 @@ async function authenticate(SASL, entity, mechname, credentials) { }); } -export default function sasl({ streamFeatures }, credentials) { - const SASL = new SASLFactory(); - +export default function sasl({ streamFeatures, saslFactory }, credentials) { streamFeatures.use("mechanisms", NS, async ({ stanza, entity }) => { const offered = getMechanismNames(stanza); - const supported = SASL._mechs.map(({ name }) => name); + const supported = saslFactory._mechs.map(({ name }) => name); // eslint-disable-next-line unicorn/prefer-array-find const intersection = supported.filter((mech) => { return offered.includes(mech); @@ -88,7 +85,7 @@ export default function sasl({ streamFeatures }, credentials) { if (typeof credentials === "function") { await credentials( - (creds) => authenticate(SASL, entity, mech, creds, stanza), + (creds) => authenticate(saslFactory, entity, mech, creds, stanza), mech, ); } else { @@ -96,15 +93,11 @@ export default function sasl({ streamFeatures }, credentials) { mech = "ANONYMOUS"; } - await authenticate(SASL, entity, mech, credentials, stanza); + await authenticate(saslFactory, entity, mech, credentials, stanza); } await entity.restart(); }); - return { - use(...args) { - return SASL.use(...args); - }, - }; + return {}; } diff --git a/packages/sasl/package.json b/packages/sasl/package.json index 8bb5ef1b..ec63bb0b 100644 --- a/packages/sasl/package.json +++ b/packages/sasl/package.json @@ -15,8 +15,7 @@ "dependencies": { "@xmpp/base64": "^0.13.2", "@xmpp/error": "^0.13.2", - "@xmpp/xml": "^0.13.2", - "saslmechanisms": "^0.1.1" + "@xmpp/xml": "^0.13.2" }, "engines": { "node": ">= 20" diff --git a/packages/sasl2/index.js b/packages/sasl2/index.js index a73c2c8d..bc2ae17a 100644 --- a/packages/sasl2/index.js +++ b/packages/sasl2/index.js @@ -2,7 +2,6 @@ import { encode, decode } from "@xmpp/base64"; import SASLError from "@xmpp/sasl/lib/SASLError.js"; import jid from "@xmpp/jid"; import xml from "@xmpp/xml"; -import SASLFactory from "saslmechanisms"; // https://xmpp.org/extensions/xep-0388.html // https://xmpp.org/extensions/xep-0386.html @@ -13,7 +12,7 @@ const BIND2_NS = "urn:xmpp:bind:0"; const FAST_NS = "urn:xmpp:fast:0"; async function authenticate( - SASL, + saslFactory, inlineHandlers, bindInlineHandlers, entity, @@ -22,7 +21,7 @@ async function authenticate( userAgent, features, ) { - const mech = SASL.create([mechname]); + const mech = saslFactory.create([mechname]); if (!mech) { throw new Error("No compatible mechanism"); } @@ -162,8 +161,11 @@ async function authenticate( await Promise.all([promise, ...hPromises]); } -export default function sasl2({ streamFeatures }, credentials, userAgent) { - const SASL = new SASLFactory(); +export default function sasl2( + { streamFeatures, saslFactory }, + credentials, + userAgent, +) { const handlers = {}; const bindHandlers = {}; @@ -182,7 +184,7 @@ export default function sasl2({ streamFeatures }, credentials, userAgent) { ?.getChildren("mechanism", FAST_NS) ?.map((m) => m.text()) || [], ); - const supported = SASL._mechs.map(({ name }) => name); + const supported = saslFactory._mechs.map(({ name }) => name); const intersection = supported .map((mech) => ({ @@ -195,7 +197,7 @@ export default function sasl2({ streamFeatures }, credentials, userAgent) { if (typeof credentials === "function") { await credentials((creds, mech) => { authenticate( - SASL, + saslFactory, handlers, bindHandlers, entity, @@ -212,7 +214,7 @@ export default function sasl2({ streamFeatures }, credentials, userAgent) { } await authenticate( - SASL, + saslFactory, handlers, bindHandlers, entity, @@ -227,9 +229,6 @@ export default function sasl2({ streamFeatures }, credentials, userAgent) { }); return { - use(...args) { - return SASL.use(...args); - }, inline(name, xmlns, handler) { handlers["{" + xmlns + "}" + name] = handler; }, diff --git a/packages/sasl2/package.json b/packages/sasl2/package.json index ad7903df..15705e48 100644 --- a/packages/sasl2/package.json +++ b/packages/sasl2/package.json @@ -16,8 +16,7 @@ "@xmpp/error": "^0.13.0", "@xmpp/sasl": "^0.13.2", "@xmpp/jid": "^0.13.0", - "@xmpp/xml": "^0.13.0", - "saslmechanisms": "^0.1.1" + "@xmpp/xml": "^0.13.0" }, "engines": { "node": ">= 14" From 9973b6f5dbd4321414fa4558caba77a0824d9adc Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sun, 22 Dec 2024 14:20:13 +0100 Subject: [PATCH 27/27] add make e2e with prosody modules install --- .github/workflows/CI.yml | 5 ----- Makefile | 15 +++++++++++++-- package-lock.json | 3 +-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4ad7ce18..47bab9f5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,11 +29,6 @@ jobs: sudo apt-get update sudo apt-get -y install lua5.3 liblua5.3-dev prosody-trunk lua-bitop lua-sec luarocks sudo service prosody stop - cd server - prosodyctl --config prosody.cfg.lua install mod_sasl2 - prosodyctl --config prosody.cfg.lua install mod_sasl2_bind2 - prosodyctl --config prosody.cfg.lua install mod_sasl2_fast - prosodyctl --config prosody.cfg.lua install mod_sasl2_sm # - run: npm install -g npm - run: make diff --git a/Makefile b/Makefile index 2fd85c48..d1f0b5e3 100644 --- a/Makefile +++ b/Makefile @@ -19,14 +19,25 @@ test: ci: npm install - npm run test + make unit make lint make restart npx lerna run prepublish make bundle - npm run e2e + make e2e make bundlesize +unit: + npm run test + +e2e: + $(warning e2e tests require prosody-trunk and luarocks) + cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2 > /dev/null + 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 rm -f server/localhost.key diff --git a/package-lock.json b/package-lock.json index 6d2bc59b..73fc2e01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14825,8 +14825,7 @@ "@xmpp/error": "^0.13.0", "@xmpp/jid": "^0.13.0", "@xmpp/sasl": "^0.13.2", - "@xmpp/xml": "^0.13.0", - "saslmechanisms": "^0.1.1" + "@xmpp/xml": "^0.13.0" }, "engines": { "node": ">= 14"