Skip to content

Commit 38c8b55

Browse files
authored
fix parsing continuation frames in websocket (#3247)
1 parent 9302599 commit 38c8b55

File tree

3 files changed

+100
-14
lines changed

3 files changed

+100
-14
lines changed

lib/web/websocket/receiver.js

+55-13
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const assert = require('node:assert')
55
const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
66
const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols')
77
const { channels } = require('../../core/diagnostics')
8-
const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived, utf8Decode, isControlFrame } = require('./util')
8+
const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived, utf8Decode, isControlFrame, isContinuationFrame } = require('./util')
99
const { WebsocketFrameSend } = require('./frame')
1010
const { CloseEvent } = require('./events')
1111

@@ -80,6 +80,18 @@ class ByteParser extends Writable {
8080
payloadLength
8181
})
8282

83+
if (loop) {
84+
continue
85+
} else {
86+
return
87+
}
88+
} else if (isContinuationFrame(opcode)) {
89+
const loop = this.parseContinuationFrame(callback, {
90+
fin,
91+
fragmented,
92+
payloadLength
93+
})
94+
8395
if (loop) {
8496
continue
8597
} else {
@@ -96,9 +108,6 @@ class ByteParser extends Writable {
96108
this.#state = parserStates.PAYLOADLENGTH_64
97109
}
98110

99-
// TODO(@KhafraDev): handle continuation frames separately as their
100-
// semantics are different from TEXT/BINARY frames.
101-
this.#info.originalOpcode ??= opcode
102111
this.#info.opcode = opcode
103112
this.#info.masked = masked
104113
this.#info.fin = fin
@@ -146,19 +155,16 @@ class ByteParser extends Writable {
146155
// If there is still more data in this chunk that needs to be read
147156
return callback()
148157
} else if (this.#byteOffset >= this.#info.payloadLength) {
149-
// If the server sent multiple frames in a single chunk
150-
151158
const body = this.consume(this.#info.payloadLength)
152-
153159
this.#fragments.push(body)
154160

155-
// If the frame is unfragmented, or a fragmented frame was terminated,
156-
// a message was received
157-
if (!this.#info.fragmented || (this.#info.fin && this.#info.opcode === opcodes.CONTINUATION)) {
161+
// If the frame is not fragmented, a message has been received.
162+
// If the frame is fragmented, it will terminate with a fin bit set
163+
// and an opcode of 0 (continuation), therefore we handle that when
164+
// parsing continuation frames, not here.
165+
if (!this.#info.fragmented) {
158166
const fullMessage = Buffer.concat(this.#fragments)
159-
160-
websocketMessageReceived(this.ws, this.#info.originalOpcode, fullMessage)
161-
167+
websocketMessageReceived(this.ws, this.#info.opcode, fullMessage)
162168
this.#info = {}
163169
this.#fragments.length = 0
164170
}
@@ -265,6 +271,9 @@ class ByteParser extends Writable {
265271
// Control frames can have a payload length of 125 bytes MAX
266272
callback(new Error('Payload length for control frame exceeded 125 bytes.'))
267273
return false
274+
} else if (this.#byteOffset < info.payloadLength) {
275+
callback()
276+
return false
268277
}
269278

270279
const body = this.consume(info.payloadLength)
@@ -357,6 +366,39 @@ class ByteParser extends Writable {
357366
return true
358367
}
359368

369+
/**
370+
* Parses continuation frames.
371+
* @param {Buffer} data
372+
* @param {(err?: Error) => void} callback
373+
* @param {{ fin: boolean, fragmented: boolean, payloadLength: number }} info
374+
*/
375+
parseContinuationFrame (callback, info) {
376+
// If we received a continuation frame before we started parsing another frame.
377+
if (this.#info.opcode === undefined) {
378+
callback(new Error('Received unexpected continuation frame.'))
379+
return false
380+
} else if (this.#byteOffset < info.payloadLength) {
381+
callback()
382+
return false
383+
}
384+
385+
const body = this.consume(info.payloadLength)
386+
this.#fragments.push(body)
387+
388+
// A fragmented message consists of a single frame with the FIN bit
389+
// clear and an opcode other than 0, followed by zero or more frames
390+
// with the FIN bit clear and the opcode set to 0, and terminated by
391+
// a single frame with the FIN bit set and an opcode of 0.
392+
if (info.fin) {
393+
const message = Buffer.concat(this.#fragments)
394+
websocketMessageReceived(this.ws, this.#info.opcode, message)
395+
this.#fragments.length = 0
396+
this.#info = {}
397+
}
398+
399+
return true
400+
}
401+
360402
get closingInfo () {
361403
return this.#info.closeInfo
362404
}

lib/web/websocket/util.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,10 @@ function isControlFrame (opcode) {
222222
)
223223
}
224224

225+
function isContinuationFrame (opcode) {
226+
return opcode === opcodes.CONTINUATION
227+
}
228+
225229
// https://nodejs.org/api/intl.html#detecting-internationalization-support
226230
const hasIntl = typeof process.versions.icu === 'string'
227231
const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undefined
@@ -250,5 +254,6 @@ module.exports = {
250254
failWebsocketConnection,
251255
websocketMessageReceived,
252256
utf8Decode,
253-
isControlFrame
257+
isControlFrame,
258+
isContinuationFrame
254259
}

test/websocket/continuation-frames.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict'
2+
3+
const { test } = require('node:test')
4+
const { WebSocketServer } = require('ws')
5+
const { WebSocket } = require('../..')
6+
const { tspl } = require('@matteo.collina/tspl')
7+
8+
test('Receiving multiple continuation frames works as expected', async (t) => {
9+
const p = tspl(t, { plan: 1 })
10+
11+
const frames = [
12+
Buffer.from([0x01, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f]), // text frame "hello" (fragmented)
13+
Buffer.from([0x00, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f]), // continuation frame "hello" (fin clear)
14+
Buffer.from([0x00, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f]), // continuation frame "hello" (fin clear)
15+
Buffer.from([0x80, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f]) // continuation frame "hello" (fin set)
16+
]
17+
18+
const server = new WebSocketServer({ port: 0 })
19+
20+
server.on('connection', (ws) => {
21+
const socket = ws._socket
22+
23+
for (const frame of frames) {
24+
socket.write(frame)
25+
}
26+
})
27+
28+
const ws = new WebSocket(`ws://localhost:${server.address().port}`)
29+
30+
ws.onerror = p.fail
31+
ws.onmessage = (e) => p.deepStrictEqual(e.data, 'hellohellohellohello')
32+
33+
t.after(() => {
34+
server.close()
35+
ws.close()
36+
})
37+
38+
await p.completed
39+
})

0 commit comments

Comments
 (0)