websocket.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. 'use strict'
  2. const { webidl } = require('../fetch/webidl')
  3. const { DOMException } = require('../fetch/constants')
  4. const { URLSerializer } = require('../fetch/dataURL')
  5. const { staticPropertyDescriptors, states, opcodes, emptyBuffer } = require('./constants')
  6. const {
  7. kWebSocketURL,
  8. kReadyState,
  9. kController,
  10. kBinaryType,
  11. kResponse,
  12. kSentClose,
  13. kByteParser
  14. } = require('./symbols')
  15. const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, fireEvent } = require('./util')
  16. const { establishWebSocketConnection } = require('./connection')
  17. const { WebsocketFrameSend } = require('./frame')
  18. const { ByteParser } = require('./receiver')
  19. const { kEnumerableProperty, isBlobLike } = require('../core/util')
  20. const { getGlobalDispatcher } = require('../global')
  21. const { types } = require('util')
  22. let experimentalWarned = false
  23. // https://websockets.spec.whatwg.org/#interface-definition
  24. class WebSocket extends EventTarget {
  25. #events = {
  26. open: null,
  27. error: null,
  28. close: null,
  29. message: null
  30. }
  31. #bufferedAmount = 0
  32. #protocol = ''
  33. #extensions = ''
  34. /**
  35. * @param {string} url
  36. * @param {string|string[]} protocols
  37. */
  38. constructor (url, protocols = []) {
  39. super()
  40. webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket constructor' })
  41. if (!experimentalWarned) {
  42. experimentalWarned = true
  43. process.emitWarning('WebSockets are experimental, expect them to change at any time.', {
  44. code: 'UNDICI-WS'
  45. })
  46. }
  47. const options = webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'](protocols)
  48. url = webidl.converters.USVString(url)
  49. protocols = options.protocols
  50. // 1. Let urlRecord be the result of applying the URL parser to url.
  51. let urlRecord
  52. try {
  53. urlRecord = new URL(url)
  54. } catch (e) {
  55. // 2. If urlRecord is failure, then throw a "SyntaxError" DOMException.
  56. throw new DOMException(e, 'SyntaxError')
  57. }
  58. // 3. If urlRecord’s scheme is not "ws" or "wss", then throw a
  59. // "SyntaxError" DOMException.
  60. if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') {
  61. throw new DOMException(
  62. `Expected a ws: or wss: protocol, got ${urlRecord.protocol}`,
  63. 'SyntaxError'
  64. )
  65. }
  66. // 4. If urlRecord’s fragment is non-null, then throw a "SyntaxError"
  67. // DOMException.
  68. if (urlRecord.hash) {
  69. throw new DOMException('Got fragment', 'SyntaxError')
  70. }
  71. // 5. If protocols is a string, set protocols to a sequence consisting
  72. // of just that string.
  73. if (typeof protocols === 'string') {
  74. protocols = [protocols]
  75. }
  76. // 6. If any of the values in protocols occur more than once or otherwise
  77. // fail to match the requirements for elements that comprise the value
  78. // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket
  79. // protocol, then throw a "SyntaxError" DOMException.
  80. if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) {
  81. throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
  82. }
  83. if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) {
  84. throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
  85. }
  86. // 7. Set this's url to urlRecord.
  87. this[kWebSocketURL] = urlRecord
  88. // 8. Let client be this's relevant settings object.
  89. // 9. Run this step in parallel:
  90. // 1. Establish a WebSocket connection given urlRecord, protocols,
  91. // and client.
  92. this[kController] = establishWebSocketConnection(
  93. urlRecord,
  94. protocols,
  95. this,
  96. (response) => this.#onConnectionEstablished(response),
  97. options
  98. )
  99. // Each WebSocket object has an associated ready state, which is a
  100. // number representing the state of the connection. Initially it must
  101. // be CONNECTING (0).
  102. this[kReadyState] = WebSocket.CONNECTING
  103. // The extensions attribute must initially return the empty string.
  104. // The protocol attribute must initially return the empty string.
  105. // Each WebSocket object has an associated binary type, which is a
  106. // BinaryType. Initially it must be "blob".
  107. this[kBinaryType] = 'blob'
  108. }
  109. /**
  110. * @see https://websockets.spec.whatwg.org/#dom-websocket-close
  111. * @param {number|undefined} code
  112. * @param {string|undefined} reason
  113. */
  114. close (code = undefined, reason = undefined) {
  115. webidl.brandCheck(this, WebSocket)
  116. if (code !== undefined) {
  117. code = webidl.converters['unsigned short'](code, { clamp: true })
  118. }
  119. if (reason !== undefined) {
  120. reason = webidl.converters.USVString(reason)
  121. }
  122. // 1. If code is present, but is neither an integer equal to 1000 nor an
  123. // integer in the range 3000 to 4999, inclusive, throw an
  124. // "InvalidAccessError" DOMException.
  125. if (code !== undefined) {
  126. if (code !== 1000 && (code < 3000 || code > 4999)) {
  127. throw new DOMException('invalid code', 'InvalidAccessError')
  128. }
  129. }
  130. let reasonByteLength = 0
  131. // 2. If reason is present, then run these substeps:
  132. if (reason !== undefined) {
  133. // 1. Let reasonBytes be the result of encoding reason.
  134. // 2. If reasonBytes is longer than 123 bytes, then throw a
  135. // "SyntaxError" DOMException.
  136. reasonByteLength = Buffer.byteLength(reason)
  137. if (reasonByteLength > 123) {
  138. throw new DOMException(
  139. `Reason must be less than 123 bytes; received ${reasonByteLength}`,
  140. 'SyntaxError'
  141. )
  142. }
  143. }
  144. // 3. Run the first matching steps from the following list:
  145. if (this[kReadyState] === WebSocket.CLOSING || this[kReadyState] === WebSocket.CLOSED) {
  146. // If this's ready state is CLOSING (2) or CLOSED (3)
  147. // Do nothing.
  148. } else if (!isEstablished(this)) {
  149. // If the WebSocket connection is not yet established
  150. // Fail the WebSocket connection and set this's ready state
  151. // to CLOSING (2).
  152. failWebsocketConnection(this, 'Connection was closed before it was established.')
  153. this[kReadyState] = WebSocket.CLOSING
  154. } else if (!isClosing(this)) {
  155. // If the WebSocket closing handshake has not yet been started
  156. // Start the WebSocket closing handshake and set this's ready
  157. // state to CLOSING (2).
  158. // - If neither code nor reason is present, the WebSocket Close
  159. // message must not have a body.
  160. // - If code is present, then the status code to use in the
  161. // WebSocket Close message must be the integer given by code.
  162. // - If reason is also present, then reasonBytes must be
  163. // provided in the Close message after the status code.
  164. const frame = new WebsocketFrameSend()
  165. // If neither code nor reason is present, the WebSocket Close
  166. // message must not have a body.
  167. // If code is present, then the status code to use in the
  168. // WebSocket Close message must be the integer given by code.
  169. if (code !== undefined && reason === undefined) {
  170. frame.frameData = Buffer.allocUnsafe(2)
  171. frame.frameData.writeUInt16BE(code, 0)
  172. } else if (code !== undefined && reason !== undefined) {
  173. // If reason is also present, then reasonBytes must be
  174. // provided in the Close message after the status code.
  175. frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength)
  176. frame.frameData.writeUInt16BE(code, 0)
  177. // the body MAY contain UTF-8-encoded data with value /reason/
  178. frame.frameData.write(reason, 2, 'utf-8')
  179. } else {
  180. frame.frameData = emptyBuffer
  181. }
  182. /** @type {import('stream').Duplex} */
  183. const socket = this[kResponse].socket
  184. socket.write(frame.createFrame(opcodes.CLOSE), (err) => {
  185. if (!err) {
  186. this[kSentClose] = true
  187. }
  188. })
  189. // Upon either sending or receiving a Close control frame, it is said
  190. // that _The WebSocket Closing Handshake is Started_ and that the
  191. // WebSocket connection is in the CLOSING state.
  192. this[kReadyState] = states.CLOSING
  193. } else {
  194. // Otherwise
  195. // Set this's ready state to CLOSING (2).
  196. this[kReadyState] = WebSocket.CLOSING
  197. }
  198. }
  199. /**
  200. * @see https://websockets.spec.whatwg.org/#dom-websocket-send
  201. * @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data
  202. */
  203. send (data) {
  204. webidl.brandCheck(this, WebSocket)
  205. webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket.send' })
  206. data = webidl.converters.WebSocketSendData(data)
  207. // 1. If this's ready state is CONNECTING, then throw an
  208. // "InvalidStateError" DOMException.
  209. if (this[kReadyState] === WebSocket.CONNECTING) {
  210. throw new DOMException('Sent before connected.', 'InvalidStateError')
  211. }
  212. // 2. Run the appropriate set of steps from the following list:
  213. // https://datatracker.ietf.org/doc/html/rfc6455#section-6.1
  214. // https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
  215. if (!isEstablished(this) || isClosing(this)) {
  216. return
  217. }
  218. /** @type {import('stream').Duplex} */
  219. const socket = this[kResponse].socket
  220. // If data is a string
  221. if (typeof data === 'string') {
  222. // If the WebSocket connection is established and the WebSocket
  223. // closing handshake has not yet started, then the user agent
  224. // must send a WebSocket Message comprised of the data argument
  225. // using a text frame opcode; if the data cannot be sent, e.g.
  226. // because it would need to be buffered but the buffer is full,
  227. // the user agent must flag the WebSocket as full and then close
  228. // the WebSocket connection. Any invocation of this method with a
  229. // string argument that does not throw an exception must increase
  230. // the bufferedAmount attribute by the number of bytes needed to
  231. // express the argument as UTF-8.
  232. const value = Buffer.from(data)
  233. const frame = new WebsocketFrameSend(value)
  234. const buffer = frame.createFrame(opcodes.TEXT)
  235. this.#bufferedAmount += value.byteLength
  236. socket.write(buffer, () => {
  237. this.#bufferedAmount -= value.byteLength
  238. })
  239. } else if (types.isArrayBuffer(data)) {
  240. // If the WebSocket connection is established, and the WebSocket
  241. // closing handshake has not yet started, then the user agent must
  242. // send a WebSocket Message comprised of data using a binary frame
  243. // opcode; if the data cannot be sent, e.g. because it would need
  244. // to be buffered but the buffer is full, the user agent must flag
  245. // the WebSocket as full and then close the WebSocket connection.
  246. // The data to be sent is the data stored in the buffer described
  247. // by the ArrayBuffer object. Any invocation of this method with an
  248. // ArrayBuffer argument that does not throw an exception must
  249. // increase the bufferedAmount attribute by the length of the
  250. // ArrayBuffer in bytes.
  251. const value = Buffer.from(data)
  252. const frame = new WebsocketFrameSend(value)
  253. const buffer = frame.createFrame(opcodes.BINARY)
  254. this.#bufferedAmount += value.byteLength
  255. socket.write(buffer, () => {
  256. this.#bufferedAmount -= value.byteLength
  257. })
  258. } else if (ArrayBuffer.isView(data)) {
  259. // If the WebSocket connection is established, and the WebSocket
  260. // closing handshake has not yet started, then the user agent must
  261. // send a WebSocket Message comprised of data using a binary frame
  262. // opcode; if the data cannot be sent, e.g. because it would need to
  263. // be buffered but the buffer is full, the user agent must flag the
  264. // WebSocket as full and then close the WebSocket connection. The
  265. // data to be sent is the data stored in the section of the buffer
  266. // described by the ArrayBuffer object that data references. Any
  267. // invocation of this method with this kind of argument that does
  268. // not throw an exception must increase the bufferedAmount attribute
  269. // by the length of data’s buffer in bytes.
  270. const ab = Buffer.from(data, data.byteOffset, data.byteLength)
  271. const frame = new WebsocketFrameSend(ab)
  272. const buffer = frame.createFrame(opcodes.BINARY)
  273. this.#bufferedAmount += ab.byteLength
  274. socket.write(buffer, () => {
  275. this.#bufferedAmount -= ab.byteLength
  276. })
  277. } else if (isBlobLike(data)) {
  278. // If the WebSocket connection is established, and the WebSocket
  279. // closing handshake has not yet started, then the user agent must
  280. // send a WebSocket Message comprised of data using a binary frame
  281. // opcode; if the data cannot be sent, e.g. because it would need to
  282. // be buffered but the buffer is full, the user agent must flag the
  283. // WebSocket as full and then close the WebSocket connection. The data
  284. // to be sent is the raw data represented by the Blob object. Any
  285. // invocation of this method with a Blob argument that does not throw
  286. // an exception must increase the bufferedAmount attribute by the size
  287. // of the Blob object’s raw data, in bytes.
  288. const frame = new WebsocketFrameSend()
  289. data.arrayBuffer().then((ab) => {
  290. const value = Buffer.from(ab)
  291. frame.frameData = value
  292. const buffer = frame.createFrame(opcodes.BINARY)
  293. this.#bufferedAmount += value.byteLength
  294. socket.write(buffer, () => {
  295. this.#bufferedAmount -= value.byteLength
  296. })
  297. })
  298. }
  299. }
  300. get readyState () {
  301. webidl.brandCheck(this, WebSocket)
  302. // The readyState getter steps are to return this's ready state.
  303. return this[kReadyState]
  304. }
  305. get bufferedAmount () {
  306. webidl.brandCheck(this, WebSocket)
  307. return this.#bufferedAmount
  308. }
  309. get url () {
  310. webidl.brandCheck(this, WebSocket)
  311. // The url getter steps are to return this's url, serialized.
  312. return URLSerializer(this[kWebSocketURL])
  313. }
  314. get extensions () {
  315. webidl.brandCheck(this, WebSocket)
  316. return this.#extensions
  317. }
  318. get protocol () {
  319. webidl.brandCheck(this, WebSocket)
  320. return this.#protocol
  321. }
  322. get onopen () {
  323. webidl.brandCheck(this, WebSocket)
  324. return this.#events.open
  325. }
  326. set onopen (fn) {
  327. webidl.brandCheck(this, WebSocket)
  328. if (this.#events.open) {
  329. this.removeEventListener('open', this.#events.open)
  330. }
  331. if (typeof fn === 'function') {
  332. this.#events.open = fn
  333. this.addEventListener('open', fn)
  334. } else {
  335. this.#events.open = null
  336. }
  337. }
  338. get onerror () {
  339. webidl.brandCheck(this, WebSocket)
  340. return this.#events.error
  341. }
  342. set onerror (fn) {
  343. webidl.brandCheck(this, WebSocket)
  344. if (this.#events.error) {
  345. this.removeEventListener('error', this.#events.error)
  346. }
  347. if (typeof fn === 'function') {
  348. this.#events.error = fn
  349. this.addEventListener('error', fn)
  350. } else {
  351. this.#events.error = null
  352. }
  353. }
  354. get onclose () {
  355. webidl.brandCheck(this, WebSocket)
  356. return this.#events.close
  357. }
  358. set onclose (fn) {
  359. webidl.brandCheck(this, WebSocket)
  360. if (this.#events.close) {
  361. this.removeEventListener('close', this.#events.close)
  362. }
  363. if (typeof fn === 'function') {
  364. this.#events.close = fn
  365. this.addEventListener('close', fn)
  366. } else {
  367. this.#events.close = null
  368. }
  369. }
  370. get onmessage () {
  371. webidl.brandCheck(this, WebSocket)
  372. return this.#events.message
  373. }
  374. set onmessage (fn) {
  375. webidl.brandCheck(this, WebSocket)
  376. if (this.#events.message) {
  377. this.removeEventListener('message', this.#events.message)
  378. }
  379. if (typeof fn === 'function') {
  380. this.#events.message = fn
  381. this.addEventListener('message', fn)
  382. } else {
  383. this.#events.message = null
  384. }
  385. }
  386. get binaryType () {
  387. webidl.brandCheck(this, WebSocket)
  388. return this[kBinaryType]
  389. }
  390. set binaryType (type) {
  391. webidl.brandCheck(this, WebSocket)
  392. if (type !== 'blob' && type !== 'arraybuffer') {
  393. this[kBinaryType] = 'blob'
  394. } else {
  395. this[kBinaryType] = type
  396. }
  397. }
  398. /**
  399. * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
  400. */
  401. #onConnectionEstablished (response) {
  402. // processResponse is called when the "response’s header list has been received and initialized."
  403. // once this happens, the connection is open
  404. this[kResponse] = response
  405. const parser = new ByteParser(this)
  406. parser.on('drain', function onParserDrain () {
  407. this.ws[kResponse].socket.resume()
  408. })
  409. response.socket.ws = this
  410. this[kByteParser] = parser
  411. // 1. Change the ready state to OPEN (1).
  412. this[kReadyState] = states.OPEN
  413. // 2. Change the extensions attribute’s value to the extensions in use, if
  414. // it is not the null value.
  415. // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
  416. const extensions = response.headersList.get('sec-websocket-extensions')
  417. if (extensions !== null) {
  418. this.#extensions = extensions
  419. }
  420. // 3. Change the protocol attribute’s value to the subprotocol in use, if
  421. // it is not the null value.
  422. // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9
  423. const protocol = response.headersList.get('sec-websocket-protocol')
  424. if (protocol !== null) {
  425. this.#protocol = protocol
  426. }
  427. // 4. Fire an event named open at the WebSocket object.
  428. fireEvent('open', this)
  429. }
  430. }
  431. // https://websockets.spec.whatwg.org/#dom-websocket-connecting
  432. WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING
  433. // https://websockets.spec.whatwg.org/#dom-websocket-open
  434. WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN
  435. // https://websockets.spec.whatwg.org/#dom-websocket-closing
  436. WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING
  437. // https://websockets.spec.whatwg.org/#dom-websocket-closed
  438. WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED
  439. Object.defineProperties(WebSocket.prototype, {
  440. CONNECTING: staticPropertyDescriptors,
  441. OPEN: staticPropertyDescriptors,
  442. CLOSING: staticPropertyDescriptors,
  443. CLOSED: staticPropertyDescriptors,
  444. url: kEnumerableProperty,
  445. readyState: kEnumerableProperty,
  446. bufferedAmount: kEnumerableProperty,
  447. onopen: kEnumerableProperty,
  448. onerror: kEnumerableProperty,
  449. onclose: kEnumerableProperty,
  450. close: kEnumerableProperty,
  451. onmessage: kEnumerableProperty,
  452. binaryType: kEnumerableProperty,
  453. send: kEnumerableProperty,
  454. extensions: kEnumerableProperty,
  455. protocol: kEnumerableProperty,
  456. [Symbol.toStringTag]: {
  457. value: 'WebSocket',
  458. writable: false,
  459. enumerable: false,
  460. configurable: true
  461. }
  462. })
  463. Object.defineProperties(WebSocket, {
  464. CONNECTING: staticPropertyDescriptors,
  465. OPEN: staticPropertyDescriptors,
  466. CLOSING: staticPropertyDescriptors,
  467. CLOSED: staticPropertyDescriptors
  468. })
  469. webidl.converters['sequence<DOMString>'] = webidl.sequenceConverter(
  470. webidl.converters.DOMString
  471. )
  472. webidl.converters['DOMString or sequence<DOMString>'] = function (V) {
  473. if (webidl.util.Type(V) === 'Object' && Symbol.iterator in V) {
  474. return webidl.converters['sequence<DOMString>'](V)
  475. }
  476. return webidl.converters.DOMString(V)
  477. }
  478. // This implements the propsal made in https://github.com/whatwg/websockets/issues/42
  479. webidl.converters.WebSocketInit = webidl.dictionaryConverter([
  480. {
  481. key: 'protocols',
  482. converter: webidl.converters['DOMString or sequence<DOMString>'],
  483. get defaultValue () {
  484. return []
  485. }
  486. },
  487. {
  488. key: 'dispatcher',
  489. converter: (V) => V,
  490. get defaultValue () {
  491. return getGlobalDispatcher()
  492. }
  493. },
  494. {
  495. key: 'headers',
  496. converter: webidl.nullableConverter(webidl.converters.HeadersInit)
  497. }
  498. ])
  499. webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'] = function (V) {
  500. if (webidl.util.Type(V) === 'Object' && !(Symbol.iterator in V)) {
  501. return webidl.converters.WebSocketInit(V)
  502. }
  503. return { protocols: webidl.converters['DOMString or sequence<DOMString>'](V) }
  504. }
  505. webidl.converters.WebSocketSendData = function (V) {
  506. if (webidl.util.Type(V) === 'Object') {
  507. if (isBlobLike(V)) {
  508. return webidl.converters.Blob(V, { strict: false })
  509. }
  510. if (ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)) {
  511. return webidl.converters.BufferSource(V)
  512. }
  513. }
  514. return webidl.converters.USVString(V)
  515. }
  516. module.exports = {
  517. WebSocket
  518. }