1 | 'use strict';
|
---|
2 |
|
---|
3 | const EventEmitter = require('events');
|
---|
4 | const https = require('https');
|
---|
5 | const http = require('http');
|
---|
6 | const net = require('net');
|
---|
7 | const tls = require('tls');
|
---|
8 | const { randomBytes, createHash } = require('crypto');
|
---|
9 | const { URL } = require('url');
|
---|
10 |
|
---|
11 | const PerMessageDeflate = require('./permessage-deflate');
|
---|
12 | const Receiver = require('./receiver');
|
---|
13 | const Sender = require('./sender');
|
---|
14 | const {
|
---|
15 | BINARY_TYPES,
|
---|
16 | EMPTY_BUFFER,
|
---|
17 | GUID,
|
---|
18 | kStatusCode,
|
---|
19 | kWebSocket,
|
---|
20 | NOOP
|
---|
21 | } = require('./constants');
|
---|
22 | const { addEventListener, removeEventListener } = require('./event-target');
|
---|
23 | const { format, parse } = require('./extension');
|
---|
24 | const { toBuffer } = require('./buffer-util');
|
---|
25 |
|
---|
26 | const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
|
---|
27 | const protocolVersions = [8, 13];
|
---|
28 | const closeTimeout = 30 * 1000;
|
---|
29 |
|
---|
30 | /**
|
---|
31 | * Class representing a WebSocket.
|
---|
32 | *
|
---|
33 | * @extends EventEmitter
|
---|
34 | */
|
---|
35 | class WebSocket extends EventEmitter {
|
---|
36 | /**
|
---|
37 | * Create a new `WebSocket`.
|
---|
38 | *
|
---|
39 | * @param {(String|url.URL)} address The URL to which to connect
|
---|
40 | * @param {(String|String[])} [protocols] The subprotocols
|
---|
41 | * @param {Object} [options] Connection options
|
---|
42 | */
|
---|
43 | constructor(address, protocols, options) {
|
---|
44 | super();
|
---|
45 |
|
---|
46 | this._binaryType = BINARY_TYPES[0];
|
---|
47 | this._closeCode = 1006;
|
---|
48 | this._closeFrameReceived = false;
|
---|
49 | this._closeFrameSent = false;
|
---|
50 | this._closeMessage = '';
|
---|
51 | this._closeTimer = null;
|
---|
52 | this._extensions = {};
|
---|
53 | this._protocol = '';
|
---|
54 | this._readyState = WebSocket.CONNECTING;
|
---|
55 | this._receiver = null;
|
---|
56 | this._sender = null;
|
---|
57 | this._socket = null;
|
---|
58 |
|
---|
59 | if (address !== null) {
|
---|
60 | this._bufferedAmount = 0;
|
---|
61 | this._isServer = false;
|
---|
62 | this._redirects = 0;
|
---|
63 |
|
---|
64 | if (Array.isArray(protocols)) {
|
---|
65 | protocols = protocols.join(', ');
|
---|
66 | } else if (typeof protocols === 'object' && protocols !== null) {
|
---|
67 | options = protocols;
|
---|
68 | protocols = undefined;
|
---|
69 | }
|
---|
70 |
|
---|
71 | initAsClient(this, address, protocols, options);
|
---|
72 | } else {
|
---|
73 | this._isServer = true;
|
---|
74 | }
|
---|
75 | }
|
---|
76 |
|
---|
77 | /**
|
---|
78 | * This deviates from the WHATWG interface since ws doesn't support the
|
---|
79 | * required default "blob" type (instead we define a custom "nodebuffer"
|
---|
80 | * type).
|
---|
81 | *
|
---|
82 | * @type {String}
|
---|
83 | */
|
---|
84 | get binaryType() {
|
---|
85 | return this._binaryType;
|
---|
86 | }
|
---|
87 |
|
---|
88 | set binaryType(type) {
|
---|
89 | if (!BINARY_TYPES.includes(type)) return;
|
---|
90 |
|
---|
91 | this._binaryType = type;
|
---|
92 |
|
---|
93 | //
|
---|
94 | // Allow to change `binaryType` on the fly.
|
---|
95 | //
|
---|
96 | if (this._receiver) this._receiver._binaryType = type;
|
---|
97 | }
|
---|
98 |
|
---|
99 | /**
|
---|
100 | * @type {Number}
|
---|
101 | */
|
---|
102 | get bufferedAmount() {
|
---|
103 | if (!this._socket) return this._bufferedAmount;
|
---|
104 |
|
---|
105 | return this._socket._writableState.length + this._sender._bufferedBytes;
|
---|
106 | }
|
---|
107 |
|
---|
108 | /**
|
---|
109 | * @type {String}
|
---|
110 | */
|
---|
111 | get extensions() {
|
---|
112 | return Object.keys(this._extensions).join();
|
---|
113 | }
|
---|
114 |
|
---|
115 | /**
|
---|
116 | * @type {String}
|
---|
117 | */
|
---|
118 | get protocol() {
|
---|
119 | return this._protocol;
|
---|
120 | }
|
---|
121 |
|
---|
122 | /**
|
---|
123 | * @type {Number}
|
---|
124 | */
|
---|
125 | get readyState() {
|
---|
126 | return this._readyState;
|
---|
127 | }
|
---|
128 |
|
---|
129 | /**
|
---|
130 | * @type {String}
|
---|
131 | */
|
---|
132 | get url() {
|
---|
133 | return this._url;
|
---|
134 | }
|
---|
135 |
|
---|
136 | /**
|
---|
137 | * Set up the socket and the internal resources.
|
---|
138 | *
|
---|
139 | * @param {net.Socket} socket The network socket between the server and client
|
---|
140 | * @param {Buffer} head The first packet of the upgraded stream
|
---|
141 | * @param {Number} [maxPayload=0] The maximum allowed message size
|
---|
142 | * @private
|
---|
143 | */
|
---|
144 | setSocket(socket, head, maxPayload) {
|
---|
145 | const receiver = new Receiver(
|
---|
146 | this.binaryType,
|
---|
147 | this._extensions,
|
---|
148 | this._isServer,
|
---|
149 | maxPayload
|
---|
150 | );
|
---|
151 |
|
---|
152 | this._sender = new Sender(socket, this._extensions);
|
---|
153 | this._receiver = receiver;
|
---|
154 | this._socket = socket;
|
---|
155 |
|
---|
156 | receiver[kWebSocket] = this;
|
---|
157 | socket[kWebSocket] = this;
|
---|
158 |
|
---|
159 | receiver.on('conclude', receiverOnConclude);
|
---|
160 | receiver.on('drain', receiverOnDrain);
|
---|
161 | receiver.on('error', receiverOnError);
|
---|
162 | receiver.on('message', receiverOnMessage);
|
---|
163 | receiver.on('ping', receiverOnPing);
|
---|
164 | receiver.on('pong', receiverOnPong);
|
---|
165 |
|
---|
166 | socket.setTimeout(0);
|
---|
167 | socket.setNoDelay();
|
---|
168 |
|
---|
169 | if (head.length > 0) socket.unshift(head);
|
---|
170 |
|
---|
171 | socket.on('close', socketOnClose);
|
---|
172 | socket.on('data', socketOnData);
|
---|
173 | socket.on('end', socketOnEnd);
|
---|
174 | socket.on('error', socketOnError);
|
---|
175 |
|
---|
176 | this._readyState = WebSocket.OPEN;
|
---|
177 | this.emit('open');
|
---|
178 | }
|
---|
179 |
|
---|
180 | /**
|
---|
181 | * Emit the `'close'` event.
|
---|
182 | *
|
---|
183 | * @private
|
---|
184 | */
|
---|
185 | emitClose() {
|
---|
186 | if (!this._socket) {
|
---|
187 | this._readyState = WebSocket.CLOSED;
|
---|
188 | this.emit('close', this._closeCode, this._closeMessage);
|
---|
189 | return;
|
---|
190 | }
|
---|
191 |
|
---|
192 | if (this._extensions[PerMessageDeflate.extensionName]) {
|
---|
193 | this._extensions[PerMessageDeflate.extensionName].cleanup();
|
---|
194 | }
|
---|
195 |
|
---|
196 | this._receiver.removeAllListeners();
|
---|
197 | this._readyState = WebSocket.CLOSED;
|
---|
198 | this.emit('close', this._closeCode, this._closeMessage);
|
---|
199 | }
|
---|
200 |
|
---|
201 | /**
|
---|
202 | * Start a closing handshake.
|
---|
203 | *
|
---|
204 | * +----------+ +-----------+ +----------+
|
---|
205 | * - - -|ws.close()|-->|close frame|-->|ws.close()|- - -
|
---|
206 | * | +----------+ +-----------+ +----------+ |
|
---|
207 | * +----------+ +-----------+ |
|
---|
208 | * CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING
|
---|
209 | * +----------+ +-----------+ |
|
---|
210 | * | | | +---+ |
|
---|
211 | * +------------------------+-->|fin| - - - -
|
---|
212 | * | +---+ | +---+
|
---|
213 | * - - - - -|fin|<---------------------+
|
---|
214 | * +---+
|
---|
215 | *
|
---|
216 | * @param {Number} [code] Status code explaining why the connection is closing
|
---|
217 | * @param {String} [data] A string explaining why the connection is closing
|
---|
218 | * @public
|
---|
219 | */
|
---|
220 | close(code, data) {
|
---|
221 | if (this.readyState === WebSocket.CLOSED) return;
|
---|
222 | if (this.readyState === WebSocket.CONNECTING) {
|
---|
223 | const msg = 'WebSocket was closed before the connection was established';
|
---|
224 | return abortHandshake(this, this._req, msg);
|
---|
225 | }
|
---|
226 |
|
---|
227 | if (this.readyState === WebSocket.CLOSING) {
|
---|
228 | if (this._closeFrameSent && this._closeFrameReceived) this._socket.end();
|
---|
229 | return;
|
---|
230 | }
|
---|
231 |
|
---|
232 | this._readyState = WebSocket.CLOSING;
|
---|
233 | this._sender.close(code, data, !this._isServer, (err) => {
|
---|
234 | //
|
---|
235 | // This error is handled by the `'error'` listener on the socket. We only
|
---|
236 | // want to know if the close frame has been sent here.
|
---|
237 | //
|
---|
238 | if (err) return;
|
---|
239 |
|
---|
240 | this._closeFrameSent = true;
|
---|
241 | if (this._closeFrameReceived) this._socket.end();
|
---|
242 | });
|
---|
243 |
|
---|
244 | //
|
---|
245 | // Specify a timeout for the closing handshake to complete.
|
---|
246 | //
|
---|
247 | this._closeTimer = setTimeout(
|
---|
248 | this._socket.destroy.bind(this._socket),
|
---|
249 | closeTimeout
|
---|
250 | );
|
---|
251 | }
|
---|
252 |
|
---|
253 | /**
|
---|
254 | * Send a ping.
|
---|
255 | *
|
---|
256 | * @param {*} [data] The data to send
|
---|
257 | * @param {Boolean} [mask] Indicates whether or not to mask `data`
|
---|
258 | * @param {Function} [cb] Callback which is executed when the ping is sent
|
---|
259 | * @public
|
---|
260 | */
|
---|
261 | ping(data, mask, cb) {
|
---|
262 | if (this.readyState === WebSocket.CONNECTING) {
|
---|
263 | throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
|
---|
264 | }
|
---|
265 |
|
---|
266 | if (typeof data === 'function') {
|
---|
267 | cb = data;
|
---|
268 | data = mask = undefined;
|
---|
269 | } else if (typeof mask === 'function') {
|
---|
270 | cb = mask;
|
---|
271 | mask = undefined;
|
---|
272 | }
|
---|
273 |
|
---|
274 | if (typeof data === 'number') data = data.toString();
|
---|
275 |
|
---|
276 | if (this.readyState !== WebSocket.OPEN) {
|
---|
277 | sendAfterClose(this, data, cb);
|
---|
278 | return;
|
---|
279 | }
|
---|
280 |
|
---|
281 | if (mask === undefined) mask = !this._isServer;
|
---|
282 | this._sender.ping(data || EMPTY_BUFFER, mask, cb);
|
---|
283 | }
|
---|
284 |
|
---|
285 | /**
|
---|
286 | * Send a pong.
|
---|
287 | *
|
---|
288 | * @param {*} [data] The data to send
|
---|
289 | * @param {Boolean} [mask] Indicates whether or not to mask `data`
|
---|
290 | * @param {Function} [cb] Callback which is executed when the pong is sent
|
---|
291 | * @public
|
---|
292 | */
|
---|
293 | pong(data, mask, cb) {
|
---|
294 | if (this.readyState === WebSocket.CONNECTING) {
|
---|
295 | throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
|
---|
296 | }
|
---|
297 |
|
---|
298 | if (typeof data === 'function') {
|
---|
299 | cb = data;
|
---|
300 | data = mask = undefined;
|
---|
301 | } else if (typeof mask === 'function') {
|
---|
302 | cb = mask;
|
---|
303 | mask = undefined;
|
---|
304 | }
|
---|
305 |
|
---|
306 | if (typeof data === 'number') data = data.toString();
|
---|
307 |
|
---|
308 | if (this.readyState !== WebSocket.OPEN) {
|
---|
309 | sendAfterClose(this, data, cb);
|
---|
310 | return;
|
---|
311 | }
|
---|
312 |
|
---|
313 | if (mask === undefined) mask = !this._isServer;
|
---|
314 | this._sender.pong(data || EMPTY_BUFFER, mask, cb);
|
---|
315 | }
|
---|
316 |
|
---|
317 | /**
|
---|
318 | * Send a data message.
|
---|
319 | *
|
---|
320 | * @param {*} data The message to send
|
---|
321 | * @param {Object} [options] Options object
|
---|
322 | * @param {Boolean} [options.compress] Specifies whether or not to compress
|
---|
323 | * `data`
|
---|
324 | * @param {Boolean} [options.binary] Specifies whether `data` is binary or
|
---|
325 | * text
|
---|
326 | * @param {Boolean} [options.fin=true] Specifies whether the fragment is the
|
---|
327 | * last one
|
---|
328 | * @param {Boolean} [options.mask] Specifies whether or not to mask `data`
|
---|
329 | * @param {Function} [cb] Callback which is executed when data is written out
|
---|
330 | * @public
|
---|
331 | */
|
---|
332 | send(data, options, cb) {
|
---|
333 | if (this.readyState === WebSocket.CONNECTING) {
|
---|
334 | throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
|
---|
335 | }
|
---|
336 |
|
---|
337 | if (typeof options === 'function') {
|
---|
338 | cb = options;
|
---|
339 | options = {};
|
---|
340 | }
|
---|
341 |
|
---|
342 | if (typeof data === 'number') data = data.toString();
|
---|
343 |
|
---|
344 | if (this.readyState !== WebSocket.OPEN) {
|
---|
345 | sendAfterClose(this, data, cb);
|
---|
346 | return;
|
---|
347 | }
|
---|
348 |
|
---|
349 | const opts = {
|
---|
350 | binary: typeof data !== 'string',
|
---|
351 | mask: !this._isServer,
|
---|
352 | compress: true,
|
---|
353 | fin: true,
|
---|
354 | ...options
|
---|
355 | };
|
---|
356 |
|
---|
357 | if (!this._extensions[PerMessageDeflate.extensionName]) {
|
---|
358 | opts.compress = false;
|
---|
359 | }
|
---|
360 |
|
---|
361 | this._sender.send(data || EMPTY_BUFFER, opts, cb);
|
---|
362 | }
|
---|
363 |
|
---|
364 | /**
|
---|
365 | * Forcibly close the connection.
|
---|
366 | *
|
---|
367 | * @public
|
---|
368 | */
|
---|
369 | terminate() {
|
---|
370 | if (this.readyState === WebSocket.CLOSED) return;
|
---|
371 | if (this.readyState === WebSocket.CONNECTING) {
|
---|
372 | const msg = 'WebSocket was closed before the connection was established';
|
---|
373 | return abortHandshake(this, this._req, msg);
|
---|
374 | }
|
---|
375 |
|
---|
376 | if (this._socket) {
|
---|
377 | this._readyState = WebSocket.CLOSING;
|
---|
378 | this._socket.destroy();
|
---|
379 | }
|
---|
380 | }
|
---|
381 | }
|
---|
382 |
|
---|
383 | readyStates.forEach((readyState, i) => {
|
---|
384 | const descriptor = { enumerable: true, value: i };
|
---|
385 |
|
---|
386 | Object.defineProperty(WebSocket.prototype, readyState, descriptor);
|
---|
387 | Object.defineProperty(WebSocket, readyState, descriptor);
|
---|
388 | });
|
---|
389 |
|
---|
390 | [
|
---|
391 | 'binaryType',
|
---|
392 | 'bufferedAmount',
|
---|
393 | 'extensions',
|
---|
394 | 'protocol',
|
---|
395 | 'readyState',
|
---|
396 | 'url'
|
---|
397 | ].forEach((property) => {
|
---|
398 | Object.defineProperty(WebSocket.prototype, property, { enumerable: true });
|
---|
399 | });
|
---|
400 |
|
---|
401 | //
|
---|
402 | // Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes.
|
---|
403 | // See https://html.spec.whatwg.org/multipage/comms.html#the-websocket-interface
|
---|
404 | //
|
---|
405 | ['open', 'error', 'close', 'message'].forEach((method) => {
|
---|
406 | Object.defineProperty(WebSocket.prototype, `on${method}`, {
|
---|
407 | configurable: true,
|
---|
408 | enumerable: true,
|
---|
409 | /**
|
---|
410 | * Return the listener of the event.
|
---|
411 | *
|
---|
412 | * @return {(Function|undefined)} The event listener or `undefined`
|
---|
413 | * @public
|
---|
414 | */
|
---|
415 | get() {
|
---|
416 | const listeners = this.listeners(method);
|
---|
417 | for (let i = 0; i < listeners.length; i++) {
|
---|
418 | if (listeners[i]._listener) return listeners[i]._listener;
|
---|
419 | }
|
---|
420 |
|
---|
421 | return undefined;
|
---|
422 | },
|
---|
423 | /**
|
---|
424 | * Add a listener for the event.
|
---|
425 | *
|
---|
426 | * @param {Function} listener The listener to add
|
---|
427 | * @public
|
---|
428 | */
|
---|
429 | set(listener) {
|
---|
430 | const listeners = this.listeners(method);
|
---|
431 | for (let i = 0; i < listeners.length; i++) {
|
---|
432 | //
|
---|
433 | // Remove only the listeners added via `addEventListener`.
|
---|
434 | //
|
---|
435 | if (listeners[i]._listener) this.removeListener(method, listeners[i]);
|
---|
436 | }
|
---|
437 | this.addEventListener(method, listener);
|
---|
438 | }
|
---|
439 | });
|
---|
440 | });
|
---|
441 |
|
---|
442 | WebSocket.prototype.addEventListener = addEventListener;
|
---|
443 | WebSocket.prototype.removeEventListener = removeEventListener;
|
---|
444 |
|
---|
445 | module.exports = WebSocket;
|
---|
446 |
|
---|
447 | /**
|
---|
448 | * Initialize a WebSocket client.
|
---|
449 | *
|
---|
450 | * @param {WebSocket} websocket The client to initialize
|
---|
451 | * @param {(String|url.URL)} address The URL to which to connect
|
---|
452 | * @param {String} [protocols] The subprotocols
|
---|
453 | * @param {Object} [options] Connection options
|
---|
454 | * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable
|
---|
455 | * permessage-deflate
|
---|
456 | * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
|
---|
457 | * handshake request
|
---|
458 | * @param {Number} [options.protocolVersion=13] Value of the
|
---|
459 | * `Sec-WebSocket-Version` header
|
---|
460 | * @param {String} [options.origin] Value of the `Origin` or
|
---|
461 | * `Sec-WebSocket-Origin` header
|
---|
462 | * @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
---|
463 | * size
|
---|
464 | * @param {Boolean} [options.followRedirects=false] Whether or not to follow
|
---|
465 | * redirects
|
---|
466 | * @param {Number} [options.maxRedirects=10] The maximum number of redirects
|
---|
467 | * allowed
|
---|
468 | * @private
|
---|
469 | */
|
---|
470 | function initAsClient(websocket, address, protocols, options) {
|
---|
471 | const opts = {
|
---|
472 | protocolVersion: protocolVersions[1],
|
---|
473 | maxPayload: 100 * 1024 * 1024,
|
---|
474 | perMessageDeflate: true,
|
---|
475 | followRedirects: false,
|
---|
476 | maxRedirects: 10,
|
---|
477 | ...options,
|
---|
478 | createConnection: undefined,
|
---|
479 | socketPath: undefined,
|
---|
480 | hostname: undefined,
|
---|
481 | protocol: undefined,
|
---|
482 | timeout: undefined,
|
---|
483 | method: undefined,
|
---|
484 | host: undefined,
|
---|
485 | path: undefined,
|
---|
486 | port: undefined
|
---|
487 | };
|
---|
488 |
|
---|
489 | if (!protocolVersions.includes(opts.protocolVersion)) {
|
---|
490 | throw new RangeError(
|
---|
491 | `Unsupported protocol version: ${opts.protocolVersion} ` +
|
---|
492 | `(supported versions: ${protocolVersions.join(', ')})`
|
---|
493 | );
|
---|
494 | }
|
---|
495 |
|
---|
496 | let parsedUrl;
|
---|
497 |
|
---|
498 | if (address instanceof URL) {
|
---|
499 | parsedUrl = address;
|
---|
500 | websocket._url = address.href;
|
---|
501 | } else {
|
---|
502 | parsedUrl = new URL(address);
|
---|
503 | websocket._url = address;
|
---|
504 | }
|
---|
505 |
|
---|
506 | const isUnixSocket = parsedUrl.protocol === 'ws+unix:';
|
---|
507 |
|
---|
508 | if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) {
|
---|
509 | throw new Error(`Invalid URL: ${websocket.url}`);
|
---|
510 | }
|
---|
511 |
|
---|
512 | const isSecure =
|
---|
513 | parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:';
|
---|
514 | const defaultPort = isSecure ? 443 : 80;
|
---|
515 | const key = randomBytes(16).toString('base64');
|
---|
516 | const get = isSecure ? https.get : http.get;
|
---|
517 | let perMessageDeflate;
|
---|
518 |
|
---|
519 | opts.createConnection = isSecure ? tlsConnect : netConnect;
|
---|
520 | opts.defaultPort = opts.defaultPort || defaultPort;
|
---|
521 | opts.port = parsedUrl.port || defaultPort;
|
---|
522 | opts.host = parsedUrl.hostname.startsWith('[')
|
---|
523 | ? parsedUrl.hostname.slice(1, -1)
|
---|
524 | : parsedUrl.hostname;
|
---|
525 | opts.headers = {
|
---|
526 | 'Sec-WebSocket-Version': opts.protocolVersion,
|
---|
527 | 'Sec-WebSocket-Key': key,
|
---|
528 | Connection: 'Upgrade',
|
---|
529 | Upgrade: 'websocket',
|
---|
530 | ...opts.headers
|
---|
531 | };
|
---|
532 | opts.path = parsedUrl.pathname + parsedUrl.search;
|
---|
533 | opts.timeout = opts.handshakeTimeout;
|
---|
534 |
|
---|
535 | if (opts.perMessageDeflate) {
|
---|
536 | perMessageDeflate = new PerMessageDeflate(
|
---|
537 | opts.perMessageDeflate !== true ? opts.perMessageDeflate : {},
|
---|
538 | false,
|
---|
539 | opts.maxPayload
|
---|
540 | );
|
---|
541 | opts.headers['Sec-WebSocket-Extensions'] = format({
|
---|
542 | [PerMessageDeflate.extensionName]: perMessageDeflate.offer()
|
---|
543 | });
|
---|
544 | }
|
---|
545 | if (protocols) {
|
---|
546 | opts.headers['Sec-WebSocket-Protocol'] = protocols;
|
---|
547 | }
|
---|
548 | if (opts.origin) {
|
---|
549 | if (opts.protocolVersion < 13) {
|
---|
550 | opts.headers['Sec-WebSocket-Origin'] = opts.origin;
|
---|
551 | } else {
|
---|
552 | opts.headers.Origin = opts.origin;
|
---|
553 | }
|
---|
554 | }
|
---|
555 | if (parsedUrl.username || parsedUrl.password) {
|
---|
556 | opts.auth = `${parsedUrl.username}:${parsedUrl.password}`;
|
---|
557 | }
|
---|
558 |
|
---|
559 | if (isUnixSocket) {
|
---|
560 | const parts = opts.path.split(':');
|
---|
561 |
|
---|
562 | opts.socketPath = parts[0];
|
---|
563 | opts.path = parts[1];
|
---|
564 | }
|
---|
565 |
|
---|
566 | let req = (websocket._req = get(opts));
|
---|
567 |
|
---|
568 | if (opts.timeout) {
|
---|
569 | req.on('timeout', () => {
|
---|
570 | abortHandshake(websocket, req, 'Opening handshake has timed out');
|
---|
571 | });
|
---|
572 | }
|
---|
573 |
|
---|
574 | req.on('error', (err) => {
|
---|
575 | if (req === null || req.aborted) return;
|
---|
576 |
|
---|
577 | req = websocket._req = null;
|
---|
578 | websocket._readyState = WebSocket.CLOSING;
|
---|
579 | websocket.emit('error', err);
|
---|
580 | websocket.emitClose();
|
---|
581 | });
|
---|
582 |
|
---|
583 | req.on('response', (res) => {
|
---|
584 | const location = res.headers.location;
|
---|
585 | const statusCode = res.statusCode;
|
---|
586 |
|
---|
587 | if (
|
---|
588 | location &&
|
---|
589 | opts.followRedirects &&
|
---|
590 | statusCode >= 300 &&
|
---|
591 | statusCode < 400
|
---|
592 | ) {
|
---|
593 | if (++websocket._redirects > opts.maxRedirects) {
|
---|
594 | abortHandshake(websocket, req, 'Maximum redirects exceeded');
|
---|
595 | return;
|
---|
596 | }
|
---|
597 |
|
---|
598 | req.abort();
|
---|
599 |
|
---|
600 | const addr = new URL(location, address);
|
---|
601 |
|
---|
602 | initAsClient(websocket, addr, protocols, options);
|
---|
603 | } else if (!websocket.emit('unexpected-response', req, res)) {
|
---|
604 | abortHandshake(
|
---|
605 | websocket,
|
---|
606 | req,
|
---|
607 | `Unexpected server response: ${res.statusCode}`
|
---|
608 | );
|
---|
609 | }
|
---|
610 | });
|
---|
611 |
|
---|
612 | req.on('upgrade', (res, socket, head) => {
|
---|
613 | websocket.emit('upgrade', res);
|
---|
614 |
|
---|
615 | //
|
---|
616 | // The user may have closed the connection from a listener of the `upgrade`
|
---|
617 | // event.
|
---|
618 | //
|
---|
619 | if (websocket.readyState !== WebSocket.CONNECTING) return;
|
---|
620 |
|
---|
621 | req = websocket._req = null;
|
---|
622 |
|
---|
623 | const digest = createHash('sha1')
|
---|
624 | .update(key + GUID)
|
---|
625 | .digest('base64');
|
---|
626 |
|
---|
627 | if (res.headers['sec-websocket-accept'] !== digest) {
|
---|
628 | abortHandshake(websocket, socket, 'Invalid Sec-WebSocket-Accept header');
|
---|
629 | return;
|
---|
630 | }
|
---|
631 |
|
---|
632 | const serverProt = res.headers['sec-websocket-protocol'];
|
---|
633 | const protList = (protocols || '').split(/, */);
|
---|
634 | let protError;
|
---|
635 |
|
---|
636 | if (!protocols && serverProt) {
|
---|
637 | protError = 'Server sent a subprotocol but none was requested';
|
---|
638 | } else if (protocols && !serverProt) {
|
---|
639 | protError = 'Server sent no subprotocol';
|
---|
640 | } else if (serverProt && !protList.includes(serverProt)) {
|
---|
641 | protError = 'Server sent an invalid subprotocol';
|
---|
642 | }
|
---|
643 |
|
---|
644 | if (protError) {
|
---|
645 | abortHandshake(websocket, socket, protError);
|
---|
646 | return;
|
---|
647 | }
|
---|
648 |
|
---|
649 | if (serverProt) websocket._protocol = serverProt;
|
---|
650 |
|
---|
651 | if (perMessageDeflate) {
|
---|
652 | try {
|
---|
653 | const extensions = parse(res.headers['sec-websocket-extensions']);
|
---|
654 |
|
---|
655 | if (extensions[PerMessageDeflate.extensionName]) {
|
---|
656 | perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]);
|
---|
657 | websocket._extensions[PerMessageDeflate.extensionName] =
|
---|
658 | perMessageDeflate;
|
---|
659 | }
|
---|
660 | } catch (err) {
|
---|
661 | abortHandshake(
|
---|
662 | websocket,
|
---|
663 | socket,
|
---|
664 | 'Invalid Sec-WebSocket-Extensions header'
|
---|
665 | );
|
---|
666 | return;
|
---|
667 | }
|
---|
668 | }
|
---|
669 |
|
---|
670 | websocket.setSocket(socket, head, opts.maxPayload);
|
---|
671 | });
|
---|
672 | }
|
---|
673 |
|
---|
674 | /**
|
---|
675 | * Create a `net.Socket` and initiate a connection.
|
---|
676 | *
|
---|
677 | * @param {Object} options Connection options
|
---|
678 | * @return {net.Socket} The newly created socket used to start the connection
|
---|
679 | * @private
|
---|
680 | */
|
---|
681 | function netConnect(options) {
|
---|
682 | options.path = options.socketPath;
|
---|
683 | return net.connect(options);
|
---|
684 | }
|
---|
685 |
|
---|
686 | /**
|
---|
687 | * Create a `tls.TLSSocket` and initiate a connection.
|
---|
688 | *
|
---|
689 | * @param {Object} options Connection options
|
---|
690 | * @return {tls.TLSSocket} The newly created socket used to start the connection
|
---|
691 | * @private
|
---|
692 | */
|
---|
693 | function tlsConnect(options) {
|
---|
694 | options.path = undefined;
|
---|
695 |
|
---|
696 | if (!options.servername && options.servername !== '') {
|
---|
697 | options.servername = net.isIP(options.host) ? '' : options.host;
|
---|
698 | }
|
---|
699 |
|
---|
700 | return tls.connect(options);
|
---|
701 | }
|
---|
702 |
|
---|
703 | /**
|
---|
704 | * Abort the handshake and emit an error.
|
---|
705 | *
|
---|
706 | * @param {WebSocket} websocket The WebSocket instance
|
---|
707 | * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the
|
---|
708 | * socket to destroy
|
---|
709 | * @param {String} message The error message
|
---|
710 | * @private
|
---|
711 | */
|
---|
712 | function abortHandshake(websocket, stream, message) {
|
---|
713 | websocket._readyState = WebSocket.CLOSING;
|
---|
714 |
|
---|
715 | const err = new Error(message);
|
---|
716 | Error.captureStackTrace(err, abortHandshake);
|
---|
717 |
|
---|
718 | if (stream.setHeader) {
|
---|
719 | stream.abort();
|
---|
720 |
|
---|
721 | if (stream.socket && !stream.socket.destroyed) {
|
---|
722 | //
|
---|
723 | // On Node.js >= 14.3.0 `request.abort()` does not destroy the socket if
|
---|
724 | // called after the request completed. See
|
---|
725 | // https://github.com/websockets/ws/issues/1869.
|
---|
726 | //
|
---|
727 | stream.socket.destroy();
|
---|
728 | }
|
---|
729 |
|
---|
730 | stream.once('abort', websocket.emitClose.bind(websocket));
|
---|
731 | websocket.emit('error', err);
|
---|
732 | } else {
|
---|
733 | stream.destroy(err);
|
---|
734 | stream.once('error', websocket.emit.bind(websocket, 'error'));
|
---|
735 | stream.once('close', websocket.emitClose.bind(websocket));
|
---|
736 | }
|
---|
737 | }
|
---|
738 |
|
---|
739 | /**
|
---|
740 | * Handle cases where the `ping()`, `pong()`, or `send()` methods are called
|
---|
741 | * when the `readyState` attribute is `CLOSING` or `CLOSED`.
|
---|
742 | *
|
---|
743 | * @param {WebSocket} websocket The WebSocket instance
|
---|
744 | * @param {*} [data] The data to send
|
---|
745 | * @param {Function} [cb] Callback
|
---|
746 | * @private
|
---|
747 | */
|
---|
748 | function sendAfterClose(websocket, data, cb) {
|
---|
749 | if (data) {
|
---|
750 | const length = toBuffer(data).length;
|
---|
751 |
|
---|
752 | //
|
---|
753 | // The `_bufferedAmount` property is used only when the peer is a client and
|
---|
754 | // the opening handshake fails. Under these circumstances, in fact, the
|
---|
755 | // `setSocket()` method is not called, so the `_socket` and `_sender`
|
---|
756 | // properties are set to `null`.
|
---|
757 | //
|
---|
758 | if (websocket._socket) websocket._sender._bufferedBytes += length;
|
---|
759 | else websocket._bufferedAmount += length;
|
---|
760 | }
|
---|
761 |
|
---|
762 | if (cb) {
|
---|
763 | const err = new Error(
|
---|
764 | `WebSocket is not open: readyState ${websocket.readyState} ` +
|
---|
765 | `(${readyStates[websocket.readyState]})`
|
---|
766 | );
|
---|
767 | cb(err);
|
---|
768 | }
|
---|
769 | }
|
---|
770 |
|
---|
771 | /**
|
---|
772 | * The listener of the `Receiver` `'conclude'` event.
|
---|
773 | *
|
---|
774 | * @param {Number} code The status code
|
---|
775 | * @param {String} reason The reason for closing
|
---|
776 | * @private
|
---|
777 | */
|
---|
778 | function receiverOnConclude(code, reason) {
|
---|
779 | const websocket = this[kWebSocket];
|
---|
780 |
|
---|
781 | websocket._socket.removeListener('data', socketOnData);
|
---|
782 | websocket._socket.resume();
|
---|
783 |
|
---|
784 | websocket._closeFrameReceived = true;
|
---|
785 | websocket._closeMessage = reason;
|
---|
786 | websocket._closeCode = code;
|
---|
787 |
|
---|
788 | if (code === 1005) websocket.close();
|
---|
789 | else websocket.close(code, reason);
|
---|
790 | }
|
---|
791 |
|
---|
792 | /**
|
---|
793 | * The listener of the `Receiver` `'drain'` event.
|
---|
794 | *
|
---|
795 | * @private
|
---|
796 | */
|
---|
797 | function receiverOnDrain() {
|
---|
798 | this[kWebSocket]._socket.resume();
|
---|
799 | }
|
---|
800 |
|
---|
801 | /**
|
---|
802 | * The listener of the `Receiver` `'error'` event.
|
---|
803 | *
|
---|
804 | * @param {(RangeError|Error)} err The emitted error
|
---|
805 | * @private
|
---|
806 | */
|
---|
807 | function receiverOnError(err) {
|
---|
808 | const websocket = this[kWebSocket];
|
---|
809 |
|
---|
810 | websocket._socket.removeListener('data', socketOnData);
|
---|
811 |
|
---|
812 | websocket._readyState = WebSocket.CLOSING;
|
---|
813 | websocket._closeCode = err[kStatusCode];
|
---|
814 | websocket.emit('error', err);
|
---|
815 | websocket._socket.destroy();
|
---|
816 | }
|
---|
817 |
|
---|
818 | /**
|
---|
819 | * The listener of the `Receiver` `'finish'` event.
|
---|
820 | *
|
---|
821 | * @private
|
---|
822 | */
|
---|
823 | function receiverOnFinish() {
|
---|
824 | this[kWebSocket].emitClose();
|
---|
825 | }
|
---|
826 |
|
---|
827 | /**
|
---|
828 | * The listener of the `Receiver` `'message'` event.
|
---|
829 | *
|
---|
830 | * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message
|
---|
831 | * @private
|
---|
832 | */
|
---|
833 | function receiverOnMessage(data) {
|
---|
834 | this[kWebSocket].emit('message', data);
|
---|
835 | }
|
---|
836 |
|
---|
837 | /**
|
---|
838 | * The listener of the `Receiver` `'ping'` event.
|
---|
839 | *
|
---|
840 | * @param {Buffer} data The data included in the ping frame
|
---|
841 | * @private
|
---|
842 | */
|
---|
843 | function receiverOnPing(data) {
|
---|
844 | const websocket = this[kWebSocket];
|
---|
845 |
|
---|
846 | websocket.pong(data, !websocket._isServer, NOOP);
|
---|
847 | websocket.emit('ping', data);
|
---|
848 | }
|
---|
849 |
|
---|
850 | /**
|
---|
851 | * The listener of the `Receiver` `'pong'` event.
|
---|
852 | *
|
---|
853 | * @param {Buffer} data The data included in the pong frame
|
---|
854 | * @private
|
---|
855 | */
|
---|
856 | function receiverOnPong(data) {
|
---|
857 | this[kWebSocket].emit('pong', data);
|
---|
858 | }
|
---|
859 |
|
---|
860 | /**
|
---|
861 | * The listener of the `net.Socket` `'close'` event.
|
---|
862 | *
|
---|
863 | * @private
|
---|
864 | */
|
---|
865 | function socketOnClose() {
|
---|
866 | const websocket = this[kWebSocket];
|
---|
867 |
|
---|
868 | this.removeListener('close', socketOnClose);
|
---|
869 | this.removeListener('end', socketOnEnd);
|
---|
870 |
|
---|
871 | websocket._readyState = WebSocket.CLOSING;
|
---|
872 |
|
---|
873 | //
|
---|
874 | // The close frame might not have been received or the `'end'` event emitted,
|
---|
875 | // for example, if the socket was destroyed due to an error. Ensure that the
|
---|
876 | // `receiver` stream is closed after writing any remaining buffered data to
|
---|
877 | // it. If the readable side of the socket is in flowing mode then there is no
|
---|
878 | // buffered data as everything has been already written and `readable.read()`
|
---|
879 | // will return `null`. If instead, the socket is paused, any possible buffered
|
---|
880 | // data will be read as a single chunk and emitted synchronously in a single
|
---|
881 | // `'data'` event.
|
---|
882 | //
|
---|
883 | websocket._socket.read();
|
---|
884 | websocket._receiver.end();
|
---|
885 |
|
---|
886 | this.removeListener('data', socketOnData);
|
---|
887 | this[kWebSocket] = undefined;
|
---|
888 |
|
---|
889 | clearTimeout(websocket._closeTimer);
|
---|
890 |
|
---|
891 | if (
|
---|
892 | websocket._receiver._writableState.finished ||
|
---|
893 | websocket._receiver._writableState.errorEmitted
|
---|
894 | ) {
|
---|
895 | websocket.emitClose();
|
---|
896 | } else {
|
---|
897 | websocket._receiver.on('error', receiverOnFinish);
|
---|
898 | websocket._receiver.on('finish', receiverOnFinish);
|
---|
899 | }
|
---|
900 | }
|
---|
901 |
|
---|
902 | /**
|
---|
903 | * The listener of the `net.Socket` `'data'` event.
|
---|
904 | *
|
---|
905 | * @param {Buffer} chunk A chunk of data
|
---|
906 | * @private
|
---|
907 | */
|
---|
908 | function socketOnData(chunk) {
|
---|
909 | if (!this[kWebSocket]._receiver.write(chunk)) {
|
---|
910 | this.pause();
|
---|
911 | }
|
---|
912 | }
|
---|
913 |
|
---|
914 | /**
|
---|
915 | * The listener of the `net.Socket` `'end'` event.
|
---|
916 | *
|
---|
917 | * @private
|
---|
918 | */
|
---|
919 | function socketOnEnd() {
|
---|
920 | const websocket = this[kWebSocket];
|
---|
921 |
|
---|
922 | websocket._readyState = WebSocket.CLOSING;
|
---|
923 | websocket._receiver.end();
|
---|
924 | this.end();
|
---|
925 | }
|
---|
926 |
|
---|
927 | /**
|
---|
928 | * The listener of the `net.Socket` `'error'` event.
|
---|
929 | *
|
---|
930 | * @private
|
---|
931 | */
|
---|
932 | function socketOnError() {
|
---|
933 | const websocket = this[kWebSocket];
|
---|
934 |
|
---|
935 | this.removeListener('error', socketOnError);
|
---|
936 | this.on('error', NOOP);
|
---|
937 |
|
---|
938 | if (websocket) {
|
---|
939 | websocket._readyState = WebSocket.CLOSING;
|
---|
940 | this.destroy();
|
---|
941 | }
|
---|
942 | }
|
---|