| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent e114e77 commit c5ed787
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -25,7 +25,7 @@ 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 | 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. | |
| 30 | 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. This option has no effect once HTTP/2 is negotiated — see `maxConcurrentStreams` for the h2 dispatch ceiling. | |
| 31 | 31 | * **connect** `ConnectOptions | Function | null` (optional) - Default: `null` - Configures how undici establishes TCP/TLS connections. Accepts two forms: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -39,7 +39,6 @@ class ByteParser extends Writable { | |||
| 39 | 39 | /** @type {import('./websocket').Handler} */ | |
| 40 | 40 | #handler | |
| 41 | 41 | ||
| 42 | - | ||
| 43 | 42 | /** @type {number} */ | |
| 44 | 43 | #maxFragments | |
| 45 | 44 | ||
@@ -247,7 +246,7 @@ class ByteParser extends Writable { | |||
| 247 | 246 | this.#state = parserStates.INFO | |
| 248 | 247 | } else { | |
| 249 | 248 | if (!this.#info.compressed) { | |
| 250 | - if (body.length && !this.writeFragments(body)) { | ||
| 249 | + if (!this.writeFragments(body)) { | ||
| 251 | 250 | return | |
| 252 | 251 | } | |
| 253 | 252 | ||
@@ -271,7 +270,7 @@ class ByteParser extends Writable { | |||
| 271 | 270 | return | |
| 272 | 271 | } | |
| 273 | 272 | ||
| 274 | - if (data.length && !this.writeFragments(data)) { | ||
| 273 | + if (!this.writeFragments(data)) { | ||
| 275 | 274 | return | |
| 276 | 275 | } | |
| 277 | 276 | ||
| 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 | |
|---|---|---|---|
@@ -43,6 +43,15 @@ test('Fragmented frame with a ping frame in the middle of it', (t) => { | |||
| 43 | 43 | test('Too many fragments (uncompressed)', (t, done) => { | |
| 44 | 44 | t.plan(4) | |
| 45 | 45 | ||
| 46 | + function maybeDone () { | ||
| 47 | + if (++maybeDone.callCount === 2) { | ||
| 48 | + agent.close() | ||
| 49 | + server.close(done) | ||
| 50 | + } | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + maybeDone.callCount = 0 | ||
| 54 | + | ||
| 46 | 55 | const agent = new Agent({ | |
| 47 | 56 | webSocket: { | |
| 48 | 57 | maxFragments: 3 | |
@@ -61,15 +70,15 @@ test('Too many fragments (uncompressed)', (t, done) => { | |||
| 61 | 70 | ||
| 62 | 71 | client.addEventListener('close', (event) => { | |
| 63 | 72 | t.assert.deepStrictEqual(event.code, 1006) | |
| 73 | + maybeDone() | ||
| 64 | 74 | }) | |
| 65 | 75 | }) | |
| 66 | 76 | ||
| 67 | 77 | server.on('connection', (ws) => { | |
| 68 | 78 | ws.on('close', (code, reason) => { | |
| 69 | 79 | t.assert.deepStrictEqual(code, 1008) | |
| 70 | 80 | t.assert.deepStrictEqual(reason.toString(), 'Too many message fragments') | |
| 71 | - agent.close() | ||
| 72 | - server.close(done) | ||
| 81 | + maybeDone() | ||
| 73 | 82 | }) | |
| 74 | 83 | ||
| 75 | 84 | const fragment = Buffer.from('a') | |
@@ -85,6 +94,15 @@ test('Too many fragments (uncompressed)', (t, done) => { | |||
| 85 | 94 | test('Too many fragments (compressed)', (t, done) => { | |
| 86 | 95 | t.plan(4) | |
| 87 | 96 | ||
| 97 | + function maybeDone () { | ||
| 98 | + if (++maybeDone.callCount === 2) { | ||
| 99 | + agent.close() | ||
| 100 | + server.close(done) | ||
| 101 | + } | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + maybeDone.callCount = 0 | ||
| 105 | + | ||
| 88 | 106 | const agent = new Agent({ | |
| 89 | 107 | webSocket: { | |
| 90 | 108 | maxFragments: 3 | |
@@ -106,15 +124,15 @@ test('Too many fragments (compressed)', (t, done) => { | |||
| 106 | 124 | ||
| 107 | 125 | client.addEventListener('close', (event) => { | |
| 108 | 126 | t.assert.deepStrictEqual(event.code, 1006) | |
| 127 | + maybeDone() | ||
| 109 | 128 | }) | |
| 110 | 129 | }) | |
| 111 | 130 | ||
| 112 | 131 | server.on('connection', (ws) => { | |
| 113 | 132 | ws.on('close', (code, reason) => { | |
| 114 | 133 | t.assert.deepStrictEqual(code, 1008) | |
| 115 | 134 | t.assert.deepStrictEqual(reason.toString(), 'Too many message fragments') | |
| 116 | - agent.close() | ||
| 117 | - server.close(done) | ||
| 135 | + maybeDone() | ||
| 118 | 136 | }) | |
| 119 | 137 | ||
| 120 | 138 | const fragment = Buffer.from('a') | |
@@ -126,3 +144,87 @@ test('Too many fragments (compressed)', (t, done) => { | |||
| 126 | 144 | ws.send(fragment, options) | |
| 127 | 145 | }) | |
| 128 | 146 | }) | |
| 147 | + | ||
| 148 | + test('Empty first fragment followed by non-empty continuation delivers the message', (t) => { | ||
| 149 | + // RFC 6455 §5.4 allows zero-byte fragments. A conforming server that opens | ||
| 150 | + // a fragmented message with an empty frame must be honored: the parser must | ||
| 151 | + // recognize the in-progress fragmented message when the continuation arrives. | ||
| 152 | + const server = new WebSocketServer({ port: 0 }) | ||
| 153 | + | ||
| 154 | + server.on('connection', (ws) => { | ||
| 155 | + ws.send('', { fin: false }) // Text frame fin=0, len=0 | ||
| 156 | + ws.send('hello', { fin: true }) // Continuation fin=1, "hello" | ||
| 157 | + }) | ||
| 158 | + | ||
| 159 | + after(() => { | ||
| 160 | + for (const client of server.clients) { | ||
| 161 | + client.close() | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + server.close() | ||
| 165 | + }) | ||
| 166 | + | ||
| 167 | + const ws = new WebSocket(`ws://localhost:${server.address().port}`) | ||
| 168 | + | ||
| 169 | + return new Promise((resolve) => { | ||
| 170 | + ws.addEventListener('message', ({ data }) => { | ||
| 171 | + t.assert.strictEqual(data, 'hello') | ||
| 172 | + | ||
| 173 | + ws.close() | ||
| 174 | + resolve() | ||
| 175 | + }) | ||
| 176 | + }) | ||
| 177 | + }) | ||
| 178 | + | ||
| 179 | + test('Too many empty fragments triggers close 1008', (t, done) => { | ||
| 180 | + // Empty fragments must still count toward maxFragments; otherwise a | ||
| 181 | + // peer can flood zero-byte continuation frames forever. | ||
| 182 | + t.plan(4) | ||
| 183 | + | ||
| 184 | + function maybeDone () { | ||
| 185 | + if (++maybeDone.callCount === 2) { | ||
| 186 | + agent.close() | ||
| 187 | + server.close(done) | ||
| 188 | + } | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + maybeDone.callCount = 0 | ||
| 192 | + | ||
| 193 | + const agent = new Agent({ | ||
| 194 | + webSocket: { | ||
| 195 | + maxFragments: 3 | ||
| 196 | + } | ||
| 197 | + }) | ||
| 198 | + | ||
| 199 | + const server = new WebSocketServer({ port: 0 }, () => { | ||
| 200 | + const { port } = server.address() | ||
| 201 | + const client = new WebSocket(`ws://127.0.0.1:${port}`, { | ||
| 202 | + dispatcher: agent | ||
| 203 | + }) | ||
| 204 | + | ||
| 205 | + client.addEventListener('error', (event) => { | ||
| 206 | + t.assert.ok(true) | ||
| 207 | + }) | ||
| 208 | + | ||
| 209 | + client.addEventListener('close', (event) => { | ||
| 210 | + t.assert.deepStrictEqual(event.code, 1006) | ||
| 211 | + maybeDone() | ||
| 212 | + }) | ||
| 213 | + }) | ||
| 214 | + | ||
| 215 | + server.on('connection', (ws) => { | ||
| 216 | + ws.on('close', (code, reason) => { | ||
| 217 | + t.assert.deepStrictEqual(code, 1008) | ||
| 218 | + t.assert.deepStrictEqual(reason.toString(), 'Too many message fragments') | ||
| 219 | + maybeDone() | ||
| 220 | + }) | ||
| 221 | + | ||
| 222 | + const fragment = '' | ||
| 223 | + const options = { fin: false } | ||
| 224 | + | ||
| 225 | + ws.send(fragment, options) // Text frame fin=0, len=0 | ||
| 226 | + ws.send(fragment, options) // Continuation fin=0, len=0 | ||
| 227 | + ws.send(fragment, options) // Continuation fin=0, len=0 | ||
| 228 | + ws.send(fragment, options) // Continuation fin=0, len=0 | ||
| 229 | + }) | ||
| 230 | + }) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,58 @@ | |||
| 1 | + 'use strict' | ||
| 2 | + | ||
| 3 | + const { test } = require('node:test') | ||
| 4 | + const { WebSocketServer } = require('ws') | ||
| 5 | + const { | ||
| 6 | + Agent, | ||
| 7 | + WebSocketStream, | ||
| 8 | + getGlobalDispatcher, | ||
| 9 | + setGlobalDispatcher | ||
| 10 | + } = require('../../..') | ||
| 11 | + | ||
| 12 | + test('Too many fragments via WebSocketStream triggers close 1008', async (t) => { | ||
| 13 | + // WebSocketStream reads its dispatcher from the global one (its options | ||
| 14 | + // dictionary doesn't accept a dispatcher), so we swap it for the duration | ||
| 15 | + // of this test. The fragment-count limit must apply to WebSocketStream | ||
| 16 | + // the same way it applies to the WebSocket API. | ||
| 17 | + const previous = getGlobalDispatcher() | ||
| 18 | + const agent = new Agent({ | ||
| 19 | + webSocket: { | ||
| 20 | + maxFragments: 3 | ||
| 21 | + } | ||
| 22 | + }) | ||
| 23 | + setGlobalDispatcher(agent) | ||
| 24 | + | ||
| 25 | + const server = new WebSocketServer({ port: 0 }) | ||
| 26 | + | ||
| 27 | + t.after(async () => { | ||
| 28 | + setGlobalDispatcher(previous) | ||
| 29 | + await agent.close() | ||
| 30 | + server.close() | ||
| 31 | + }) | ||
| 32 | + | ||
| 33 | + const serverClose = new Promise((resolve) => { | ||
| 34 | + server.on('connection', (ws) => { | ||
| 35 | + ws.on('close', (code, reason) => { | ||
| 36 | + resolve({ code, reason: reason.toString() }) | ||
| 37 | + }) | ||
| 38 | + | ||
| 39 | + const fragment = Buffer.from('a') | ||
| 40 | + const options = { fin: false } | ||
| 41 | + | ||
| 42 | + ws.send(fragment, options) | ||
| 43 | + ws.send(fragment, options) | ||
| 44 | + ws.send(fragment, options) | ||
| 45 | + ws.send(fragment, options) | ||
| 46 | + }) | ||
| 47 | + }) | ||
| 48 | + | ||
| 49 | + const wss = new WebSocketStream(`ws://localhost:${server.address().port}`) | ||
| 50 | + | ||
| 51 | + // The connection will be failed by the parser; both `opened` and `closed` | ||
| 52 | + // settle. We only care that the server observed the policy-violation close. | ||
| 53 | + await Promise.allSettled([wss.opened, wss.closed]) | ||
| 54 | + | ||
| 55 | + const observed = await serverClose | ||
| 56 | + t.assert.deepStrictEqual(observed.code, 1008) | ||
| 57 | + t.assert.deepStrictEqual(observed.reason, 'Too many message fragments') | ||
| 58 | + }) | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments