Skip to content

Commit 6449ae2

Browse files
authored
stream-management: fix enabling when the next element is not the response (#823)
1 parent c0548a5 commit 6449ae2

File tree

4 files changed

+185
-39
lines changed

4 files changed

+185
-39
lines changed

packages/stream-management/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Included and enabled in `@xmpp/client`.
66

77
Supports Node.js and browsers.
88

9-
Does not support requesting acks yet.
9+
When the session is resumed the `online` event is not emitted as session resumption is transparent.
10+
However `entity.status` is set to `online`.
11+
If the session fails to resume, entity will fallback to regular session establishment in which case `online` event will be emitted.
1012

11-
Responds to ack requests and resumes connection uppon disconnect whenever possible.
12-
13-
`online` event is not emitted when the session is resumed as it should be transparent.
13+
Automatically responds to acks but does not support requesting acks yet.

packages/stream-management/index.js

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,40 @@
11
'use strict'
22

33
const xml = require('@xmpp/xml')
4-
const StanzaError = require('@xmpp/middleware/lib/StanzaError')
54

65
// https://xmpp.org/extensions/xep-0198.html
76

87
const NS = 'urn:xmpp:sm:3'
98

109
async function enable(entity, resume, max) {
11-
const response = await entity.sendReceive(
10+
entity.send(
1211
xml('enable', {xmlns: NS, max, resume: resume ? 'true' : undefined})
1312
)
1413

15-
if (!response.is('enabled')) {
16-
throw StanzaError.fromElement(response)
17-
}
14+
return new Promise((resolve, reject) => {
15+
function listener(nonza) {
16+
if (nonza.is('enabled', NS)) {
17+
resolve(nonza)
18+
} else if (nonza.is('failed', NS)) {
19+
reject(nonza)
20+
} else {
21+
return
22+
}
1823

19-
return response
24+
entity.removeListener('nonza', listener)
25+
}
26+
27+
entity.on('nonza', listener)
28+
})
2029
}
2130

2231
async function resume(entity, h, previd) {
2332
const response = await entity.sendReceive(
2433
xml('resume', {xmlns: NS, h, previd})
2534
)
2635

27-
if (!response.is('resumed')) {
28-
throw StanzaError.fromElement(response)
36+
if (!response.is('resumed', NS)) {
37+
throw response
2938
}
3039

3140
return response
@@ -48,14 +57,13 @@ module.exports = function({streamFeatures, entity, middleware}) {
4857
address = jid
4958
sm.outbound = 0
5059
sm.inbound = 0
51-
sm.enabled = false
5260
})
5361

5462
entity.on('offline', () => {
55-
address = null
5663
sm.outbound = 0
5764
sm.inbound = 0
5865
sm.enabled = false
66+
sm.id = ''
5967
})
6068

6169
middleware.use((context, next) => {
@@ -73,19 +81,24 @@ module.exports = function({streamFeatures, entity, middleware}) {
7381
return next()
7482
})
7583

84+
// https://xmpp.org/extensions/xep-0198.html#enable
85+
// 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
86+
7687
streamFeatures.use('sm', NS, async (context, next) => {
7788
// Resuming
78-
if (sm.id && address) {
89+
if (sm.id) {
7990
try {
8091
await resume(entity, sm.inbound, sm.id)
92+
sm.enabled = true
8193
entity.jid = address
8294
entity.status = 'online'
8395
return true
96+
// If resumption fails, continue with session establishment
8497
// eslint-disable-next-line no-unused-vars
8598
} catch (err) {
8699
sm.id = ''
87-
address = null
88100
sm.enabled = false
101+
sm.outbound = 0
89102
}
90103
}
91104

@@ -99,10 +112,18 @@ module.exports = function({streamFeatures, entity, middleware}) {
99112
// > The counter for an entity's own sent stanzas is set to zero and started after sending either <enable/> or <enabled/>.
100113
sm.outbound = 0
101114

102-
const response = await promiseEnable
115+
try {
116+
const response = await promiseEnable
117+
sm.enabled = true
118+
sm.id = response.attrs.id
119+
sm.max = response.attrs.max
120+
// eslint-disable-next-line no-unused-vars
121+
} catch (err) {
122+
sm.enabled = false
123+
}
124+
103125
sm.inbound = 0
104-
sm.enabled = true
105-
sm.id = response.attrs.id
106-
sm.max = response.attrs.max
107126
})
127+
128+
return sm
108129
}

packages/stream-management/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"management"
1313
],
1414
"dependencies": {
15-
"@xmpp/middleware": "^0.11.0",
1615
"@xmpp/xml": "^0.11.0"
1716
},
1817
"engines": {

packages/stream-management/test.js

Lines changed: 144 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,169 @@
11
'use strict'
22

33
const test = require('ava')
4-
const {mockClient, promise, timeout} = require('@xmpp/test')
4+
const {mockClient} = require('@xmpp/test')
55

6-
test('mandatory', async t => {
6+
function tick() {
7+
return new Promise(resolve => process.nextTick(resolve))
8+
}
9+
10+
test('enable - enabled', async t => {
711
const {entity} = mockClient()
812

913
entity.mockInput(
1014
<features xmlns="http://etherx.jabber.org/streams">
11-
<session xmlns="urn:ietf:params:xml:ns:xmpp-session" />
15+
<sm xmlns="urn:xmpp:sm:3" />
1216
</features>
1317
)
1418

15-
entity.scheduleIncomingResult()
19+
entity.streamManagement.outbound = 45
20+
21+
t.deepEqual(
22+
await entity.catchOutgoing(),
23+
<enable xmlns="urn:xmpp:sm:3" resume="true" />
24+
)
25+
26+
t.is(entity.streamManagement.outbound, 0)
27+
t.is(entity.streamManagement.enabled, false)
28+
t.is(entity.streamManagement.id, '')
1629

17-
await entity.catchOutgoingSet().then(child => {
18-
t.deepEqual(child, <session xmlns="urn:ietf:params:xml:ns:xmpp-session" />)
19-
return child
20-
})
30+
entity.mockInput(
31+
<enabled
32+
xmlns="urn:xmpp:sm:3"
33+
id="some-long-sm-id"
34+
location="[2001:41D0:1:A49b::1]:9222"
35+
resume="true"
36+
/>
37+
)
2138

22-
await promise(entity, 'online')
39+
await tick()
40+
41+
t.is(entity.streamManagement.id, 'some-long-sm-id')
42+
t.is(entity.streamManagement.enabled, true)
2343
})
2444

25-
test('optional', async t => {
45+
test('enable - message - enabled', async t => {
2646
const {entity} = mockClient()
2747

2848
entity.mockInput(
2949
<features xmlns="http://etherx.jabber.org/streams">
30-
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
31-
<optional />
32-
</session>
50+
<sm xmlns="urn:xmpp:sm:3" />
3351
</features>
3452
)
3553

36-
const promiseSend = promise(entity, 'send')
54+
entity.streamManagement.outbound = 45
55+
56+
t.deepEqual(
57+
await entity.catchOutgoing(),
58+
<enable xmlns="urn:xmpp:sm:3" resume="true" />
59+
)
60+
61+
t.is(entity.streamManagement.outbound, 0)
62+
t.is(entity.streamManagement.enabled, false)
63+
t.is(entity.streamManagement.id, '')
64+
65+
entity.mockInput(<message />)
66+
67+
t.is(entity.streamManagement.enabled, false)
68+
t.is(entity.streamManagement.inbound, 1)
69+
70+
entity.mockInput(
71+
<enabled
72+
xmlns="urn:xmpp:sm:3"
73+
id="some-long-sm-id"
74+
location="[2001:41D0:1:A49b::1]:9222"
75+
resume="true"
76+
/>
77+
)
78+
79+
await tick()
80+
81+
t.is(entity.streamManagement.id, 'some-long-sm-id')
82+
t.is(entity.streamManagement.enabled, true)
83+
})
84+
85+
test('enable - failed', async t => {
86+
const {entity} = mockClient()
87+
88+
entity.mockInput(
89+
<features xmlns="http://etherx.jabber.org/streams">
90+
<sm xmlns="urn:xmpp:sm:3" />
91+
</features>
92+
)
93+
94+
entity.streamManagement.outbound = 45
95+
96+
t.deepEqual(
97+
await entity.catchOutgoing(),
98+
<enable xmlns="urn:xmpp:sm:3" resume="true" />
99+
)
100+
101+
t.is(entity.streamManagement.outbound, 0)
102+
entity.streamManagement.enabled = true
103+
104+
entity.mockInput(<failed xmlns="urn:xmpp:sm:3" />)
105+
106+
await tick()
107+
108+
t.is(entity.streamManagement.enabled, false)
109+
})
110+
111+
test('resume - resumed', async t => {
112+
const {entity} = mockClient()
113+
114+
entity.status = 'offline'
115+
entity.streamManagement.id = 'bar'
116+
117+
entity.mockInput(
118+
<features xmlns="http://etherx.jabber.org/streams">
119+
<sm xmlns="urn:xmpp:sm:3" />
120+
</features>
121+
)
122+
123+
entity.streamManagement.outbound = 45
124+
125+
t.deepEqual(
126+
await entity.catchOutgoing(),
127+
<resume xmlns="urn:xmpp:sm:3" previd="bar" h="0" />
128+
)
129+
130+
t.is(entity.streamManagement.enabled, false)
131+
132+
t.is(entity.status, 'offline')
133+
134+
entity.mockInput(<resumed xmlns="urn:xmpp:sm:3" />)
135+
136+
await tick()
137+
138+
t.is(entity.streamManagement.outbound, 45)
139+
t.is(entity.status, 'online')
140+
})
141+
142+
test('resume - failed', async t => {
143+
const {entity} = mockClient()
144+
145+
entity.status = 'bar'
146+
entity.streamManagement.id = 'bar'
147+
entity.streamManagement.enabled = true
148+
entity.streamManagement.outbound = 45
149+
150+
entity.mockInput(
151+
<features xmlns="http://etherx.jabber.org/streams">
152+
<sm xmlns="urn:xmpp:sm:3" />
153+
</features>
154+
)
155+
156+
t.deepEqual(
157+
await entity.catchOutgoing(),
158+
<resume xmlns="urn:xmpp:sm:3" previd="bar" h="0" />
159+
)
160+
161+
entity.mockInput(<failed xmlns="urn:xmpp:sm:3" />)
37162

38-
await promise(entity, 'online')
163+
await tick()
39164

40-
await timeout(promiseSend, 0).catch(err => {
41-
t.is(err.name, 'TimeoutError')
42-
})
165+
t.is(entity.status, 'bar')
166+
t.is(entity.streamManagement.id, '')
167+
t.is(entity.streamManagement.enabled, false)
168+
t.is(entity.streamManagement.outbound, 0)
43169
})

0 commit comments

Comments
 (0)