| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 8cb10f9 commit a027a4a
11 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -25,7 +25,8 @@ Returns: `Client` | |||
| 25 | 25 | * **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB. | |
| 26 | 26 | * **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable. | |
| 27 | 27 | * **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options. | |
| 28 | - * **maxFragments** `number` (optional) - Defailt: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit. | ||
| 28 | + * **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit. | ||
| 29 | + * **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit. | ||
| 29 | 30 | * **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections. | |
| 30 | 31 | * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`. | |
| 31 | 32 | * **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,7 +11,7 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = require('../core/sy | |||
| 11 | 11 | ||
| 12 | 12 | const kOnDestroyed = Symbol('onDestroyed') | |
| 13 | 13 | const kOnClosed = Symbol('onClosed') | |
| 14 | - const kWebSocketOptions = Symbol('web socket options') | ||
| 14 | + const kWebSocketOptions = Symbol('webSocketOptions') | ||
| 15 | 15 | ||
| 16 | 16 | class DispatcherBase extends Dispatcher { | |
| 17 | 17 | /** @type {boolean} */ | |
@@ -27,16 +27,20 @@ class DispatcherBase extends Dispatcher { | |||
| 27 | 27 | [kOnClosed] = null | |
| 28 | 28 | ||
| 29 | 29 | /** | |
| 30 | - * @param {{ webSocket?: { maxFragments?: number } }} [opts] | ||
| 30 | + * @param {import('../../types/dispatcher').DispatcherOptions} [opts] | ||
| 31 | 31 | */ | |
| 32 | 32 | constructor (opts) { | |
| 33 | 33 | super() | |
| 34 | 34 | this[kWebSocketOptions] = opts?.webSocket ?? {} | |
| 35 | 35 | } | |
| 36 | 36 | ||
| 37 | + /** | ||
| 38 | + * @returns {import('../../types/dispatcher').WebSocketOptions} | ||
| 39 | + */ | ||
| 37 | 40 | get webSocketOptions () { | |
| 38 | 41 | return { | |
| 39 | - maxFragments: this[kWebSocketOptions].maxFragments ?? 131072 | ||
| 42 | + maxFragments: this[kWebSocketOptions].maxFragments ?? 131072, | ||
| 43 | + maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 // 128 MB default | ||
| 40 | 44 | } | |
| 41 | 45 | } | |
| 42 | 46 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -8,40 +8,35 @@ const tail = Buffer.from([0x00, 0x00, 0xff, 0xff]) | |||
| 8 | 8 | const kBuffer = Symbol('kBuffer') | |
| 9 | 9 | const kLength = Symbol('kLength') | |
| 10 | 10 | ||
| 11 | - // Default maximum decompressed message size: 4 MB | ||
| 12 | - const kDefaultMaxDecompressedSize = 4 * 1024 * 1024 | ||
| 13 | - | ||
| 14 | 11 | class PerMessageDeflate { | |
| 15 | 12 | /** @type {import('node:zlib').InflateRaw} */ | |
| 16 | 13 | #inflate | |
| 17 | 14 | ||
| 18 | 15 | #options = {} | |
| 19 | 16 | ||
| 20 | - /** @type {boolean} */ | ||
| 21 | - #aborted = false | ||
| 22 | - | ||
| 23 | - /** @type {Function|null} */ | ||
| 24 | - #currentCallback = null | ||
| 17 | + #maxPayloadSize = 0 | ||
| 25 | 18 | ||
| 26 | 19 | /** | |
| 27 | 20 | * @param {Map<string, string>} extensions | |
| 28 | 21 | */ | |
| 29 | - constructor (extensions) { | ||
| 22 | + constructor (extensions, options) { | ||
| 30 | 23 | this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover') | |
| 31 | 24 | this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits') | |
| 25 | + | ||
| 26 | + this.#maxPayloadSize = options.maxPayloadSize | ||
| 32 | 27 | } | |
| 33 | 28 | ||
| 29 | + /** | ||
| 30 | + * Decompress a compressed payload. | ||
| 31 | + * @param {Buffer} chunk Compressed data | ||
| 32 | + * @param {boolean} fin Final fragment flag | ||
| 33 | + * @param {Function} callback Callback function | ||
| 34 | + */ | ||
| 34 | 35 | decompress (chunk, fin, callback) { | |
| 35 | 36 | // An endpoint uses the following algorithm to decompress a message. | |
| 36 | 37 | // 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the | |
| 37 | 38 | // payload of the message. | |
| 38 | 39 | // 2. Decompress the resulting data using DEFLATE. | |
| 39 | - | ||
| 40 | - if (this.#aborted) { | ||
| 41 | - callback(new MessageSizeExceededError()) | ||
| 42 | - return | ||
| 43 | - } | ||
| 44 | - | ||
| 45 | 40 | if (!this.#inflate) { | |
| 46 | 41 | let windowBits = Z_DEFAULT_WINDOWBITS | |
| 47 | 42 | ||
@@ -64,23 +59,12 @@ class PerMessageDeflate { | |||
| 64 | 59 | this.#inflate[kLength] = 0 | |
| 65 | 60 | ||
| 66 | 61 | this.#inflate.on('data', (data) => { | |
| 67 | - if (this.#aborted) { | ||
| 68 | - return | ||
| 69 | - } | ||
| 70 | - | ||
| 71 | 62 | this.#inflate[kLength] += data.length | |
| 72 | 63 | ||
| 73 | - if (this.#inflate[kLength] > kDefaultMaxDecompressedSize) { | ||
| 74 | - this.#aborted = true | ||
| 64 | + if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) { | ||
| 65 | + callback(new MessageSizeExceededError()) | ||
| 75 | 66 | this.#inflate.removeAllListeners() | |
| 76 | - this.#inflate.destroy() | ||
| 77 | 67 | this.#inflate = null | |
| 78 | - | ||
| 79 | - if (this.#currentCallback) { | ||
| 80 | - const cb = this.#currentCallback | ||
| 81 | - this.#currentCallback = null | ||
| 82 | - cb(new MessageSizeExceededError()) | ||
| 83 | - } | ||
| 84 | 68 | return | |
| 85 | 69 | } | |
| 86 | 70 | ||
@@ -93,22 +77,20 @@ class PerMessageDeflate { | |||
| 93 | 77 | }) | |
| 94 | 78 | } | |
| 95 | 79 | ||
| 96 | - this.#currentCallback = callback | ||
| 97 | 80 | this.#inflate.write(chunk) | |
| 98 | 81 | if (fin) { | |
| 99 | 82 | this.#inflate.write(tail) | |
| 100 | 83 | } | |
| 101 | 84 | ||
| 102 | 85 | this.#inflate.flush(() => { | |
| 103 | - if (this.#aborted || !this.#inflate) { | ||
| 86 | + if (!this.#inflate) { | ||
| 104 | 87 | return | |
| 105 | 88 | } | |
| 106 | 89 | ||
| 107 | 90 | const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength]) | |
| 108 | 91 | ||
| 109 | 92 | this.#inflate[kBuffer].length = 0 | |
| 110 | 93 | this.#inflate[kLength] = 0 | |
| 111 | - this.#currentCallback = null | ||
| 112 | 94 | ||
| 113 | 95 | callback(null, full) | |
| 114 | 96 | }) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -42,20 +42,24 @@ class ByteParser extends Writable { | |||
| 42 | 42 | /** @type {number} */ | |
| 43 | 43 | #maxFragments | |
| 44 | 44 | ||
| 45 | + /** @type {number} */ | ||
| 46 | + #maxPayloadSize | ||
| 47 | + | ||
| 45 | 48 | /** | |
| 46 | 49 | * @param {import('./websocket').Handler} handler | |
| 47 | 50 | * @param {Map<string, string>|null} extensions | |
| 48 | - * @param {{ maxFragments?: number }} [options] | ||
| 51 | + * @param {{ maxFragments?: number, maxPayloadSize?: number }} [options] | ||
| 49 | 52 | */ | |
| 50 | 53 | constructor (handler, extensions, options = {}) { | |
| 51 | 54 | super() | |
| 52 | 55 | ||
| 53 | 56 | this.#handler = handler | |
| 54 | 57 | this.#extensions = extensions == null ? new Map() : extensions | |
| 55 | 58 | this.#maxFragments = options.maxFragments ?? 0 | |
| 59 | + this.#maxPayloadSize = options.maxPayloadSize ?? 0 | ||
| 56 | 60 | ||
| 57 | 61 | if (this.#extensions.has('permessage-deflate')) { | |
| 58 | - this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions)) | ||
| 62 | + this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options)) | ||
| 59 | 63 | } | |
| 60 | 64 | } | |
| 61 | 65 | ||
@@ -71,6 +75,19 @@ class ByteParser extends Writable { | |||
| 71 | 75 | this.run(callback) | |
| 72 | 76 | } | |
| 73 | 77 | ||
| 78 | + #validatePayloadLength () { | ||
| 79 | + if ( | ||
| 80 | + this.#maxPayloadSize > 0 && | ||
| 81 | + !isControlFrame(this.#info.opcode) && | ||
| 82 | + this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize | ||
| 83 | + ) { | ||
| 84 | + failWebsocketConnection(this.#handler, 1009, 'Payload size exceeds maximum allowed size') | ||
| 85 | + return false | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + return true | ||
| 89 | + } | ||
| 90 | + | ||
| 74 | 91 | /** | |
| 75 | 92 | * Runs whenever a new chunk is received. | |
| 76 | 93 | * Callback is called whenever there are no more chunks buffering, | |
@@ -159,6 +176,10 @@ class ByteParser extends Writable { | |||
| 159 | 176 | if (payloadLength <= 125) { | |
| 160 | 177 | this.#info.payloadLength = payloadLength | |
| 161 | 178 | this.#state = parserStates.READ_DATA | |
| 179 | + | ||
| 180 | + if (!this.#validatePayloadLength()) { | ||
| 181 | + return | ||
| 182 | + } | ||
| 162 | 183 | } else if (payloadLength === 126) { | |
| 163 | 184 | this.#state = parserStates.PAYLOADLENGTH_16 | |
| 164 | 185 | } else if (payloadLength === 127) { | |
@@ -183,6 +204,10 @@ class ByteParser extends Writable { | |||
| 183 | 204 | ||
| 184 | 205 | this.#info.payloadLength = buffer.readUInt16BE(0) | |
| 185 | 206 | this.#state = parserStates.READ_DATA | |
| 207 | + | ||
| 208 | + if (!this.#validatePayloadLength()) { | ||
| 209 | + return | ||
| 210 | + } | ||
| 186 | 211 | } else if (this.#state === parserStates.PAYLOADLENGTH_64) { | |
| 187 | 212 | if (this.#byteOffset < 8) { | |
| 188 | 213 | return callback() | |
@@ -205,6 +230,10 @@ class ByteParser extends Writable { | |||
| 205 | 230 | ||
| 206 | 231 | this.#info.payloadLength = lower | |
| 207 | 232 | this.#state = parserStates.READ_DATA | |
| 233 | + | ||
| 234 | + if (!this.#validatePayloadLength()) { | ||
| 235 | + return | ||
| 236 | + } | ||
| 208 | 237 | } else if (this.#state === parserStates.READ_DATA) { | |
| 209 | 238 | if (this.#byteOffset < this.#info.payloadLength) { | |
| 210 | 239 | return callback() | |
@@ -217,7 +246,7 @@ class ByteParser extends Writable { | |||
| 217 | 246 | this.#state = parserStates.INFO | |
| 218 | 247 | } else { | |
| 219 | 248 | if (!this.#info.compressed) { | |
| 220 | - if (body.length && !this.writeFragments(body)) { | ||
| 249 | + if (!this.writeFragments(body)) { | ||
| 221 | 250 | return | |
| 222 | 251 | } | |
| 223 | 252 | ||
@@ -231,31 +260,41 @@ class ByteParser extends Writable { | |||
| 231 | 260 | ||
| 232 | 261 | this.#state = parserStates.INFO | |
| 233 | 262 | } else { | |
| 234 | - this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => { | ||
| 235 | - if (error) { | ||
| 236 | - // Use 1009 (Message Too Big) for decompression size limit errors | ||
| 237 | - const code = error instanceof MessageSizeExceededError ? 1009 : 1007 | ||
| 238 | - failWebsocketConnection(this.#handler, code, error.message) | ||
| 239 | - return | ||
| 240 | - } | ||
| 241 | - | ||
| 242 | - if (data.length && !this.writeFragments(data)) { | ||
| 243 | - return | ||
| 244 | - } | ||
| 245 | - | ||
| 246 | - if (!this.#info.fin) { | ||
| 247 | - this.#state = parserStates.INFO | ||
| 263 | + this.#extensions.get('permessage-deflate').decompress( | ||
| 264 | + body, | ||
| 265 | + this.#info.fin, | ||
| 266 | + (error, data) => { | ||
| 267 | + if (error) { | ||
| 268 | + const code = error instanceof MessageSizeExceededError ? 1009 : 1007 | ||
| 269 | + failWebsocketConnection(this.#handler, code, error.message) | ||
| 270 | + return | ||
| 271 | + } | ||
| 272 | + | ||
| 273 | + if (!this.writeFragments(data)) { | ||
| 274 | + return | ||
| 275 | + } | ||
| 276 | + | ||
| 277 | + // Check cumulative fragment size | ||
| 278 | + if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { | ||
| 279 | + failWebsocketConnection(this.#handler, 1009, new MessageSizeExceededError().message) | ||
| 280 | + return | ||
| 281 | + } | ||
| 282 | + | ||
| 283 | + if (!this.#info.fin) { | ||
| 284 | + this.#state = parserStates.INFO | ||
| 285 | + this.#loop = true | ||
| 286 | + this.run(callback) | ||
| 287 | + return | ||
| 288 | + } | ||
| 289 | + | ||
| 290 | + websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments()) | ||
| 291 | + | ||
| 248 | 292 | this.#loop = true | |
| 293 | + this.#state = parserStates.INFO | ||
| 249 | 294 | this.run(callback) | |
| 250 | - return | ||
| 251 | - } | ||
| 252 | - | ||
| 253 | - websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments()) | ||
| 254 | - | ||
| 255 | - this.#loop = true | ||
| 256 | - this.#state = parserStates.INFO | ||
| 257 | - this.run(callback) | ||
| 258 | - }) | ||
| 295 | + }, | ||
| 296 | + this.#fragmentsBytes | ||
| 297 | + ) | ||
| 259 | 298 | ||
| 260 | 299 | this.#loop = false | |
| 261 | 300 | break | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -258,7 +258,14 @@ class WebSocketStream { | |||
| 258 | 258 | #onConnectionEstablished (response, parsedExtensions) { | |
| 259 | 259 | this.#handler.socket = response.socket | |
| 260 | 260 | ||
| 261 | - const parser = new ByteParser(this.#handler, parsedExtensions) | ||
| 261 | + // Get options from dispatcher options | ||
| 262 | + const maxFragments = this.#handler.controller.dispatcher?.webSocketOptions?.maxFragments | ||
| 263 | + const maxPayloadSize = this.#handler.controller.dispatcher?.webSocketOptions?.maxPayloadSize | ||
| 264 | + | ||
| 265 | + const parser = new ByteParser(this.#handler, parsedExtensions, { | ||
| 266 | + maxFragments, | ||
| 267 | + maxPayloadSize | ||
| 268 | + }) | ||
| 262 | 269 | parser.on('drain', () => this.#handler.onParserDrain()) | |
| 263 | 270 | parser.on('error', (err) => this.#handler.onParserError(err)) | |
| 264 | 271 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -468,10 +468,13 @@ class WebSocket extends EventTarget { | |||
| 468 | 468 | // once this happens, the connection is open | |
| 469 | 469 | this.#handler.socket = response.socket | |
| 470 | 470 | ||
| 471 | - const maxFragments = this.#handler.controller.dispatcher?.webSocketOptions?.maxFragments | ||
| 471 | + const webSocketOptions = this.#handler.controller.dispatcher?.webSocketOptions | ||
| 472 | + const maxFragments = webSocketOptions?.maxFragments | ||
| 473 | + const maxPayloadSize = webSocketOptions?.maxPayloadSize | ||
| 472 | 474 | ||
| 473 | 475 | const parser = new ByteParser(this.#handler, parsedExtensions, { | |
| 474 | - maxFragments | ||
| 476 | + maxFragments, | ||
| 477 | + maxPayloadSize | ||
| 475 | 478 | }) | |
| 476 | 479 | parser.on('drain', () => this.#handler.onParserDrain()) | |
| 477 | 480 | parser.on('error', (err) => this.#handler.onParserError(err)) | |
| Back | FazBrowse Home | New Git URL |
0 commit comments