| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 04201f8 commit 8cb10f9
10 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -24,6 +24,8 @@ Returns: `Client` | |||
| 24 | 24 | * **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 2 seconds. | |
| 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 | + * **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. | ||
| 27 | 29 | * **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. | |
| 28 | 30 | * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`. | |
| 29 | 31 | * **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 | |
|---|---|---|---|
@@ -35,7 +35,7 @@ class Agent extends DispatcherBase { | |||
| 35 | 35 | throw new InvalidArgumentError('maxOrigins must be a number greater than 0') | |
| 36 | 36 | } | |
| 37 | 37 | ||
| 38 | - super() | ||
| 38 | + super(options) | ||
| 39 | 39 | ||
| 40 | 40 | if (connect && typeof connect !== 'function') { | |
| 41 | 41 | connect = { ...connect } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -54,7 +54,7 @@ class BalancedPool extends PoolBase { | |||
| 54 | 54 | throw new InvalidArgumentError('factory must be a function.') | |
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | - super() | ||
| 57 | + super(opts) | ||
| 58 | 58 | ||
| 59 | 59 | this[kOptions] = { ...util.deepClone(opts) } | |
| 60 | 60 | this[kOptions].interceptors = opts.interceptors | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -114,7 +114,8 @@ class Client extends DispatcherBase { | |||
| 114 | 114 | useH2c, | |
| 115 | 115 | initialWindowSize, | |
| 116 | 116 | connectionWindowSize, | |
| 117 | - pingInterval | ||
| 117 | + pingInterval, | ||
| 118 | + webSocket | ||
| 118 | 119 | } = {}) { | |
| 119 | 120 | if (keepAlive !== undefined) { | |
| 120 | 121 | throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') | |
@@ -222,7 +223,7 @@ class Client extends DispatcherBase { | |||
| 222 | 223 | throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0') | |
| 223 | 224 | } | |
| 224 | 225 | ||
| 225 | - super() | ||
| 226 | + super({ webSocket }) | ||
| 226 | 227 | ||
| 227 | 228 | if (typeof connect !== 'function') { | |
| 228 | 229 | connect = buildConnector({ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,6 +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 | 15 | ||
| 15 | 16 | class DispatcherBase extends Dispatcher { | |
| 16 | 17 | /** @type {boolean} */ | |
@@ -25,6 +26,20 @@ class DispatcherBase extends Dispatcher { | |||
| 25 | 26 | /** @type {Array<Function>|null} */ | |
| 26 | 27 | [kOnClosed] = null | |
| 27 | 28 | ||
| 29 | + /** | ||
| 30 | + * @param {{ webSocket?: { maxFragments?: number } }} [opts] | ||
| 31 | + */ | ||
| 32 | + constructor (opts) { | ||
| 33 | + super() | ||
| 34 | + this[kWebSocketOptions] = opts?.webSocket ?? {} | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + get webSocketOptions () { | ||
| 38 | + return { | ||
| 39 | + maxFragments: this[kWebSocketOptions].maxFragments ?? 131072 | ||
| 40 | + } | ||
| 41 | + } | ||
| 42 | + | ||
| 28 | 43 | /** @returns {boolean} */ | |
| 29 | 44 | get destroyed () { | |
| 30 | 45 | return this[kDestroyed] | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -63,7 +63,7 @@ class Pool extends PoolBase { | |||
| 63 | 63 | }) | |
| 64 | 64 | } | |
| 65 | 65 | ||
| 66 | - super() | ||
| 66 | + super(options) | ||
| 67 | 67 | ||
| 68 | 68 | this[kConnections] = connections || null | |
| 69 | 69 | this[kUrl] = util.parseOrigin(origin) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -39,15 +39,20 @@ class ByteParser extends Writable { | |||
| 39 | 39 | /** @type {import('./websocket').Handler} */ | |
| 40 | 40 | #handler | |
| 41 | 41 | ||
| 42 | + /** @type {number} */ | ||
| 43 | + #maxFragments | ||
| 44 | + | ||
| 42 | 45 | /** | |
| 43 | 46 | * @param {import('./websocket').Handler} handler | |
| 44 | 47 | * @param {Map<string, string>|null} extensions | |
| 48 | + * @param {{ maxFragments?: number }} [options] | ||
| 45 | 49 | */ | |
| 46 | - constructor (handler, extensions) { | ||
| 50 | + constructor (handler, extensions, options = {}) { | ||
| 47 | 51 | super() | |
| 48 | 52 | ||
| 49 | 53 | this.#handler = handler | |
| 50 | 54 | this.#extensions = extensions == null ? new Map() : extensions | |
| 55 | + this.#maxFragments = options.maxFragments ?? 0 | ||
| 51 | 56 | ||
| 52 | 57 | if (this.#extensions.has('permessage-deflate')) { | |
| 53 | 58 | this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions)) | |
@@ -212,7 +217,9 @@ class ByteParser extends Writable { | |||
| 212 | 217 | this.#state = parserStates.INFO | |
| 213 | 218 | } else { | |
| 214 | 219 | if (!this.#info.compressed) { | |
| 215 | - this.writeFragments(body) | ||
| 220 | + if (body.length && !this.writeFragments(body)) { | ||
| 221 | + return | ||
| 222 | + } | ||
| 216 | 223 | ||
| 217 | 224 | // If the frame is not fragmented, a message has been received. | |
| 218 | 225 | // If the frame is fragmented, it will terminate with a fin bit set | |
@@ -232,7 +239,9 @@ class ByteParser extends Writable { | |||
| 232 | 239 | return | |
| 233 | 240 | } | |
| 234 | 241 | ||
| 235 | - this.writeFragments(data) | ||
| 242 | + if (data.length && !this.writeFragments(data)) { | ||
| 243 | + return | ||
| 244 | + } | ||
| 236 | 245 | ||
| 237 | 246 | if (!this.#info.fin) { | |
| 238 | 247 | this.#state = parserStates.INFO | |
@@ -305,8 +314,17 @@ class ByteParser extends Writable { | |||
| 305 | 314 | } | |
| 306 | 315 | ||
| 307 | 316 | writeFragments (fragment) { | |
| 317 | + if ( | ||
| 318 | + this.#maxFragments > 0 && | ||
| 319 | + this.#fragments.length === this.#maxFragments | ||
| 320 | + ) { | ||
| 321 | + failWebsocketConnection(this.#handler, 1008, 'Too many message fragments') | ||
| 322 | + return false | ||
| 323 | + } | ||
| 324 | + | ||
| 308 | 325 | this.#fragmentsBytes += fragment.length | |
| 309 | 326 | this.#fragments.push(fragment) | |
| 327 | + return true | ||
| 310 | 328 | } | |
| 311 | 329 | ||
| 312 | 330 | consumeFragments () { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -468,7 +468,11 @@ class WebSocket extends EventTarget { | |||
| 468 | 468 | // once this happens, the connection is open | |
| 469 | 469 | this.#handler.socket = response.socket | |
| 470 | 470 | ||
| 471 | - const parser = new ByteParser(this.#handler, parsedExtensions) | ||
| 471 | + const maxFragments = this.#handler.controller.dispatcher?.webSocketOptions?.maxFragments | ||
| 472 | + | ||
| 473 | + const parser = new ByteParser(this.#handler, parsedExtensions, { | ||
| 474 | + maxFragments | ||
| 475 | + }) | ||
| 472 | 476 | parser.on('drain', () => this.#handler.onParserDrain()) | |
| 473 | 477 | parser.on('error', (err) => this.#handler.onParserError(err)) | |
| 474 | 478 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | const { test, after } = require('node:test') | |
| 4 | 4 | const { WebSocketServer } = require('ws') | |
| 5 | - const { WebSocket } = require('../..') | ||
| 5 | + const { Agent, WebSocket } = require('../..') | ||
| 6 | 6 | const diagnosticsChannel = require('node:diagnostics_channel') | |
| 7 | 7 | ||
| 8 | 8 | test('Fragmented frame with a ping frame in the middle of it', (t) => { | |
@@ -39,3 +39,90 @@ test('Fragmented frame with a ping frame in the middle of it', (t) => { | |||
| 39 | 39 | }) | |
| 40 | 40 | }) | |
| 41 | 41 | }) | |
| 42 | + | ||
| 43 | + test('Too many fragments (uncompressed)', (t, done) => { | ||
| 44 | + t.plan(4) | ||
| 45 | + | ||
| 46 | + const agent = new Agent({ | ||
| 47 | + webSocket: { | ||
| 48 | + maxFragments: 3 | ||
| 49 | + } | ||
| 50 | + }) | ||
| 51 | + | ||
| 52 | + const server = new WebSocketServer({ port: 0 }, () => { | ||
| 53 | + const { port } = server.address() | ||
| 54 | + const client = new WebSocket(`ws://127.0.0.1:${port}`, { | ||
| 55 | + dispatcher: agent | ||
| 56 | + }) | ||
| 57 | + | ||
| 58 | + client.addEventListener('error', (event) => { | ||
| 59 | + t.assert.ok(true) | ||
| 60 | + }) | ||
| 61 | + | ||
| 62 | + client.addEventListener('close', (event) => { | ||
| 63 | + t.assert.deepStrictEqual(event.code, 1006) | ||
| 64 | + }) | ||
| 65 | + }) | ||
| 66 | + | ||
| 67 | + server.on('connection', (ws) => { | ||
| 68 | + ws.on('close', (code, reason) => { | ||
| 69 | + t.assert.deepStrictEqual(code, 1008) | ||
| 70 | + t.assert.deepStrictEqual(reason.toString(), 'Too many message fragments') | ||
| 71 | + agent.close() | ||
| 72 | + server.close(done) | ||
| 73 | + }) | ||
| 74 | + | ||
| 75 | + const fragment = Buffer.from('a') | ||
| 76 | + const options = { fin: false } | ||
| 77 | + | ||
| 78 | + ws.send(fragment, options) | ||
| 79 | + ws.send(fragment, options) | ||
| 80 | + ws.send(fragment, options) | ||
| 81 | + ws.send(fragment, options) | ||
| 82 | + }) | ||
| 83 | + }) | ||
| 84 | + | ||
| 85 | + test('Too many fragments (compressed)', (t, done) => { | ||
| 86 | + t.plan(4) | ||
| 87 | + | ||
| 88 | + const agent = new Agent({ | ||
| 89 | + webSocket: { | ||
| 90 | + maxFragments: 3 | ||
| 91 | + } | ||
| 92 | + }) | ||
| 93 | + | ||
| 94 | + const server = new WebSocketServer({ | ||
| 95 | + perMessageDeflate: { threshold: 0 }, | ||
| 96 | + port: 0 | ||
| 97 | + }, () => { | ||
| 98 | + const { port } = server.address() | ||
| 99 | + const client = new WebSocket(`ws://127.0.0.1:${port}`, { | ||
| 100 | + dispatcher: agent | ||
| 101 | + }) | ||
| 102 | + | ||
| 103 | + client.addEventListener('error', (event) => { | ||
| 104 | + t.assert.ok(true) | ||
| 105 | + }) | ||
| 106 | + | ||
| 107 | + client.addEventListener('close', (event) => { | ||
| 108 | + t.assert.deepStrictEqual(event.code, 1006) | ||
| 109 | + }) | ||
| 110 | + }) | ||
| 111 | + | ||
| 112 | + server.on('connection', (ws) => { | ||
| 113 | + ws.on('close', (code, reason) => { | ||
| 114 | + t.assert.deepStrictEqual(code, 1008) | ||
| 115 | + t.assert.deepStrictEqual(reason.toString(), 'Too many message fragments') | ||
| 116 | + agent.close() | ||
| 117 | + server.close(done) | ||
| 118 | + }) | ||
| 119 | + | ||
| 120 | + const fragment = Buffer.from('a') | ||
| 121 | + const options = { fin: false } | ||
| 122 | + | ||
| 123 | + ws.send(fragment, options) | ||
| 124 | + ws.send(fragment, options) | ||
| 125 | + ws.send(fragment, options) | ||
| 126 | + ws.send(fragment, options) | ||
| 127 | + }) | ||
| 128 | + }) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -107,6 +107,8 @@ export declare namespace Client { | |||
| 107 | 107 | * @default 60000 | |
| 108 | 108 | */ | |
| 109 | 109 | pingInterval?: number; | |
| 110 | + /** WebSocket-specific configuration options. */ | ||
| 111 | + webSocket?: WebSocketOptions; | ||
| 110 | 112 | } | |
| 111 | 113 | export interface SocketInfo { | |
| 112 | 114 | localAddress?: string | |
@@ -118,6 +120,13 @@ export declare namespace Client { | |||
| 118 | 120 | bytesWritten?: number | |
| 119 | 121 | bytesRead?: number | |
| 120 | 122 | } | |
| 123 | + export interface WebSocketOptions { | ||
| 124 | + /** | ||
| 125 | + * Maximum number of fragments in a message. Set to 0 to disable the limit. | ||
| 126 | + * @default 131072 | ||
| 127 | + */ | ||
| 128 | + maxFragments?: number; | ||
| 129 | + } | ||
| 121 | 130 | } | |
| 122 | 131 | ||
| 123 | 132 | export default Client | |
| Back | FazBrowse Home | New Git URL |
0 commit comments