Skip to content

Commit

Permalink
fast: Improve implementation and add docs (#1043)
Browse files Browse the repository at this point in the history
  • Loading branch information
sonnyp authored Jan 5, 2025
1 parent 8ab0b58 commit 259d2cf
Show file tree
Hide file tree
Showing 13 changed files with 191 additions and 102 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/client-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
37 changes: 19 additions & 18 deletions packages/client-core/src/fast/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
113 changes: 90 additions & 23 deletions packages/client-core/src/fast/fast.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
},
);
Expand Down
15 changes: 13 additions & 2 deletions packages/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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, {
Expand Down
4 changes: 1 addition & 3 deletions packages/client/lib/createOnAuthenticate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
3 changes: 3 additions & 0 deletions packages/debug/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 2 additions & 7 deletions packages/sasl2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<user-agent id={id}>
<software>xmpp.js</software>
<device>Sonny's Laptop</device>
</user-agent>
);
// https://xmpp.org/extensions/xep-0388.html#initiation
return { id, software: "xmpp.js", device: "Sonny's Laptop" }; // You can also pass an xml.Element
}
```

Expand Down
59 changes: 28 additions & 31 deletions packages/sasl2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
);

Expand All @@ -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);
}
Loading

0 comments on commit 259d2cf

Please sign in to comment.