| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 494fa54 commit b941992
12 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -737,6 +737,7 @@ module.exports = { | |||
| 737 | 737 | collectAnHTTPQuotedString, | |
| 738 | 738 | serializeAMimeType, | |
| 739 | 739 | removeChars, | |
| 740 | + removeHTTPWhitespace, | ||
| 740 | 741 | minimizeSupportedMimeType, | |
| 741 | 742 | HTTP_TOKEN_CODEPOINTS, | |
| 742 | 743 | isomorphicDecode | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -8,7 +8,7 @@ const { | |||
| 8 | 8 | kReceivedClose, | |
| 9 | 9 | kResponse | |
| 10 | 10 | } = require('./symbols') | |
| 11 | - const { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished } = require('./util') | ||
| 11 | + const { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished, parseExtensions } = require('./util') | ||
| 12 | 12 | const { channels } = require('../../core/diagnostics') | |
| 13 | 13 | const { CloseEvent } = require('./events') | |
| 14 | 14 | const { makeRequest } = require('../fetch/request') | |
@@ -31,7 +31,7 @@ try { | |||
| 31 | 31 | * @param {URL} url | |
| 32 | 32 | * @param {string|string[]} protocols | |
| 33 | 33 | * @param {import('./websocket').WebSocket} ws | |
| 34 | - * @param {(response: any) => void} onEstablish | ||
| 34 | + * @param {(response: any, extensions: string[] | undefined) => void} onEstablish | ||
| 35 | 35 | * @param {Partial<import('../../types/websocket').WebSocketInit>} options | |
| 36 | 36 | */ | |
| 37 | 37 | function establishWebSocketConnection (url, protocols, client, ws, onEstablish, options) { | |
@@ -91,12 +91,11 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish, | |||
| 91 | 91 | // 9. Let permessageDeflate be a user-agent defined | |
| 92 | 92 | // "permessage-deflate" extension header value. | |
| 93 | 93 | // 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' | ||
| 96 | 95 | ||
| 97 | 96 | // 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to | |
| 98 | 97 | // request’s header list. | |
| 99 | - // request.headersList.append('sec-websocket-extensions', permessageDeflate) | ||
| 98 | + request.headersList.append('sec-websocket-extensions', permessageDeflate) | ||
| 100 | 99 | ||
| 101 | 100 | // 11. Fetch request with useParallelQueue set to true, and | |
| 102 | 101 | // processResponse given response being these steps: | |
@@ -167,10 +166,15 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish, | |||
| 167 | 166 | // header field to determine which extensions are requested is | |
| 168 | 167 | // discussed in Section 9.1.) | |
| 169 | 168 | const secExtension = response.headersList.get('Sec-WebSocket-Extensions') | |
| 169 | + let extensions | ||
| 170 | 170 | ||
| 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 | + } | ||
| 174 | 178 | } | |
| 175 | 179 | ||
| 176 | 180 | // 6. If the response includes a |Sec-WebSocket-Protocol| header field | |
@@ -206,7 +210,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish, | |||
| 206 | 210 | }) | |
| 207 | 211 | } | |
| 208 | 212 | ||
| 209 | - onEstablish(response) | ||
| 213 | + onEstablish(response, extensions) | ||
| 210 | 214 | } | |
| 211 | 215 | }) | |
| 212 | 216 | ||
@@ -290,6 +294,11 @@ function onSocketData (chunk) { | |||
| 290 | 294 | */ | |
| 291 | 295 | function onSocketClose () { | |
| 292 | 296 | 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) | ||
| 293 | 302 | ||
| 294 | 303 | // If the TCP connection was closed after the | |
| 295 | 304 | // WebSocket closing handshake was completed, the WebSocket connection | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -46,6 +46,13 @@ const parserStates = { | |||
| 46 | 46 | ||
| 47 | 47 | const emptyBuffer = Buffer.allocUnsafe(0) | |
| 48 | 48 | ||
| 49 | + const sendHints = { | ||
| 50 | + string: 1, | ||
| 51 | + typedArray: 2, | ||
| 52 | + arrayBuffer: 3, | ||
| 53 | + blob: 4 | ||
| 54 | + } | ||
| 55 | + | ||
| 49 | 56 | module.exports = { | |
| 50 | 57 | uid, | |
| 51 | 58 | sentCloseFrameState, | |
@@ -54,5 +61,6 @@ module.exports = { | |||
| 54 | 61 | opcodes, | |
| 55 | 62 | maxUnsigned16Bit, | |
| 56 | 63 | parserStates, | |
| 57 | - emptyBuffer | ||
| 64 | + emptyBuffer, | ||
| 65 | + sendHints | ||
| 58 | 66 | } | |
| Original file line number | Diff line number | Diff 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 } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -17,6 +17,7 @@ const { | |||
| 17 | 17 | } = require('./util') | |
| 18 | 18 | const { WebsocketFrameSend } = require('./frame') | |
| 19 | 19 | const { closeWebSocketConnection } = require('./connection') | |
| 20 | + const { PerMessageDeflate } = require('./permessage-deflate') | ||
| 20 | 21 | ||
| 21 | 22 | // This code was influenced by ws released under the MIT license. | |
| 22 | 23 | // Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com> | |
@@ -33,10 +34,18 @@ class ByteParser extends Writable { | |||
| 33 | 34 | #info = {} | |
| 34 | 35 | #fragments = [] | |
| 35 | 36 | ||
| 36 | - constructor (ws) { | ||
| 37 | + /** @type {Map<string, PerMessageDeflate>} */ | ||
| 38 | + #extensions | ||
| 39 | + | ||
| 40 | + constructor (ws, extensions) { | ||
| 37 | 41 | super() | |
| 38 | 42 | ||
| 39 | 43 | 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 | + } | ||
| 40 | 49 | } | |
| 41 | 50 | ||
| 42 | 51 | /** | |
@@ -91,7 +100,16 @@ class ByteParser extends Writable { | |||
| 91 | 100 | // the negotiated extensions defines the meaning of such a nonzero | |
| 92 | 101 | // value, the receiving endpoint MUST _Fail the WebSocket | |
| 93 | 102 | // 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) { | ||
| 95 | 113 | failWebsocketConnection(this.ws, 'RSV1, RSV2, RSV3 must be clear') | |
| 96 | 114 | return | |
| 97 | 115 | } | |
@@ -122,7 +140,7 @@ class ByteParser extends Writable { | |||
| 122 | 140 | return | |
| 123 | 141 | } | |
| 124 | 142 | ||
| 125 | - if (isContinuationFrame(opcode) && this.#fragments.length === 0) { | ||
| 143 | + if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) { | ||
| 126 | 144 | failWebsocketConnection(this.ws, 'Unexpected continuation frame') | |
| 127 | 145 | return | |
| 128 | 146 | } | |
@@ -138,6 +156,7 @@ class ByteParser extends Writable { | |||
| 138 | 156 | ||
| 139 | 157 | if (isTextBinaryFrame(opcode)) { | |
| 140 | 158 | this.#info.binaryType = opcode | |
| 159 | + this.#info.compressed = rsv1 !== 0 | ||
| 141 | 160 | } | |
| 142 | 161 | ||
| 143 | 162 | this.#info.opcode = opcode | |
@@ -185,21 +204,50 @@ class ByteParser extends Writable { | |||
| 185 | 204 | ||
| 186 | 205 | if (isControlFrame(this.#info.opcode)) { | |
| 187 | 206 | this.#loop = this.parseControlFrame(body) | |
| 207 | + this.#state = parserStates.INFO | ||
| 188 | 208 | } 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 | ||
| 199 | 249 | } | |
| 200 | 250 | } | |
| 201 | - | ||
| 202 | - this.#state = parserStates.INFO | ||
| 203 | 251 | } | |
| 204 | 252 | } | |
| 205 | 253 | } | |
@@ -333,7 +381,6 @@ class ByteParser extends Writable { | |||
| 333 | 381 | this.ws[kReadyState] = states.CLOSING | |
| 334 | 382 | this.ws[kReceivedClose] = true | |
| 335 | 383 | ||
| 336 | - this.end() | ||
| 337 | 384 | return false | |
| 338 | 385 | } else if (opcode === opcodes.PING) { | |
| 339 | 386 | // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in | |
| Original file line number | Diff line number | Diff 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 } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments