diff --git a/package-lock.json b/package-lock.json
index 26c3679c..5bc4318d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14123,6 +14123,7 @@
"license": "ISC",
"dependencies": {
"@xmpp/connection": "^0.14.0",
+ "@xmpp/events": "^0.14.0",
"@xmpp/jid": "^0.14.0",
"@xmpp/sasl": "^0.14.0",
"@xmpp/xml": "^0.14.0",
diff --git a/packages/client-core/package.json b/packages/client-core/package.json
index 2316fcd9..c6ed923c 100644
--- a/packages/client-core/package.json
+++ b/packages/client-core/package.json
@@ -9,6 +9,7 @@
"main": "index.js",
"dependencies": {
"@xmpp/connection": "^0.14.0",
+ "@xmpp/events": "^0.14.0",
"@xmpp/jid": "^0.14.0",
"@xmpp/sasl": "^0.14.0",
"@xmpp/xml": "^0.14.0",
diff --git a/packages/client-core/src/fast/README.md b/packages/client-core/src/fast/README.md
index aa0e02b5..8940b9a3 100644
--- a/packages/client-core/src/fast/README.md
+++ b/packages/client-core/src/fast/README.md
@@ -1,34 +1,35 @@
# fast
-fast for `@xmpp/client`.
+fast for `@xmpp/client`. Included and enabled in `@xmpp/client`.
-Included and enabled in `@xmpp/client`.
+By default `@xmpp/fast` stores the token in memory and as such fast authentication will only be available starting with the first reconnect.
-## Usage
+You can supply your own functions to store and retrieve the token from a persistent database.
-Resource is optional and will be chosen by the server if omitted.
+If fast authentication fails, regular authentication with `credentials` will happen.
-### string
+## Usage
```js
import { xmpp } from "@xmpp/client";
-const client = xmpp({ resource: "laptop" });
-```
-
-### function
-
-Instead, you can provide a function that will be called every time resource binding occurs (every (re)connect).
+const client = xmpp({
+ ...
+});
-```js
-import { xmpp } from "@xmpp/client";
-
-const client = xmpp({ resource: onBind });
+client.fast.fetchToken = async () => {
+ const value = await secureStorage.get("token")
+ return JSON.parse(value);
+}
-async function onBind(bind) {
- const resource = await fetchResource();
- return resource;
+client.fast.saveToken = async (token) => {
+ await secureStorage.set("token", JSON.stringify(token));
}
+
+// Debugging only
+client.fast.on("error", (error) => {
+ console.log("fast error", error);
+})
```
## References
diff --git a/packages/client-core/src/fast/fast.js b/packages/client-core/src/fast/fast.js
index 00140d20..2b3adbbc 100644
--- a/packages/client-core/src/fast/fast.js
+++ b/packages/client-core/src/fast/fast.js
@@ -1,46 +1,113 @@
+import { EventEmitter } from "@xmpp/events";
import { getAvailableMechanisms } from "@xmpp/sasl";
import xml from "@xmpp/xml";
import SASLFactory from "saslmechanisms";
const NS = "urn:xmpp:fast:0";
-export default function fast({ sasl2 }) {
+export default function fast({ sasl2 }, { saveToken, fetchToken } = {}) {
const saslFactory = new SASLFactory();
- const fast = {
- token: null,
- expiry: null,
+ const fast = new EventEmitter();
+
+ let token;
+ saveToken ??= async function saveToken(t) {
+ token = t;
+ };
+ fetchToken ??= async function fetchToken() {
+ return token;
+ };
+
+ Object.assign(fast, {
+ async saveToken() {
+ try {
+ await saveToken();
+ } catch (err) {
+ fast.emit("error", err);
+ }
+ },
+ async fetchToken() {
+ try {
+ return await fetchToken();
+ } catch (err) {
+ fast.emit("error", err);
+ }
+ },
saslFactory,
- mechanisms: [],
- mechanism: null,
- available() {
- return !!(this.token && this.mechanism);
+ async auth({
+ authenticate,
+ entity,
+ userAgent,
+ token,
+ credentials,
+ streamFeatures,
+ features,
+ }) {
+ try {
+ await authenticate({
+ saslFactory: fast.saslFactory,
+ mechanism: token.mechanism,
+ credentials: {
+ ...credentials,
+ password: token.token,
+ },
+ streamFeatures: [
+ ...streamFeatures,
+ xml("fast", {
+ xmlns: NS,
+ }),
+ ],
+ entity,
+ userAgent,
+ features,
+ });
+ return true;
+ } catch (err) {
+ fast.emit("error", err);
+ return false;
+ }
},
- };
+ _requestToken(streamFeatures) {
+ streamFeatures.push(
+ xml("request-token", {
+ xmlns: NS,
+ mechanism: fast.mechanism,
+ }),
+ );
+ },
+ });
+
+ function reset() {
+ fast.mechanism = null;
+ }
+ reset();
sasl2.use(
NS,
async (element) => {
- if (!element.is("fast", NS)) return;
- fast.mechanisms = getAvailableMechanisms(element, NS, saslFactory);
- fast.mechanism = fast.mechanisms[0];
+ if (!element.is("fast", NS)) return reset();
- if (!fast.mechanism) return;
+ fast.available = true;
- if (!fast.token) {
- return xml("request-token", {
- xmlns: NS,
- mechanism: fast.mechanism,
- });
- }
+ const mechanisms = getAvailableMechanisms(element, NS, saslFactory);
+ const mechanism = mechanisms[0];
+
+ if (!mechanism) return reset();
+ fast.mechanism = mechanism;
- return xml("fast", { xmlns: NS });
+ // The rest is handled by @xmpp/sasl2
},
async (element) => {
if (element.is("token", NS)) {
- const { token, expiry } = element.attrs;
- fast.token = token;
- fast.expiry = expiry;
+ try {
+ await saveToken({
+ mechanism: fast.mechanism,
+ token: element.attrs.token,
+ expiry: element.attrs.expiry,
+ });
+ } catch (err) {
+ fast.emit("error", err);
+ }
}
},
);
diff --git a/packages/client/index.js b/packages/client/index.js
index ddeb26ef..70f55ae4 100644
--- a/packages/client/index.js
+++ b/packages/client/index.js
@@ -26,7 +26,7 @@ import anonymous from "@xmpp/sasl-anonymous";
import htsha256none from "@xmpp/sasl-ht-sha-256-none";
function client(options = {}) {
- const { resource, credentials, username, password, userAgent, ...params } =
+ let { resource, credentials, username, password, userAgent, ...params } =
options;
const { domain, service } = params;
@@ -58,11 +58,22 @@ function client(options = {}) {
anonymous,
}).map(([k, v]) => ({ [k]: v(saslFactory) }));
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
+ const id = globalThis.crypto?.randomUUID?.();
+
+ let user_agent =
+ userAgent instanceof xml.Element
+ ? userAgent
+ : xml("user-agent", { id: userAgent?.id || id }, [
+ userAgent?.software && xml("software", {}, userAgent.software),
+ userAgent?.device && xml("device", {}, userAgent.device),
+ ]);
+
// Stream features - order matters and define priority
const starttls = setupIfAvailable(_starttls, { streamFeatures });
const sasl2 = _sasl2(
{ streamFeatures, saslFactory },
- createOnAuthenticate(credentials ?? { username, password }, userAgent),
+ createOnAuthenticate(credentials ?? { username, password }, user_agent),
);
const fast = setupIfAvailable(_fast, {
diff --git a/packages/client/lib/createOnAuthenticate.js b/packages/client/lib/createOnAuthenticate.js
index c47ca72b..e248eb8f 100644
--- a/packages/client/lib/createOnAuthenticate.js
+++ b/packages/client/lib/createOnAuthenticate.js
@@ -16,9 +16,7 @@ export default function createOnAuthenticate(credentials, userAgent) {
return;
}
- if (fast?.token) {
- credentials.password = fast.token;
- }
+ credentials.token = await fast?.fetchToken?.();
await authenticate(credentials, mechanisms[0], userAgent);
};
diff --git a/packages/debug/index.js b/packages/debug/index.js
index 0c182163..e1b9e063 100644
--- a/packages/debug/index.js
+++ b/packages/debug/index.js
@@ -7,6 +7,7 @@ import clone from "ltx/lib/clone.js";
const NS_SASL = "urn:ietf:params:xml:ns:xmpp-sasl";
const NS_SASL2 = "urn:xmpp:sasl:2";
const NS_COMPONENT = "jabber:component:accept";
+const NS_FAST = "urn:xmpp:fast:0";
const SENSITIVES = [
["handshake", NS_COMPONENT],
@@ -39,6 +40,8 @@ export function hideSensitive(element) {
hide(element.getChild("initial-response"));
} else if (element.getNS() === NS_SASL2) {
hide(element.getChild("additional-data"));
+ const token = element.getChild("token", NS_FAST);
+ token && (token.attrs.token = "hidden by xmpp.js");
}
return element;
diff --git a/packages/sasl2/README.md b/packages/sasl2/README.md
index 65b91c53..00076e43 100644
--- a/packages/sasl2/README.md
+++ b/packages/sasl2/README.md
@@ -59,13 +59,8 @@ async function getUserAgent() {
id = await crypto.randomUUID();
localStorage.set("user-agent-id", id);
}
- return (
- // https://xmpp.org/extensions/xep-0388.html#initiation
-
- xmpp.js
- Sonny's Laptop
-
- );
+ // https://xmpp.org/extensions/xep-0388.html#initiation
+ return { id, software: "xmpp.js", device: "Sonny's Laptop" }; // You can also pass an xml.Element
}
```
diff --git a/packages/sasl2/index.js b/packages/sasl2/index.js
index 25798486..a00d83a2 100644
--- a/packages/sasl2/index.js
+++ b/packages/sasl2/index.js
@@ -98,49 +98,46 @@ export default function sasl2({ streamFeatures, saslFactory }, onAuthenticate) {
"authentication",
NS,
async ({ entity }, _next, element) => {
- const streamFeatures = await getStreamFeatures({ element, features });
+ const mechanisms = getAvailableMechanisms(element, NS, saslFactory);
+ if (mechanisms.length === 0) {
+ throw new SASLError("SASL: No compatible mechanism available.");
+ }
- const fastStreamFeature = [...streamFeatures].find((el) =>
- el?.is("fast", "urn:xmpp:fast:0"),
- );
- const is_fast = fastStreamFeature && fast;
+ const streamFeatures = await getStreamFeatures({ element, features });
+ const fast_available = !!fast?.mechanism;
+ await onAuthenticate(done, mechanisms, fast_available && fast);
async function done(credentials, mechanism, userAgent) {
- const params = {
- entity,
- credentials,
- userAgent,
- streamFeatures,
- features,
- };
-
- if (is_fast) {
- try {
- await authenticate({
- saslFactory: fast.saslFactory,
- mechanism: fast.mechanisms[0],
- ...params,
+ if (fast_available) {
+ const { token } = credentials;
+ // eslint-disable-next-line unicorn/no-negated-condition
+ if (!token) {
+ fast._requestToken(streamFeatures);
+ } else {
+ const success = await fast.auth({
+ authenticate,
+ entity,
+ userAgent,
+ token,
+ streamFeatures,
+ features,
+ credentials,
});
- return;
- } catch {
+ if (success) return;
// If fast authentication fails, continue and try with sasl
- streamFeatures.delete(fastStreamFeature);
}
}
await authenticate({
+ entity,
+ userAgent,
+ streamFeatures,
+ features,
saslFactory,
mechanism,
- ...params,
+ credentials,
});
}
-
- const mechanisms = getAvailableMechanisms(element, NS, saslFactory);
- if (mechanisms.length === 0) {
- throw new SASLError("SASL: No compatible mechanism available.");
- }
-
- await onAuthenticate(done, mechanisms, is_fast && fast);
},
);
@@ -167,5 +164,5 @@ async function getStreamFeatures({ element, features }) {
promises.push(feature[0](element));
}
- return new Set(await Promise.all(promises));
+ return Promise.all(promises);
}
diff --git a/packages/sasl2/test.js b/packages/sasl2/test.js
index b2042769..547de79c 100644
--- a/packages/sasl2/test.js
+++ b/packages/sasl2/test.js
@@ -4,6 +4,13 @@ const username = "foo";
const password = "bar";
const credentials = { username, password };
+const userAgent = (
+
+ software
+ device
+
+);
+
test("No compatible mechanism available", async () => {
const { entity } = mockClient({ username, password });
@@ -21,7 +28,7 @@ test("No compatible mechanism available", async () => {
});
test("with object credentials", async () => {
- const { entity } = mockClient({ credentials });
+ const { entity } = mockClient({ credentials, userAgent });
entity.mockInput(
@@ -34,6 +41,7 @@ test("with object credentials", async () => {
expect(await promise(entity, "send")).toEqual(
AGZvbwBiYXI=
+ {userAgent}
,
);
@@ -95,7 +103,7 @@ test("with function credentials", async () => {
});
test("failure", async () => {
- const { entity } = mockClient({ credentials });
+ const { entity } = mockClient({ credentials, userAgent });
entity.mockInput(
@@ -108,6 +116,7 @@ test("failure", async () => {
expect(await promise(entity, "send")).toEqual(
AGZvbwBiYXI=
+ {userAgent}
,
);
diff --git a/packages/stream-management/bind2.test.js b/packages/stream-management/bind2.test.js
index 4bdfdbbf..a2a93203 100644
--- a/packages/stream-management/bind2.test.js
+++ b/packages/stream-management/bind2.test.js
@@ -20,14 +20,11 @@ test("enable", async () => {
);
const stanza_out = await entity.catchOutgoing();
- expect(stanza_out).toEqual(
-
- {stanza_out.getChild("initial-response")}
-
-
-
- ,
- );
+ const enable = stanza_out
+ .getChild("bind", "urn:xmpp:bind:0")
+ .getChild("enable");
+ enable.parent = null;
+ expect(enable).toEqual();
expect(sm.enabled).toBe(false);
expect(sm.id).toBe("");
@@ -67,14 +64,11 @@ test("Client failed to enable stream management", async () => {
);
const stanza_out = await entity.catchOutgoing();
- expect(stanza_out).toEqual(
-
- {stanza_out.getChild("initial-response")}
-
-
-
- ,
- );
+ const enable = stanza_out
+ .getChild("bind", "urn:xmpp:bind:0")
+ .getChild("enable");
+ enable.parent = null;
+ expect(enable).toEqual();
expect(sm.enabled).toBe(false);
expect(sm.id).toBe("");
diff --git a/packages/test/index.js b/packages/test/index.js
index 88ca81ee..72e940ee 100644
--- a/packages/test/index.js
+++ b/packages/test/index.js
@@ -2,6 +2,7 @@ import context from "./context.js";
import xml from "@xmpp/xml";
import jid from "@xmpp/jid";
import mockClient from "./mockClient.js";
+import mockClientCore from "./mockClientCore.js";
import { delay, promise, timeout } from "@xmpp/events";
import id from "@xmpp/id";
@@ -11,6 +12,7 @@ export {
jid,
jid as JID,
mockClient,
+ mockClientCore,
delay,
promise,
timeout,
diff --git a/packages/test/mockClientCore.js b/packages/test/mockClientCore.js
new file mode 100644
index 00000000..bf1509b8
--- /dev/null
+++ b/packages/test/mockClientCore.js
@@ -0,0 +1,10 @@
+import { Client } from "@xmpp/client-core";
+import Connection from "@xmpp/connection";
+import context from "./context.js";
+
+export default function mockClient(options) {
+ const xmpp = new Client(options);
+ xmpp.send = Connection.prototype.send;
+ const ctx = context(xmpp);
+ return Object.assign(xmpp, ctx);
+}