From 762e94d354307e76fb24d248d32b6c793de7566b Mon Sep 17 00:00:00 2001 From: Sonny Date: Mon, 23 Dec 2024 15:28:38 +0100 Subject: [PATCH] Implement Bind 2 (#1036) --- Makefile | 3 +- packages/client-core/src/bind2/bind2.js | 64 ++++++++++++++++++++----- packages/client/index.js | 9 ++-- packages/sasl2/index.js | 35 ++++++-------- packages/stream-management/index.js | 46 +++++++++++++++--- server/prosody.cfg.lua | 2 +- 6 files changed, 115 insertions(+), 44 deletions(-) diff --git a/Makefile b/Makefile index 11e52185..9e717c36 100644 --- a/Makefile +++ b/Makefile @@ -34,10 +34,9 @@ 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_sm > /dev/null # https://github.com/xmppjs/xmpp.js/pull/1006 # 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: diff --git a/packages/client-core/src/bind2/bind2.js b/packages/client-core/src/bind2/bind2.js index 5862e115..89faed20 100644 --- a/packages/client-core/src/bind2/bind2.js +++ b/packages/client-core/src/bind2/bind2.js @@ -2,16 +2,56 @@ import xml from "@xmpp/xml"; const NS_BIND = "urn:xmpp:bind:0"; -export default function bind2({ sasl2 }, tag) { - sasl2.use(NS_BIND, async (element) => { - if (!element.is("bind", NS_BIND)) return; - - tag = typeof tag === "function" ? await tag() : tag; - - return xml( - "bind", - { xmlns: "urn:xmpp:bind:0" }, - tag && xml("tag", null, tag), - ); - }); +export default function bind2({ sasl2, entity }, tag) { + const features = new Map(); + + sasl2.use( + NS_BIND, + async (element) => { + if (!element.is("bind", NS_BIND)) return; + + tag = typeof tag === "function" ? await tag() : tag; + + const sessionFeatures = await getSessionFeatures({ element, features }); + + return xml( + "bind", + { xmlns: "urn:xmpp:bind:0" }, + tag && xml("tag", null, tag), + ...sessionFeatures, + ); + }, + (element) => { + const aid = element.root().getChildText("authorization-identifier"); + if (aid) entity._jid(aid); + + for (const child of element.getChildElements()) { + const feature = features.get(child.getNS()); + if (!feature?.[1]) continue; + feature?.[1](child); + } + }, + ); + + return { + use(ns, req, res) { + features.set(ns, [req, res]); + }, + }; +} + +function getSessionFeatures({ element, features }) { + const promises = []; + + const inline = element.getChild("inline"); + if (!inline) return promises; + + for (const element of inline.getChildElements()) { + const xmlns = element.attrs.var; + const feature = features.get(xmlns); + if (!feature) continue; + promises.push(feature[0](element)); + } + + return Promise.all(promises); } diff --git a/packages/client/index.js b/packages/client/index.js index 86bbc2fb..54a35d90 100644 --- a/packages/client/index.js +++ b/packages/client/index.js @@ -60,6 +60,11 @@ function client(options = {}) { { streamFeatures, saslFactory }, createOnAuthenticate(credentials ?? { username, password }, userAgent), ); + + // SASL2 inline features + const bind2 = _bind2({ sasl2, entity }, resource); + + // Stream features - order matters and define priority const sasl = _sasl( { streamFeatures, saslFactory }, createOnAuthenticate(credentials ?? { username, password }, userAgent), @@ -68,6 +73,7 @@ function client(options = {}) { streamFeatures, entity, middleware, + bind2, }); const resourceBinding = _resourceBinding( { iqCaller, streamFeatures }, @@ -78,9 +84,6 @@ function client(options = {}) { streamFeatures, }); - // SASL2 inline features - const bind2 = _bind2({ sasl2 }, resource); - return Object.assign(entity, { entity, reconnect, diff --git a/packages/sasl2/index.js b/packages/sasl2/index.js index b8ad5e46..ab191d4f 100644 --- a/packages/sasl2/index.js +++ b/packages/sasl2/index.js @@ -1,6 +1,5 @@ import { encode, decode } from "@xmpp/base64"; import SASLError from "@xmpp/sasl/lib/SASLError.js"; -import jid from "@xmpp/jid"; import xml from "@xmpp/xml"; // https://xmpp.org/extensions/xep-0388.html @@ -17,7 +16,8 @@ async function authenticate({ mechanism, credentials, userAgent, - sessionFeatures, + streamFeatures, + features, }) { const mech = saslFactory.create([mechanism]); if (!mech) { @@ -61,7 +61,6 @@ async function authenticate({ } if (element.name === "continue") { - // No tasks supported yet reject(new Error("continue is not supported yet")); return; } @@ -71,17 +70,13 @@ async function authenticate({ 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); - } + + for (const child of element.getChildElements()) { + const feature = features.get(child.getNS()); + if (!feature?.[1]) continue; + feature?.[1](child); } + resolve(element); return; } @@ -97,7 +92,7 @@ async function authenticate({ mech.clientFirst && xml("initial-response", {}, encode(mech.response(creds))), userAgent, - ...sessionFeatures, + ...streamFeatures, ]), ) .catch(reject); @@ -105,7 +100,6 @@ async function authenticate({ } export default function sasl2({ streamFeatures, saslFactory }, onAuthenticate) { - // inline const features = new Map(); streamFeatures.use( @@ -120,7 +114,7 @@ export default function sasl2({ streamFeatures, saslFactory }, onAuthenticate) { throw new SASLError("SASL: No compatible mechanism available."); } - const sessionFeatures = await getSessionFeatures({ element, features }); + const streamFeatures = await getStreamFeatures({ element, features }); async function done(credentials, mechanism, userAgent) { await authenticate({ @@ -129,7 +123,8 @@ export default function sasl2({ streamFeatures, saslFactory }, onAuthenticate) { mechanism, credentials, userAgent, - sessionFeatures, + streamFeatures, + features, }); } @@ -141,12 +136,12 @@ export default function sasl2({ streamFeatures, saslFactory }, onAuthenticate) { return { use(ns, req, res) { - features.set(ns, req, res); + features.set(ns, [req, res]); }, }; } -function getSessionFeatures({ element, features }) { +function getStreamFeatures({ element, features }) { const promises = []; const inline = element.getChild("inline"); @@ -156,7 +151,7 @@ function getSessionFeatures({ element, features }) { const xmlns = element.getNS(); const feature = features.get(xmlns); if (!feature) continue; - promises.push(feature(element)); + promises.push(feature[0](element)); } return Promise.all(promises); diff --git a/packages/stream-management/index.js b/packages/stream-management/index.js index 4ec2bff7..d3d2667d 100644 --- a/packages/stream-management/index.js +++ b/packages/stream-management/index.js @@ -42,9 +42,8 @@ export default function streamManagement({ streamFeatures, entity, middleware, + bind2, }) { - let address = null; - const sm = { allowResume: true, preferredMaximum: null, @@ -55,8 +54,7 @@ export default function streamManagement({ max: null, }; - entity.on("online", (jid) => { - address = jid; + entity.on("online", () => { sm.outbound = 0; sm.inbound = 0; }); @@ -83,9 +81,23 @@ export default function streamManagement({ return next(); }); + if (bind2) { + setupBind2({ bind2, sm, entity }); + } else { + setupStreamFeature({ streamFeatures, sm, entity }); + } + + return sm; +} + +function setupStreamFeature({ streamFeatures, sm, entity }) { + let address = null; + entity.on("online", (jid) => { + address = jid; + }); + // 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 - streamFeatures.use("sm", NS, async (context, next) => { // Resuming if (sm.id) { @@ -124,6 +136,28 @@ export default function streamManagement({ sm.inbound = 0; }); +} - return sm; +function setupBind2({ bind2, sm, entity }) { + let address = null; + entity.on("online", (jid) => { + address = jid; + }); + bind2.use( + "urn:xmpp:sm:3", + (element) => { + if (!element.is("feature")) return; + // eslint-disable-next-line unicorn/prefer-ternary + if (sm.id) { + return xml("resume", { xmlns: NS }); + } else { + return xml("enable", { xmlns: NS }); + } + }, + (element) => { + if (!element.is("enabled")) return; + console.log("ENABLED"); + sm.enabled = true; + }, + ); } diff --git a/server/prosody.cfg.lua b/server/prosody.cfg.lua index 56f05b3a..c917b4a2 100644 --- a/server/prosody.cfg.lua +++ b/server/prosody.cfg.lua @@ -24,9 +24,9 @@ modules_enabled = { "smacks"; "sasl2"; "sasl2_bind2"; + "sasl2_sm"; -- https://github.com/xmppjs/xmpp.js/pull/1006 -- "sasl2_fast"; - -- "sasl2_sm"; }; modules_disabled = {