Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

websocket: fix ByteParser.run and parseCloseBody #3080

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 16 additions & 12 deletions lib/web/websocket/receiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class ByteParser extends Writable {
// sending a Close frame in response, the endpoint typically echos the
// status code it received.)
let body = emptyBuffer
if (this.#info.closeInfo.code) {
if (this.#info.closeInfo?.code) {
body = Buffer.allocUnsafe(2)
body.writeUInt16BE(this.#info.closeInfo.code, 0)
}
Expand Down Expand Up @@ -298,26 +298,30 @@ class ByteParser extends Writable {
// defined as the status code (Section 7.4) contained in the first Close
// control frame received by the application
code = data.readUInt16BE(0)
if (!isValidStatusCode(code)) {
return null
}
} else {
return null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A close frame doesn't need to contain a reason or code, data (the body) can be empty here. You could assert the data length isn't 1 here, but we do check for its length elsewhere so it'd just be for safety.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is:
https://app.codecov.io/gh/nodejs/undici/pull/3080/blob/lib/web/websocket/receiver.js

This line is covered by test. So it is possible that the size of the data is 0.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, I said that. The buffer here is not the entire frame (which has to have a minimum length of 2), but the body of a frame. If there is no code and no reason, it is still a valid close frame, and will be empty here. Returning null means that the frame body is invalid, but that's not correct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, the body can be empty or 2+ bytes, but it cannot be 1 byte, which is why I mentioned earlier that for safety we could assert that the body isn't a single byte. We already check elsewhere, but as I mentioned earlier, would be for safety.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well but it is happening:

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what that has to do with what I said?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means, that your claim, that we already handled all cases with data.length < 2 is invalid. Either data.length === 0 or data.length === 1 or both are not covered by the calling function.

}

// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6
/** @type {Buffer} */
let reason = data.subarray(2)
/** @type {number} */
let reasonOffset = 2

// Remove BOM
if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) {
reason = reason.subarray(3)
// Detect BOM and ignore it
if (data.length >= 5 && data[2] === 0xEF && data[3] === 0xBB && data[4] === 0xBF) {
reasonOffset = 5
}

if (code !== undefined && !isValidStatusCode(code)) {
return null
if (data.length - reasonOffset < 1) {
return { code, reason: '' }
}

let reason = ''
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a misunderstanding here. Returning null means that the close frame's body is invalid. If we can't parse the reason, due to invalid utf-8, we need to return null here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be a false conclusion. We got a valid code, so we wont need to set the code 1005 or 1006. The message is somewhat corrupt. So. We need to prefer the valid code and discard the message.

try {
reason = utf8Decode(reason)
} catch {
return null
}
reason = utf8Decode(data.subarray(reasonOffset))
} catch { }

return { code, reason }
}
Expand Down
65 changes: 47 additions & 18 deletions test/websocket/receive.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
'use strict'

const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const assert = require('node:assert')
const { WebSocketServer } = require('ws')
const { WebSocket } = require('../..')

test('Receiving a frame with a payload length > 2^31-1 bytes', () => {
test('Receiving a frame with a payload length > 2^31-1 bytes', async (t) => {
t = tspl(t, { plan: 1 })

const server = new WebSocketServer({ port: 0 })

server.on('connection', (ws) => {
Expand All @@ -16,19 +18,20 @@ test('Receiving a frame with a payload length > 2^31-1 bytes', () => {

const ws = new WebSocket(`ws://localhost:${server.address().port}`)

return new Promise((resolve, reject) => {
ws.onmessage = reject
ws.onmessage = t.fail

ws.addEventListener('error', (event) => {
assert.ok(event.error instanceof Error) // error event is emitted
ws.close()
server.close()
resolve()
})
ws.addEventListener('error', (event) => {
ws.close()
server.close()
t.ok(event.error instanceof Error) // error event is emitted
})

await t.completed
})

test('Receiving an ArrayBuffer', () => {
test('Receiving an ArrayBuffer', async (t) => {
t = tspl(t, { plan: 3 })

const server = new WebSocketServer({ port: 0 })

server.on('connection', (ws) => {
Expand All @@ -43,18 +46,44 @@ test('Receiving an ArrayBuffer', () => {

ws.addEventListener('open', () => {
ws.binaryType = 'what'
assert.equal(ws.binaryType, 'blob')
t.strictEqual(ws.binaryType, 'blob')

ws.binaryType = 'arraybuffer' // <--
ws.send('Hello')
})

return new Promise((resolve) => {
ws.addEventListener('message', ({ data }) => {
assert.ok(data instanceof ArrayBuffer)
assert.deepStrictEqual(Buffer.from(data), Buffer.from('Hello'))
server.close()
resolve()
ws.addEventListener('message', ({ data }) => {
t.ok(data instanceof ArrayBuffer)
t.deepStrictEqual(Buffer.from(data), Buffer.from('Hello'))
server.close()
})

await t.completed
})

test('Receiving a close reason', async (t) => {
t = tspl(t, { plan: 1 })

const server = new WebSocketServer({ port: 0 })

server.on('connection', (ws) => {
ws.on('message', (data, isBinary) => {
ws.send(data, { binary: true })

ws.close(1000, Buffer.from('\uFEFFGood Bye!'))
})
})

const ws = new WebSocket(`ws://localhost:${server.address().port}`)

ws.addEventListener('open', () => {
ws.send('Hello')
})

ws.addEventListener('close', ({ reason }) => {
t.strictEqual(reason, 'Good Bye!')
server.close()
})

await t.completed
})