websocket.js 18 KB

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