diff --git a/lib/web/websocket/permessage-deflate.js b/lib/web/websocket/permessage-deflate.js new file mode 100644 index 00000000000..6b808e7239e --- /dev/null +++ b/lib/web/websocket/permessage-deflate.js @@ -0,0 +1,39 @@ +'use strict' + +const { createInflateRaw } = require('node:zlib') + +const tail = Buffer.from([0x00, 0x00, 0xff, 0xff]) +const kBuffer = Symbol('kBuffer') + +class PerMessageDeflate { + /** @type {import('node:zlib').InflateRaw} */ + #inflate + + decompress (chunk, fin, callback) { + // An endpoint uses the following algorithm to decompress a message. + // 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the + // payload of the message. + // 2. Decompress the resulting data using DEFLATE. + + if (!this.#inflate) { + this.#inflate = createInflateRaw(/* TODO */) + this.#inflate[kBuffer] = [] + + this.#inflate.on('data', (data) => this.#inflate[kBuffer].push(data)) + } + + this.#inflate.write(chunk) + if (fin) { + this.#inflate.write(tail) + } + + this.#inflate.flush(() => { + const full = Buffer.concat(this.#inflate[kBuffer]) + callback(full) + + this.#inflate[kBuffer].length = 0 + }) + } +} + +module.exports = { PerMessageDeflate } diff --git a/lib/web/websocket/receiver.js b/lib/web/websocket/receiver.js index 748d14123c5..ea94bd50b53 100644 --- a/lib/web/websocket/receiver.js +++ b/lib/web/websocket/receiver.js @@ -17,6 +17,7 @@ const { } = require('./util') const { WebsocketFrameSend } = require('./frame') const { closeWebSocketConnection } = require('./connection') +const { PerMessageDeflate } = require('./permessage-deflate') // This code was influenced by ws released under the MIT license. // Copyright (c) 2011 Einar Otto Stangvik @@ -33,14 +34,18 @@ class ByteParser extends Writable { #info = {} #fragments = [] - /** @type {Set} */ + /** @type {Map} */ #extensions constructor (ws, extensions) { super() this.ws = ws - this.#extensions = new Set(extensions) + this.#extensions = extensions == null ? new Map() : extensions + + if (this.#extensions.has('permessage-deflate')) { + this.#extensions.set('permessage-deflate', new PerMessageDeflate()) + } } /** @@ -157,6 +162,7 @@ class ByteParser extends Writable { this.#info.masked = masked this.#info.fin = fin this.#info.fragmented = fragmented + this.#info.compressed = rsv1 !== 0 } else if (this.#state === parserStates.PAYLOADLENGTH_16) { if (this.#byteOffset < 2) { return callback() @@ -199,16 +205,22 @@ class ByteParser extends Writable { if (isControlFrame(this.#info.opcode)) { this.#loop = this.parseControlFrame(body) } else { - this.#fragments.push(body) - - // If the frame is not fragmented, a message has been received. - // If the frame is fragmented, it will terminate with a fin bit set - // and an opcode of 0 (continuation), therefore we handle that when - // parsing continuation frames, not here. - if (!this.#info.fragmented && this.#info.fin) { - const fullMessage = Buffer.concat(this.#fragments) - websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage) - this.#fragments.length = 0 + if (!this.#info.compressed) { + this.#fragments.push(body) + + // If the frame is not fragmented, a message has been received. + // If the frame is fragmented, it will terminate with a fin bit set + // and an opcode of 0 (continuation), therefore we handle that when + // parsing continuation frames, not here. + if (!this.#info.fragmented && this.#info.fin) { + const fullMessage = Buffer.concat(this.#fragments) + websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage) + this.#fragments.length = 0 + } + } else { + this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (data) => { + websocketMessageReceived(this.ws, this.#info.binaryType, data) + }) } }