Skip to content

fix(sm): handle SASL2 inline SM resumption and suppress duplicate <enable>#380

Merged
mremond merged 1 commit intomainfrom
fix/sm-sasl2-inline
May 4, 2026
Merged

fix(sm): handle SASL2 inline SM resumption and suppress duplicate <enable>#380
mremond merged 1 commit intomainfrom
fix/sm-sasl2-inline

Conversation

@mremond
Copy link
Copy Markdown
Member

@mremond mremond commented May 4, 2026

Summary

Two bugs triggered when ejabberd uses SASL2 (XEP-0388) + bind2 (XEP-0386) + FAST (XEP-0484), observed in a real connection log.

Bug 1 — SASL2 inline SM resumption hangs (connect() times out after 30s)

When <resumed h="N"/> is embedded inside the SASL2 <success> element (XEP-0198 inline resumption), xmpp.js processes it entirely inside the SM plugin: it calls sm.emit("resumed") and then entity._ready(true). The _ready(true) path sets this.status = "online" without emitting the online event, and no top-level <resumed/> nonza is dispatched either. Connection.ts was not listening to sm.emit("resumed"), so connect() never resolved.

Fix: add sm.on('resumed', ...) alongside the existing sm.on('fail') listener. The handler calls handleResult(true) (resolves the pending promise as a resumption) and persists SM state. The resolved guard makes it a no-op when the traditional nonza path already fired.

Bug 2 — Duplicate <enable> silently disables SM

After the bind2 handshake completes, ejabberd sends a second <stream:features> that still includes <sm>. xmpp.js's setupStreamFeature middleware sees the <sm> element and sends a duplicate <enable>. The server rejects it with <failed> "Stream management is already enabled", and the xmpp.js catch block clears sm.enabled for the session.

Fix: register a prependListener on 'element' (fires before middleware) that strips <sm> from the post-auth <stream:features> when bind2 is negotiating SM inline. A bind2SmEnabling flag gates the removal and self-resets after the first matching element so only one <stream:features> is affected.

…able>

Two bugs observed in XMPP log when ejabberd uses SASL2+bind2+FAST:

1. SASL2 inline SM resumption: when <resumed h="N"/> is embedded inside
   the SASL2 <success> element, xmpp.js never emits a top-level <resumed/>
   nonza and never emits 'online' (entity._ready(true) skips it). Only
   sm.emit("resumed") fires on the SM plugin. Connection.ts was not
   listening for this event, causing connect() to hang until the 30-second
   timeout.

   Fix: register sm.on('resumed', ...) alongside the existing sm.on('fail')
   listener. The handler calls handleResult(true) to resolve the pending
   connection promise and persists the SM state. The nonza-based path is
   unaffected (resolved=true guard makes the sm handler a no-op there).

2. Duplicate <enable> after bind2: ejabberd sends a second <stream:features>
   containing <sm> after the SASL2+bind2 handshake. xmpp.js's
   setupStreamFeature middleware sees it and sends a duplicate <enable>
   stanza. The server rejects this with <failed> "Stream management is
   already enabled", and the xmpp.js catch block silently clears sm.enabled.

   Fix: register a prependListener on 'element' (runs before middleware)
   that strips <sm> from post-auth <stream:features> when bind2 is
   negotiating SM inline. A bind2SmEnabling flag gates the removal and
   self-resets after the first matching features element so only one
   <stream:features> is affected.

Regression tests added for both paths in Connection.test.ts. Also adds
prependListener: vi.fn() to createMockXmppClient so the guard
(typeof prependListener === 'function') passes in tests.
@mremond mremond merged commit e957bb9 into main May 4, 2026
1 check passed
@mremond mremond deleted the fix/sm-sasl2-inline branch May 4, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant