Skip to content

Commit 4987a00

Browse files
nodejs-github-botmarco-ippolito
authored andcommitted
deps: update undici to 6.18.0
PR-URL: #53073 Reviewed-By: Marco Ippolito <[email protected]> Reviewed-By: Matthew Aitken <[email protected]> Reviewed-By: Trivikram Kamat <[email protected]> Reviewed-By: Rafael Gonzaga <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 8653d92 commit 4987a00

12 files changed

+580
-135
lines changed

deps/undici/src/lib/web/fetch/data-url.js

+1
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,7 @@ module.exports = {
737737
collectAnHTTPQuotedString,
738738
serializeAMimeType,
739739
removeChars,
740+
removeHTTPWhitespace,
740741
minimizeSupportedMimeType,
741742
HTTP_TOKEN_CODEPOINTS,
742743
isomorphicDecode

deps/undici/src/lib/web/websocket/connection.js

+18-9
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const {
88
kReceivedClose,
99
kResponse
1010
} = require('./symbols')
11-
const { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished } = require('./util')
11+
const { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished, parseExtensions } = require('./util')
1212
const { channels } = require('../../core/diagnostics')
1313
const { CloseEvent } = require('./events')
1414
const { makeRequest } = require('../fetch/request')
@@ -31,7 +31,7 @@ try {
3131
* @param {URL} url
3232
* @param {string|string[]} protocols
3333
* @param {import('./websocket').WebSocket} ws
34-
* @param {(response: any) => void} onEstablish
34+
* @param {(response: any, extensions: string[] | undefined) => void} onEstablish
3535
* @param {Partial<import('../../types/websocket').WebSocketInit>} options
3636
*/
3737
function establishWebSocketConnection (url, protocols, client, ws, onEstablish, options) {
@@ -91,12 +91,11 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
9191
// 9. Let permessageDeflate be a user-agent defined
9292
// "permessage-deflate" extension header value.
9393
// https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673
94-
// TODO: enable once permessage-deflate is supported
95-
const permessageDeflate = '' // 'permessage-deflate; 15'
94+
const permessageDeflate = 'permessage-deflate; client_max_window_bits'
9695

9796
// 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to
9897
// request’s header list.
99-
// request.headersList.append('sec-websocket-extensions', permessageDeflate)
98+
request.headersList.append('sec-websocket-extensions', permessageDeflate)
10099

101100
// 11. Fetch request with useParallelQueue set to true, and
102101
// processResponse given response being these steps:
@@ -167,10 +166,15 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
167166
// header field to determine which extensions are requested is
168167
// discussed in Section 9.1.)
169168
const secExtension = response.headersList.get('Sec-WebSocket-Extensions')
169+
let extensions
170170

171-
if (secExtension !== null && secExtension !== permessageDeflate) {
172-
failWebsocketConnection(ws, 'Received different permessage-deflate than the one set.')
173-
return
171+
if (secExtension !== null) {
172+
extensions = parseExtensions(secExtension)
173+
174+
if (!extensions.has('permessage-deflate')) {
175+
failWebsocketConnection(ws, 'Sec-WebSocket-Extensions header does not match.')
176+
return
177+
}
174178
}
175179

176180
// 6. If the response includes a |Sec-WebSocket-Protocol| header field
@@ -206,7 +210,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
206210
})
207211
}
208212

209-
onEstablish(response)
213+
onEstablish(response, extensions)
210214
}
211215
})
212216

@@ -290,6 +294,11 @@ function onSocketData (chunk) {
290294
*/
291295
function onSocketClose () {
292296
const { ws } = this
297+
const { [kResponse]: response } = ws
298+
299+
response.socket.off('data', onSocketData)
300+
response.socket.off('close', onSocketClose)
301+
response.socket.off('error', onSocketError)
293302

294303
// If the TCP connection was closed after the
295304
// WebSocket closing handshake was completed, the WebSocket connection

deps/undici/src/lib/web/websocket/constants.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ const parserStates = {
4646

4747
const emptyBuffer = Buffer.allocUnsafe(0)
4848

49+
const sendHints = {
50+
string: 1,
51+
typedArray: 2,
52+
arrayBuffer: 3,
53+
blob: 4
54+
}
55+
4956
module.exports = {
5057
uid,
5158
sentCloseFrameState,
@@ -54,5 +61,6 @@ module.exports = {
5461
opcodes,
5562
maxUnsigned16Bit,
5663
parserStates,
57-
emptyBuffer
64+
emptyBuffer,
65+
sendHints
5866
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use strict'
2+
3+
const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib')
4+
const { isValidClientWindowBits } = require('./util')
5+
6+
const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
7+
const kBuffer = Symbol('kBuffer')
8+
const kLength = Symbol('kLength')
9+
10+
class PerMessageDeflate {
11+
/** @type {import('node:zlib').InflateRaw} */
12+
#inflate
13+
14+
#options = {}
15+
16+
constructor (extensions) {
17+
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
18+
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
19+
}
20+
21+
decompress (chunk, fin, callback) {
22+
// An endpoint uses the following algorithm to decompress a message.
23+
// 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
24+
// payload of the message.
25+
// 2. Decompress the resulting data using DEFLATE.
26+
27+
if (!this.#inflate) {
28+
let windowBits = Z_DEFAULT_WINDOWBITS
29+
30+
if (this.#options.serverMaxWindowBits) { // empty values default to Z_DEFAULT_WINDOWBITS
31+
if (!isValidClientWindowBits(this.#options.serverMaxWindowBits)) {
32+
callback(new Error('Invalid server_max_window_bits'))
33+
return
34+
}
35+
36+
windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
37+
}
38+
39+
this.#inflate = createInflateRaw({ windowBits })
40+
this.#inflate[kBuffer] = []
41+
this.#inflate[kLength] = 0
42+
43+
this.#inflate.on('data', (data) => {
44+
this.#inflate[kBuffer].push(data)
45+
this.#inflate[kLength] += data.length
46+
})
47+
48+
this.#inflate.on('error', (err) => {
49+
this.#inflate = null
50+
callback(err)
51+
})
52+
}
53+
54+
this.#inflate.write(chunk)
55+
if (fin) {
56+
this.#inflate.write(tail)
57+
}
58+
59+
this.#inflate.flush(() => {
60+
const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
61+
62+
this.#inflate[kBuffer].length = 0
63+
this.#inflate[kLength] = 0
64+
65+
callback(null, full)
66+
})
67+
}
68+
}
69+
70+
module.exports = { PerMessageDeflate }

deps/undici/src/lib/web/websocket/receiver.js

+63-16
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const {
1717
} = require('./util')
1818
const { WebsocketFrameSend } = require('./frame')
1919
const { closeWebSocketConnection } = require('./connection')
20+
const { PerMessageDeflate } = require('./permessage-deflate')
2021

