codekingpro/portable-devtools
114k
1'use strict'
2
3const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = require('./symbols')
4const { states, opcodes } = require('./constants')
5const { ErrorEvent, createFastMessageEvent } = require('./events')
6const { isUtf8 } = require('node:buffer')
7const { collectASequenceOfCodePointsFast, removeHTTPWhitespace } = require('../fetch/data-url')
8
9/* globals Blob */
10
11/**
12 * @param {import('./websocket').WebSocket} ws
13 * @returns {boolean}
14 */
15function isConnecting (ws) {
16 // If the WebSocket connection is not yet established, and the connection
17 // is not yet closed, then the WebSocket connection is in the CONNECTING state.
18 return ws[kReadyState] === states.CONNECTING
19}
20
21/**
22 * @param {import('./websocket').WebSocket} ws
23 * @returns {boolean}
24 */
25function isEstablished (ws) {
26 // If the server's response is validated as provided for above, it is
27 // said that _The WebSocket Connection is Established_ and that the
28 // WebSocket Connection is in the OPEN state.
29 return ws[kReadyState] === states.OPEN
30}
31
32/**
33 * @param {import('./websocket').WebSocket} ws
34 * @returns {boolean}
35 */
36function isClosing (ws) {
37 // Upon either sending or receiving a Close control frame, it is said
38 // that _The WebSocket Closing Handshake is Started_ and that the
39 // WebSocket connection is in the CLOSING state.
40 return ws[kReadyState] === states.CLOSING
41}
42
43/**
44 * @param {import('./websocket').WebSocket} ws
45 * @returns {boolean}
46 */
47function isClosed (ws) {
48 return ws[kReadyState] === states.CLOSED
49}
50
51/**
52 * @see https://dom.spec.whatwg.org/#concept-event-fire
53 * @param {string} e
54 * @param {EventTarget} target
55 * @param {(...args: ConstructorParameters<typeof Event>) => Event} eventFactory
56 * @param {EventInit | undefined} eventInitDict
57 */
58function fireEvent (e, target, eventFactory = (type, init) => new Event(type, init), eventInitDict = {}) {
59 // 1. If eventConstructor is not given, then let eventConstructor be Event.
60
61 // 2. Let event be the result of creating an event given eventConstructor,
62 // in the relevant realm of target.
63 // 3. Initialize event’s type attribute to e.
64 const event = eventFactory(e, eventInitDict)
65
66 // 4. Initialize any other IDL attributes of event as described in the
67 // invocation of this algorithm.
68
69 // 5. Return the result of dispatching event at target, with legacy target
70 // override flag set if set.
71 target.dispatchEvent(event)
72}
73
74/**
75 * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
76 * @param {import('./websocket').WebSocket} ws
77 * @param {number} type Opcode
78 * @param {Buffer} data application data
79 */
80function websocketMessageReceived (ws, type, data) {
81 // 1. If ready state is not OPEN (1), then return.
82 if (ws[kReadyState] !== states.OPEN) {
83 return
84 }
85
86 // 2. Let dataForEvent be determined by switching on type and binary type:
87 let dataForEvent
88
89 if (type === opcodes.TEXT) {
90 // -> type indicates that the data is Text
91 // a new DOMString containing data
92 try {
93 dataForEvent = utf8Decode(data)
94 } catch {
95 failWebsocketConnection(ws, 'Received invalid UTF-8 in text frame.')
96 return
97 }
98 } else if (type === opcodes.BINARY) {
99 if (ws[kBinaryType] === 'blob') {
100 // -> type indicates that the data is Binary and binary type is "blob"
101 // a new Blob object, created in the relevant Realm of the WebSocket
102 // object, that represents data as its raw data
103 dataForEvent = new Blob([data])
104 } else {
105 // -> type indicates that the data is Binary and binary type is "arraybuffer"
106 // a new ArrayBuffer object, created in the relevant Realm of the
107 // WebSocket object, whose contents are data
108 dataForEvent = toArrayBuffer(data)
109 }
110 }
111
112 // 3. Fire an event named message at the WebSocket object, using MessageEvent,
113 // with the origin attribute initialized to the serialization of the WebSocket
114 // object’s url's origin, and the data attribute initialized to dataForEvent.
115 fireEvent('message', ws, createFastMessageEvent, {
116 origin: ws[kWebSocketURL].origin,
117 data: dataForEvent
118 })
119}
120
121function toArrayBuffer (buffer) {
122 if (buffer.byteLength === buffer.buffer.byteLength) {
123 return buffer.buffer
124 }
125 return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
126}
127
128/**
129 * @see https://datatracker.ietf.org/doc/html/rfc6455
130 * @see https://datatracker.ietf.org/doc/html/rfc2616
131 * @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407
132 * @param {string} protocol
133 */
134function isValidSubprotocol (protocol) {
135 // If present, this value indicates one
136 // or more comma-separated subprotocol the client wishes to speak,
137 // ordered by preference. The elements that comprise this value
138 // MUST be non-empty strings with characters in the range U+0021 to
139 // U+007E not including separator characters as defined in
140 // [RFC2616] and MUST all be unique strings.
141 if (protocol.length === 0) {
142 return false
143 }
144
145 for (let i = 0; i < protocol.length; ++i) {
146 const code = protocol.charCodeAt(i)
147
148 if (
149 code < 0x21 || // CTL, contains SP (0x20) and HT (0x09)
150 code > 0x7E ||
151 code === 0x22 || // "
152 code === 0x28 || // (
153 code === 0x29 || // )
154 code === 0x2C || // ,
155 code === 0x2F || // /
156 code === 0x3A || // :
157 code === 0x3B || // ;
158 code === 0x3C || // <
159 code === 0x3D || // =
160 code === 0x3E || // >
161 code === 0x3F || // ?
162 code === 0x40 || // @
163 code === 0x5B || // [
164 code === 0x5C || // \
165 code === 0x5D || // ]
166 code === 0x7B || // {
167 code === 0x7D // }
168 ) {
169 return false
170 }
171 }
172
173 return true
174}
175
176/**
177 * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4
178 * @param {number} code
179 */
180function isValidStatusCode (code) {
181 if (code >= 1000 && code < 1015) {
182 return (
183 code !== 1004 && // reserved
184 code !== 1005 && // "MUST NOT be set as a status code"
185 code !== 1006 // "MUST NOT be set as a status code"
186 )
187 }
188
189 return code >= 3000 && code <= 4999
190}
191
192/**
193 * @param {import('./websocket').WebSocket} ws
194 * @param {string|undefined} reason
195 */
196function failWebsocketConnection (ws, reason) {
197 const { [kController]: controller, [kResponse]: response } = ws
198
199 controller.abort()
200
201 if (response?.socket && !response.socket.destroyed) {
202 response.socket.destroy()
203 }
204
205 if (reason) {
206 // TODO: process.nextTick
207 fireEvent('error', ws, (type, init) => new ErrorEvent(type, init), {
208 error: new Error(reason),
209 message: reason
210 })
211 }
212}
213
214/**
215 * @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5
216 * @param {number} opcode
217 */
218function isControlFrame (opcode) {
219 return (
220 opcode === opcodes.CLOSE ||
221 opcode === opcodes.PING ||
222 opcode === opcodes.PONG
223 )
224}
225
226function isContinuationFrame (opcode) {
227 return opcode === opcodes.CONTINUATION
228}
229
230function isTextBinaryFrame (opcode) {
231 return opcode === opcodes.TEXT || opcode === opcodes.BINARY
232}
233
234function isValidOpcode (opcode) {
235 return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
236}
237
238/**
239 * Parses a Sec-WebSocket-Extensions header value.
240 * @param {string} extensions
241 * @returns {Map<string, string>}
242 */
243// TODO(@Uzlopak, @KhafraDev): make compliant https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
244function parseExtensions (extensions) {
245 const position = { position: 0 }
246 const extensionList = new Map()
247
248 while (position.position < extensions.length) {
249 const pair = collectASequenceOfCodePointsFast(';', extensions, position)
250 const [name, value = ''] = pair.split('=')
251
252 extensionList.set(
253 removeHTTPWhitespace(name, true, false),
254 removeHTTPWhitespace(value, false, true)
255 )
256
257 position.position++
258 }
259
260 return extensionList
261}
262
263/**
264 * @see https://www.rfc-editor.org/rfc/rfc7692#section-7.1.2.2
265 * @description "client-max-window-bits = 1*DIGIT"
266 * @param {string} value
267 */
268function isValidClientWindowBits (value) {
269 // Must have at least one character
270 if (value.length === 0) {
271 return false
272 }
273
274 // Check all characters are ASCII digits
275 for (let i = 0; i < value.length; i++) {
276 const byte = value.charCodeAt(i)
277
278 if (byte < 0x30 || byte > 0x39) {
279 return false
280 }
281 }
282
283 // Check numeric range: zlib requires windowBits in range 8-15
284 const num = Number.parseInt(value, 10)
285 return num >= 8 && num <= 15
286}
287
288// https://nodejs.org/api/intl.html#detecting-internationalization-support
289const hasIntl = typeof process.versions.icu === 'string'
290const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undefined
291
292/**
293 * Converts a Buffer to utf-8, even on platforms without icu.
294 * @param {Buffer} buffer
295 */
296const utf8Decode = hasIntl
297 ? fatalDecoder.decode.bind(fatalDecoder)
298 : function (buffer) {
299 if (isUtf8(buffer)) {
300 return buffer.toString('utf-8')
301 }
302 throw new TypeError('Invalid utf-8 received.')
303 }
304
305module.exports = {
306 isConnecting,
307 isEstablished,
308 isClosing,
309 isClosed,
310 fireEvent,
311 isValidSubprotocol,
312 isValidStatusCode,
313 failWebsocketConnection,
314 websocketMessageReceived,
315 utf8Decode,
316 isControlFrame,
317 isContinuationFrame,
318 isTextBinaryFrame,
319 isValidOpcode,
320 parseExtensions,
321 isValidClientWindowBits
322}
323 