Spaces:
Runtime error
Runtime error
| const diagnosticsChannel = require('diagnostics_channel') | |
| const { uid, states } = require('./constants') | |
| const { | |
| kReadyState, | |
| kSentClose, | |
| kByteParser, | |
| kReceivedClose | |
| } = require('./symbols') | |
| const { fireEvent, failWebsocketConnection } = require('./util') | |
| const { CloseEvent } = require('./events') | |
| const { makeRequest } = require('../fetch/request') | |
| const { fetching } = require('../fetch/index') | |
| const { Headers } = require('../fetch/headers') | |
| const { getGlobalDispatcher } = require('../global') | |
| const { kHeadersList } = require('../core/symbols') | |
| const channels = {} | |
| channels.open = diagnosticsChannel.channel('undici:websocket:open') | |
| channels.close = diagnosticsChannel.channel('undici:websocket:close') | |
| channels.socketError = diagnosticsChannel.channel('undici:websocket:socket_error') | |
| /** @type {import('crypto')} */ | |
| let crypto | |
| try { | |
| crypto = require('crypto') | |
| } catch { | |
| } | |
| /** | |
| * @see https://websockets.spec.whatwg.org/#concept-websocket-establish | |
| * @param {URL} url | |
| * @param {string|string[]} protocols | |
| * @param {import('./websocket').WebSocket} ws | |
| * @param {(response: any) => void} onEstablish | |
| * @param {Partial<import('../../types/websocket').WebSocketInit>} options | |
| */ | |
| function establishWebSocketConnection (url, protocols, ws, onEstablish, options) { | |
| // 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s | |
| // scheme is "ws", and to "https" otherwise. | |
| const requestURL = url | |
| requestURL.protocol = url.protocol === 'ws:' ? 'http:' : 'https:' | |
| // 2. Let request be a new request, whose URL is requestURL, client is client, | |
| // service-workers mode is "none", referrer is "no-referrer", mode is | |
| // "websocket", credentials mode is "include", cache mode is "no-store" , | |
| // and redirect mode is "error". | |
| const request = makeRequest({ | |
| urlList: [requestURL], | |
| serviceWorkers: 'none', | |
| referrer: 'no-referrer', | |
| mode: 'websocket', | |
| credentials: 'include', | |
| cache: 'no-store', | |
| redirect: 'error' | |
| }) | |
| // Note: undici extension, allow setting custom headers. | |
| if (options.headers) { | |
| const headersList = new Headers(options.headers)[kHeadersList] | |
| request.headersList = headersList | |
| } | |
| // 3. Append (`Upgrade`, `websocket`) to request’s header list. | |
| // 4. Append (`Connection`, `Upgrade`) to request’s header list. | |
| // Note: both of these are handled by undici currently. | |
| // https://github.com/nodejs/undici/blob/68c269c4144c446f3f1220951338daef4a6b5ec4/lib/client.js#L1397 | |
| // 5. Let keyValue be a nonce consisting of a randomly selected | |
| // 16-byte value that has been forgiving-base64-encoded and | |
| // isomorphic encoded. | |
| const keyValue = crypto.randomBytes(16).toString('base64') | |
| // 6. Append (`Sec-WebSocket-Key`, keyValue) to request’s | |
| // header list. | |
| request.headersList.append('sec-websocket-key', keyValue) | |
| // 7. Append (`Sec-WebSocket-Version`, `13`) to request’s | |
| // header list. | |
| request.headersList.append('sec-websocket-version', '13') | |
| // 8. For each protocol in protocols, combine | |
| // (`Sec-WebSocket-Protocol`, protocol) in request’s header | |
| // list. | |
| for (const protocol of protocols) { | |
| request.headersList.append('sec-websocket-protocol', protocol) | |
| } | |
| // 9. Let permessageDeflate be a user-agent defined | |
| // "permessage-deflate" extension header value. | |
| // https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673 | |
| // TODO: enable once permessage-deflate is supported | |
| const permessageDeflate = '' // 'permessage-deflate; 15' | |
| // 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to | |
| // request’s header list. | |
| // request.headersList.append('sec-websocket-extensions', permessageDeflate) | |
| // 11. Fetch request with useParallelQueue set to true, and | |
| // processResponse given response being these steps: | |
| const controller = fetching({ | |
| request, | |
| useParallelQueue: true, | |
| dispatcher: options.dispatcher ?? getGlobalDispatcher(), | |
| processResponse (response) { | |
| // 1. If response is a network error or its status is not 101, | |
| // fail the WebSocket connection. | |
| if (response.type === 'error' || response.status !== 101) { | |
| failWebsocketConnection(ws, 'Received network error or non-101 status code.') | |
| return | |
| } | |
| // 2. If protocols is not the empty list and extracting header | |
| // list values given `Sec-WebSocket-Protocol` and response’s | |
| // header list results in null, failure, or the empty byte | |
| // sequence, then fail the WebSocket connection. | |
| if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) { | |
| failWebsocketConnection(ws, 'Server did not respond with sent protocols.') | |
| return | |
| } | |
| // 3. Follow the requirements stated step 2 to step 6, inclusive, | |
| // of the last set of steps in section 4.1 of The WebSocket | |
| // Protocol to validate response. This either results in fail | |
| // the WebSocket connection or the WebSocket connection is | |
| // established. | |
| // 2. If the response lacks an |Upgrade| header field or the |Upgrade| | |
| // header field contains a value that is not an ASCII case- | |
| // insensitive match for the value "websocket", the client MUST | |
| // _Fail the WebSocket Connection_. | |
| if (response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') { | |
| failWebsocketConnection(ws, 'Server did not set Upgrade header to "websocket".') | |
| return | |
| } | |
| // 3. If the response lacks a |Connection| header field or the | |
| // |Connection| header field doesn't contain a token that is an | |
| // ASCII case-insensitive match for the value "Upgrade", the client | |
| // MUST _Fail the WebSocket Connection_. | |
| if (response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') { | |
| failWebsocketConnection(ws, 'Server did not set Connection header to "upgrade".') | |
| return | |
| } | |
| // 4. If the response lacks a |Sec-WebSocket-Accept| header field or | |
| // the |Sec-WebSocket-Accept| contains a value other than the | |
| // base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket- | |
| // Key| (as a string, not base64-decoded) with the string "258EAFA5- | |
| // E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and | |
| // trailing whitespace, the client MUST _Fail the WebSocket | |
| // Connection_. | |
| const secWSAccept = response.headersList.get('Sec-WebSocket-Accept') | |
| const digest = crypto.createHash('sha1').update(keyValue + uid).digest('base64') | |
| if (secWSAccept !== digest) { | |
| failWebsocketConnection(ws, 'Incorrect hash received in Sec-WebSocket-Accept header.') | |
| return | |
| } | |
| // 5. If the response includes a |Sec-WebSocket-Extensions| header | |
| // field and this header field indicates the use of an extension | |
| // that was not present in the client's handshake (the server has | |
| // indicated an extension not requested by the client), the client | |
| // MUST _Fail the WebSocket Connection_. (The parsing of this | |
| // header field to determine which extensions are requested is | |
| // discussed in Section 9.1.) | |
| const secExtension = response.headersList.get('Sec-WebSocket-Extensions') | |
| if (secExtension !== null && secExtension !== permessageDeflate) { | |
| failWebsocketConnection(ws, 'Received different permessage-deflate than the one set.') | |
| return | |
| } | |
| // 6. If the response includes a |Sec-WebSocket-Protocol| header field | |
| // and this header field indicates the use of a subprotocol that was | |
| // not present in the client's handshake (the server has indicated a | |
| // subprotocol not requested by the client), the client MUST _Fail | |
| // the WebSocket Connection_. | |
| const secProtocol = response.headersList.get('Sec-WebSocket-Protocol') | |
| if (secProtocol !== null && secProtocol !== request.headersList.get('Sec-WebSocket-Protocol')) { | |
| failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.') | |
| return | |
| } | |
| response.socket.on('data', onSocketData) | |
| response.socket.on('close', onSocketClose) | |
| response.socket.on('error', onSocketError) | |
| if (channels.open.hasSubscribers) { | |
| channels.open.publish({ | |
| address: response.socket.address(), | |
| protocol: secProtocol, | |
| extensions: secExtension | |
| }) | |
| } | |
| onEstablish(response) | |
| } | |
| }) | |
| return controller | |
| } | |
| /** | |
| * @param {Buffer} chunk | |
| */ | |
| function onSocketData (chunk) { | |
| if (!this.ws[kByteParser].write(chunk)) { | |
| this.pause() | |
| } | |
| } | |
| /** | |
| * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol | |
| * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4 | |
| */ | |
| function onSocketClose () { | |
| const { ws } = this | |
| // If the TCP connection was closed after the | |
| // WebSocket closing handshake was completed, the WebSocket connection | |
| // is said to have been closed _cleanly_. | |
| const wasClean = ws[kSentClose] && ws[kReceivedClose] | |
| let code = 1005 | |
| let reason = '' | |
| const result = ws[kByteParser].closingInfo | |
| if (result) { | |
| code = result.code ?? 1005 | |
| reason = result.reason | |
| } else if (!ws[kSentClose]) { | |
| // If _The WebSocket | |
| // Connection is Closed_ and no Close control frame was received by the | |
| // endpoint (such as could occur if the underlying transport connection | |
| // is lost), _The WebSocket Connection Close Code_ is considered to be | |
| // 1006. | |
| code = 1006 | |
| } | |
| // 1. Change the ready state to CLOSED (3). | |
| ws[kReadyState] = states.CLOSED | |
| // 2. If the user agent was required to fail the WebSocket | |
| // connection, or if the WebSocket connection was closed | |
| // after being flagged as full, fire an event named error | |
| // at the WebSocket object. | |
| // TODO | |
| // 3. Fire an event named close at the WebSocket object, | |
| // using CloseEvent, with the wasClean attribute | |
| // initialized to true if the connection closed cleanly | |
| // and false otherwise, the code attribute initialized to | |
| // the WebSocket connection close code, and the reason | |
| // attribute initialized to the result of applying UTF-8 | |
| // decode without BOM to the WebSocket connection close | |
| // reason. | |
| fireEvent('close', ws, CloseEvent, { | |
| wasClean, code, reason | |
| }) | |
| if (channels.close.hasSubscribers) { | |
| channels.close.publish({ | |
| websocket: ws, | |
| code, | |
| reason | |
| }) | |
| } | |
| } | |
| function onSocketError (error) { | |
| const { ws } = this | |
| ws[kReadyState] = states.CLOSING | |
| if (channels.socketError.hasSubscribers) { | |
| channels.socketError.publish(error) | |
| } | |
| this.destroy() | |
| } | |
| module.exports = { | |
| establishWebSocketConnection | |
| } | |