2122
// This code was influenced by ws released under the MIT license.
2223
// Copyright (c) 2011 Einar Otto Stangvik <[email protected]>
@@ -33,10 +34,18 @@ class ByteParser extends Writable {
3334
#info = {}
3435
#fragments = []
3536

36-
constructor (ws) {
37+
/** @type {Map<string, PerMessageDeflate>} */
38+
#extensions
39+
40+
constructor (ws, extensions) {
3741
super()
3842

3943
this.ws = ws
44+
this.#extensions = extensions == null ? new Map() : extensions
45+
46+
if (this.#extensions.has('permessage-deflate')) {
47+
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
48+
}
4049
}
4150

4251
/**
@@ -91,7 +100,16 @@ class ByteParser extends Writable {
91100
// the negotiated extensions defines the meaning of such a nonzero
92101
// value, the receiving endpoint MUST _Fail the WebSocket
93102
// Connection_.
94-
if (rsv1 !== 0 || rsv2 !== 0 || rsv3 !== 0) {
103+
// This document allocates the RSV1 bit of the WebSocket header for
104+
// PMCEs and calls the bit the "Per-Message Compressed" bit. On a
105+
// WebSocket connection where a PMCE is in use, this bit indicates
106+
// whether a message is compressed or not.
107+
if (rsv1 !== 0 && !this.#extensions.has('permessage-deflate')) {
108+
failWebsocketConnection(this.ws, 'Expected RSV1 to be clear.')
109+
return
110+
}
111+
112+
if (rsv2 !== 0 || rsv3 !== 0) {
95113
failWebsocketConnection(this.ws, 'RSV1, RSV2, RSV3 must be clear')
96114
return
97115
}
@@ -122,7 +140,7 @@ class ByteParser extends Writable {
122140
return
123141
}
124142

125-
if (isContinuationFrame(opcode) && this.#fragments.length === 0) {
143+
if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) {
126144
failWebsocketConnection(this.ws, 'Unexpected continuation frame')
127145
return
128146
}
@@ -138,6 +156,7 @@ class ByteParser extends Writable {
138156

139157
if (isTextBinaryFrame(opcode)) {
140158
this.#info.binaryType = opcode
159+
this.#info.compressed = rsv1 !== 0
141160
}
142161

143162
this.#info.opcode = opcode
@@ -185,21 +204,50 @@ class ByteParser extends Writable {
185204

186205
if (isControlFrame(this.#info.opcode)) {
187206
this.#loop = this.parseControlFrame(body)
207+
this.#state = parserStates.INFO
188208
} else {
189-
this.#fragments.push(body)
190-
191-
// If the frame is not fragmented, a message has been received.
192-
// If the frame is fragmented, it will terminate with a fin bit set
193-
// and an opcode of 0 (continuation), therefore we handle that when
194-
// parsing continuation frames, not here.
195-
if (!this.#info.fragmented && this.#info.fin) {
196-
const fullMessage = Buffer.concat(this.#fragments)
197-
websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
198-
this.#fragments.length = 0
209+
if (!this.#info.compressed) {
210+
this.#fragments.push(body)
211+
212+
// If the frame is not fragmented, a message has been received.
213+
// If the frame is fragmented, it will terminate with a fin bit set
214+
// and an opcode of 0 (continuation), therefore we handle that when
215+
// parsing continuation frames, not here.
216+
if (!this.#info.fragmented && this.#info.fin) {
217+
const fullMessage = Buffer.concat(this.#fragments)
218+
websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
219+
this.#fragments.length = 0
220+
}
221+
222+
this.#state = parserStates.INFO
223+
} else {
224+
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
225+
if (error) {
226+
closeWebSocketConnection(this.ws, 1007, error.message, error.message.length)
227+
return
228+
}
229+
230+
this.#fragments.push(data)
231+
232+
if (!this.#info.fin) {
233+
this.#state = parserStates.INFO
234+
this.#loop = true
235+
this.run(callback)
236+
return
237+
}
238+
239+
websocketMessageReceived(this.ws, this.#info.binaryType, Buffer.concat(this.#fragments))
240+
241+
this.#loop = true
242+
this.#state = parserStates.INFO
243+
this.run(callback)
244+
this.#fragments.length = 0
245+
})
246+
247+
this.#loop = false
248+
break
199249
}
200250
}
201-
202-
this.#state = parserStates.INFO
203251
}
204252
}
205253
}
@@ -333,7 +381,6 @@ class ByteParser extends Writable {
333381
this.ws[kReadyState] = states.CLOSING
334382
this.ws[kReceivedClose] = true
335383

336-
this.end()
337384
return false
338385
} else if (opcode === opcodes.PING) {
339386
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
'use strict'
2+
3+
const { WebsocketFrameSend } = require('./frame')
4+
const { opcodes, sendHints } = require('./constants')
5+
6+
/** @type {Uint8Array} */
7+
const FastBuffer = Buffer[Symbol.species]
8+
9+
class SendQueue {
10+
#queued = new Set()
11+
#size = 0
12+
13+
/** @type {import('net').Socket} */
14+
#socket
15+
16+
constructor (socket) {
17+
this.#socket = socket
18+
}
19+
20+
add (item, cb, hint) {
21+
if (hint !== sendHints.blob) {
22+
const data = clone(item, hint)
23+
24+
if (this.#size === 0) {
25+
this.#dispatch(data, cb, hint)
26+
} else {
27+
this.#queued.add([data, cb, true, hint])
28+
this.#size++
29+
30+
this.#run()
31+
}
32+
33+
return
34+
}
35+
36+
const promise = item.arrayBuffer()
37+
const queue = [null, cb, false, hint]
38+
promise.then((ab) => {
39+
queue[0] = clone(ab, hint)
40+
queue[2] = true
41+
42+
this.#run()
43+
})
44+
45+
this.#queued.add(queue)
46+
this.#size++
47+
}
48+
49+
#run () {
50+
for (const queued of this.#queued) {
51+
const [data, cb, done, hint] = queued
52+
53+
if (!done) return
54+
55+
this.#queued.delete(queued)
56+
this.#size--
57+
58+
this.#dispatch(data, cb, hint)
59+
}
60+
}
61+
62+
#dispatch (data, cb, hint) {
63+
const frame = new WebsocketFrameSend()
64+
const opcode = hint === sendHints.string ? opcodes.TEXT : opcodes.BINARY
65+
66+
frame.frameData = data
67+
const buffer = frame.createFrame(opcode)
68+
69+
this.#socket.write(buffer, cb)
70+
}
71+
}
72+
73+
function clone (data, hint) {
74+
switch (hint) {
75+
case sendHints.string:
76+
return Buffer.from(data)
77+
case sendHints.arrayBuffer:
78+
case sendHints.blob:
79+
return new FastBuffer(data)
80+
case sendHints.typedArray:
81+
return Buffer.copyBytesFrom(data)
82+
}
83+
}
84+
85+
module.exports = { SendQueue }

0 commit comments

Comments
 (0)