diff --git a/package-lock.json b/package-lock.json index d6265add6..95715ac7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "should-sinon": "^0.0.6", "sinon": "^21.0.0", "socket.io-client": "^4.8.1", + "socket.io-parser": "~4.2.6", "sort-package-json": "^2.15.1", "svgo": "^4.0.0", "vite": "^6.4.4", diff --git a/package.json b/package.json index dc75a1128..025c4e6b1 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "should-sinon": "^0.0.6", "sinon": "^21.0.0", "socket.io-client": "^4.8.1", + "socket.io-parser": "~4.2.6", "sort-package-json": "^2.15.1", "svgo": "^4.0.0", "vite": "^6.4.4", diff --git a/test/ui/socket-parser.spec.js b/test/ui/socket-parser.spec.js new file mode 100644 index 000000000..98b1faf50 --- /dev/null +++ b/test/ui/socket-parser.spec.js @@ -0,0 +1,32 @@ +const should = require('should') // eslint-disable-line no-unused-vars +const { Encoder, Decoder, PacketType } = require('socket.io-parser') + +const { socketParser } = require('../../ui/src/util/socket-parser.js') + +function encodeWithBuffers (count) { + const buffers = Array.from({ length: count }, (_, i) => Buffer.from([i])) + const packet = { type: PacketType.EVENT, nsp: '/', data: ['widget-load:chart', buffers] } + return { buffers, parts: new Encoder().encode(packet) } +} + +function decode (decoder, parts) { + let decoded = null + decoder.on('decoded', (packet) => { decoded = packet }) + parts.forEach((part) => decoder.add(part)) + return decoded +} + +describe('socketParser', function () { + it('decodes a packet with more than 10 binary attachments', function () { + const { buffers, parts } = encodeWithBuffers(25) + const decoded = decode(new socketParser.Decoder(), parts) + decoded.data[0].should.equal('widget-load:chart') + decoded.data[1].should.have.length(25) + decoded.data[1].forEach((b, i) => Buffer.from(b).equals(buffers[i]).should.be.true()) + }) + + it('replaces a default decoder that rejects the same packet', function () { + const { parts } = encodeWithBuffers(25) + ;(() => decode(new Decoder(), parts)).should.throw(/too many attachments/) + }) +}) diff --git a/ui/src/main.mjs b/ui/src/main.mjs index 16bec2e8a..174ad4732 100644 --- a/ui/src/main.mjs +++ b/ui/src/main.mjs @@ -11,6 +11,7 @@ import Alerts from './services/alerts.js' import Resize from './directives/resize.js' import { getOrCreateClientId } from './util/client-id' import { nextReconnectInterval } from './util/reconnect-interval' +import { socketParser } from './util/socket-parser' // Vuetify import '@mdi/font/css/materialdesignicons.css' @@ -172,6 +173,7 @@ fetch('_setup') const socket = io({ ...setup.socketio, reconnection: false, + parser: socketParser, query: { clientId, ...(editKey ? { editKey } : {}) } }) diff --git a/ui/src/util/socket-parser.js b/ui/src/util/socket-parser.js new file mode 100644 index 000000000..a15bb8ea1 --- /dev/null +++ b/ui/src/util/socket-parser.js @@ -0,0 +1,11 @@ +import { Decoder, Encoder } from 'socket.io-parser' + +class UnlimitedAttachmentsDecoder extends Decoder { + constructor () { + super({ maxAttachments: Infinity }) + } +} + +const socketParser = { Encoder, Decoder: UnlimitedAttachmentsDecoder } + +export { socketParser }