util.js 8.9 KB

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