• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

websockets / ws / 7185456451

12 Dec 2023 06:02PM UTC coverage: 99.935% (+0.001%) from 99.934%
7185456451

push

github

lpinca
[dist] 8.15.1

1064 of 1069 branches covered (0.0%)

1548 of 1549 relevant lines covered (99.94%)

26575.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.39
/lib/websocket-server.js
1
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$" }] */
2

3
'use strict';
4

5
const EventEmitter = require('events');
27✔
6
const http = require('http');
27✔
7
const { Duplex } = require('stream');
27✔
8
const { createHash } = require('crypto');
27✔
9

10
const extension = require('./extension');
27✔
11
const PerMessageDeflate = require('./permessage-deflate');
27✔
12
const subprotocol = require('./subprotocol');
27✔
13
const WebSocket = require('./websocket');
27✔
14
const { GUID, kWebSocket } = require('./constants');
27✔
15

16
const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
27✔
17

18
const RUNNING = 0;
27✔
19
const CLOSING = 1;
27✔
20
const CLOSED = 2;
27✔
21

22
/**
23
 * Class representing a WebSocket server.
24
 *
25
 * @extends EventEmitter
26
 */
27
class WebSocketServer extends EventEmitter {
28
  /**
29
   * Create a `WebSocketServer` instance.
30
   *
31
   * @param {Object} options Configuration options
32
   * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether
33
   *     any of the `'message'`, `'ping'`, and `'pong'` events can be emitted
34
   *     multiple times in the same tick
35
   * @param {Number} [options.backlog=511] The maximum length of the queue of
36
   *     pending connections
37
   * @param {Boolean} [options.clientTracking=true] Specifies whether or not to
38
   *     track clients
39
   * @param {Function} [options.handleProtocols] A hook to handle protocols
40
   * @param {String} [options.host] The hostname where to bind the server
41
   * @param {Number} [options.maxPayload=104857600] The maximum allowed message
42
   *     size
43
   * @param {Boolean} [options.noServer=false] Enable no server mode
44
   * @param {String} [options.path] Accept only connections matching this path
45
   * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable
46
   *     permessage-deflate
47
   * @param {Number} [options.port] The port where to bind the server
48
   * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S
49
   *     server to use
50
   * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
51
   *     not to skip UTF-8 validation for text and close messages
52
   * @param {Function} [options.verifyClient] A hook to reject connections
53
   * @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket`
54
   *     class to use. It must be the `WebSocket` class or class that extends it
55
   * @param {Function} [callback] A listener for the `listening` event
56
   */
57
  constructor(options, callback) {
58
    super();
4,875✔
59

60
    options = {
4,875✔
61
      allowSynchronousEvents: false,
62
      maxPayload: 100 * 1024 * 1024,
63
      skipUTF8Validation: false,
64
      perMessageDeflate: false,
65
      handleProtocols: null,
66
      clientTracking: true,
67
      verifyClient: null,
68
      noServer: false,
69
      backlog: null, // use default (511 as implemented in net.js)
70
      server: null,
71
      host: null,
72
      path: null,
73
      port: null,
74
      WebSocket,
75
      ...options
76
    };
77

78
    if (
4,875✔
79
      (options.port == null && !options.server && !options.noServer) ||
24,132✔
80
      (options.port != null && (options.server || options.noServer)) ||
81
      (options.server && options.noServer)
82
    ) {
83
      throw new TypeError(
162✔
84
        'One and only one of the "port", "server", or "noServer" options ' +
85
          'must be specified'
86
      );
87
    }
88

89
    if (options.port != null) {
4,713✔
90
      this._server = http.createServer((req, res) => {
3,822✔
91
        const body = http.STATUS_CODES[426];
27✔
92

93
        res.writeHead(426, {
27✔
94
          'Content-Length': body.length,
95
          'Content-Type': 'text/plain'
96
        });
97
        res.end(body);
27✔
98
      });
99
      this._server.listen(
3,822✔
100
        options.port,
101
        options.host,
102
        options.backlog,
103
        callback
104
      );
105
    } else if (options.server) {
891✔
106
      this._server = options.server;
540✔
107
    }
108

109
    if (this._server) {
4,713✔
110
      const emitConnection = this.emit.bind(this, 'connection');
4,362✔
111

112
      this._removeListeners = addListeners(this._server, {
4,362✔
113
        listening: this.emit.bind(this, 'listening'),
114
        error: this.emit.bind(this, 'error'),
115
        upgrade: (req, socket, head) => {
116
          this.handleUpgrade(req, socket, head, emitConnection);
3,957✔
117
        }
118
      });
119
    }
120

121
    if (options.perMessageDeflate === true) options.perMessageDeflate = {};
4,713✔
122
    if (options.clientTracking) {
4,713✔
123
      this.clients = new Set();
4,659✔
124
      this._shouldEmitClose = false;
4,659✔
125
    }
126

127
    this.options = options;
4,713✔
128
    this._state = RUNNING;
4,713✔
129
  }
130

131
  /**
132
   * Returns the bound address, the address family name, and port of the server
133
   * as reported by the operating system if listening on an IP socket.
134
   * If the server is listening on a pipe or UNIX domain socket, the name is
135
   * returned as a string.
136
   *
137
   * @return {(Object|String|null)} The address of the server
138
   * @public
139
   */
140
  address() {
141
    if (this.options.noServer) {
3,687✔
142
      throw new Error('The server is operating in "noServer" mode');
27✔
143
    }
144

145
    if (!this._server) return null;
3,660✔
146
    return this._server.address();
3,633✔
147
  }
148

149
  /**
150
   * Stop the server from accepting new connections and emit the `'close'` event
151
   * when all existing connections are closed.
152
   *
153
   * @param {Function} [cb] A one-time listener for the `'close'` event
154
   * @public
155
   */
156
  close(cb) {
157
    if (this._state === CLOSED) {
4,254✔
158
      if (cb) {
54✔
159
        this.once('close', () => {
27✔
160
          cb(new Error('The server is not running'));
27✔
161
        });
162
      }
163

164
      process.nextTick(emitClose, this);
54✔
165
      return;
54✔
166
    }
167

168
    if (cb) this.once('close', cb);
4,200✔
169

170
    if (this._state === CLOSING) return;
4,200✔
171
    this._state = CLOSING;
4,119✔
172

173
    if (this.options.noServer || this.options.server) {
4,119✔
174
      if (this._server) {
324✔
175
        this._removeListeners();
216✔
176
        this._removeListeners = this._server = null;
216✔
177
      }
178

179
      if (this.clients) {
324✔
180
        if (!this.clients.size) {
297✔
181
          process.nextTick(emitClose, this);
183✔
182
        } else {
183
          this._shouldEmitClose = true;
114✔
184
        }
185
      } else {
186
        process.nextTick(emitClose, this);
27✔
187
      }
188
    } else {
189
      const server = this._server;
3,795✔
190

191
      this._removeListeners();
3,795✔
192
      this._removeListeners = this._server = null;
3,795✔
193

194
      //
195
      // The HTTP/S server was created internally. Close it, and rely on its
196
      // `'close'` event.
197
      //
198
      server.close(() => {
3,795✔
199
        emitClose(this);
3,795✔
200
      });
201
    }
202
  }
203

204
  /**
205
   * See if a given request should be handled by this server instance.
206
   *
207
   * @param {http.IncomingMessage} req Request object to inspect
208
   * @return {Boolean} `true` if the request is valid, else `false`
209
   * @public
210
   */
211
  shouldHandle(req) {
212
    if (this.options.path) {
4,065✔
213
      const index = req.url.indexOf('?');
189✔
214
      const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
189✔
215

216
      if (pathname !== this.options.path) return false;
189✔
217
    }
218

219
    return true;
4,011✔
220
  }
221

222
  /**
223
   * Handle a HTTP Upgrade request.
224
   *
225
   * @param {http.IncomingMessage} req The request object
226
   * @param {Duplex} socket The network socket between the server and client
227
   * @param {Buffer} head The first packet of the upgraded stream
228
   * @param {Function} cb Callback
229
   * @public
230
   */
231
  handleUpgrade(req, socket, head, cb) {
232
    socket.on('error', socketOnError);
4,200✔
233

234
    const key = req.headers['sec-websocket-key'];
4,200✔
235
    const version = +req.headers['sec-websocket-version'];
4,200✔
236

237
    if (req.method !== 'GET') {
4,200✔
238
      const message = 'Invalid HTTP method';
54✔
239
      abortHandshakeOrEmitwsClientError(this, req, socket, 405, message);
54✔
240
      return;
54✔
241
    }
242

243
    if (req.headers.upgrade.toLowerCase() !== 'websocket') {
4,146✔
244
      const message = 'Invalid Upgrade header';
27✔
245
      abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
27✔
246
      return;
27✔
247
    }
248

249
    if (!key || !keyRegex.test(key)) {
4,119✔
250
      const message = 'Missing or invalid Sec-WebSocket-Key header';
81✔
251
      abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
81✔
252
      return;
81✔
253
    }
254

255
    if (version !== 8 && version !== 13) {
4,038✔
256
      const message = 'Missing or invalid Sec-WebSocket-Version header';
54✔
257
      abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
54✔
258
      return;
54✔
259
    }
260

261
    if (!this.shouldHandle(req)) {
3,984✔
262
      abortHandshake(socket, 400);
27✔
263
      return;
27✔
264
    }
265

266
    const secWebSocketProtocol = req.headers['sec-websocket-protocol'];
3,957✔
267
    let protocols = new Set();
3,957✔
268

269
    if (secWebSocketProtocol !== undefined) {
3,957✔
270
      try {
135✔
271
        protocols = subprotocol.parse(secWebSocketProtocol);
135✔
272
      } catch (err) {
273
        const message = 'Invalid Sec-WebSocket-Protocol header';
27✔
274
        abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
27✔
275
        return;
27✔
276
      }
277
    }
278

279
    const secWebSocketExtensions = req.headers['sec-websocket-extensions'];
3,930✔
280
    const extensions = {};
3,930✔
281

282
    if (
3,930✔
283
      this.options.perMessageDeflate &&
4,497✔
284
      secWebSocketExtensions !== undefined
285
    ) {
286
      const perMessageDeflate = new PerMessageDeflate(
567✔
287
        this.options.perMessageDeflate,
288
        true,
289
        this.options.maxPayload
290
      );
291

292
      try {
567✔
293
        const offers = extension.parse(secWebSocketExtensions);
567✔
294

295
        if (offers[PerMessageDeflate.extensionName]) {
567✔
296
          perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
540✔
297
          extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
513✔
298
        }
299
      } catch (err) {
300
        const message =
301
          'Invalid or unacceptable Sec-WebSocket-Extensions header';
27✔
302
        abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
27✔
303
        return;
27✔
304
      }
305
    }
306

307
    //
308
    // Optionally call external client verification handler.
309
    //
310
    if (this.options.verifyClient) {
3,903✔
311
      const info = {
243✔
312
        origin:
313
          req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
243✔
314
        secure: !!(req.socket.authorized || req.socket.encrypted),
486✔
315
        req
316
      };
317

318
      if (this.options.verifyClient.length === 2) {
243✔
319
        this.options.verifyClient(info, (verified, code, message, headers) => {
189✔
320
          if (!verified) {
189✔
321
            return abortHandshake(socket, code || 401, message, headers);
81✔
322
          }
323

324
          this.completeUpgrade(
108✔
325
            extensions,
326
            key,
327
            protocols,
328
            req,
329
            socket,
330
            head,
331
            cb
332
          );
333
        });
334
        return;
189✔
335
      }
336

337
      if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
54✔
338
    }
339

340
    this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
3,687✔
341
  }
342

343
  /**
344
   * Upgrade the connection to WebSocket.
345
   *
346
   * @param {Object} extensions The accepted extensions
347
   * @param {String} key The value of the `Sec-WebSocket-Key` header
348
   * @param {Set} protocols The subprotocols
349
   * @param {http.IncomingMessage} req The request object
350
   * @param {Duplex} socket The network socket between the server and client
351
   * @param {Buffer} head The first packet of the upgraded stream
352
   * @param {Function} cb Callback
353
   * @throws {Error} If called more than once with the same socket
354
   * @private
355
   */
356
  completeUpgrade(extensions, key, protocols, req, socket, head, cb) {
357
    //
358
    // Destroy the socket if the client has already sent a FIN packet.
359
    //
360
    if (!socket.readable || !socket.writable) return socket.destroy();
3,795✔
361

362
    if (socket[kWebSocket]) {
3,714✔
363
      throw new Error(
27✔
364
        'server.handleUpgrade() was called more than once with the same ' +
365
          'socket, possibly due to a misconfiguration'
366
      );
367
    }
368

369
    if (this._state > RUNNING) return abortHandshake(socket, 503);
3,687✔
370

371
    const digest = createHash('sha1')
3,660✔
372
      .update(key + GUID)
373
      .digest('base64');
374

375
    const headers = [
3,660✔
376
      'HTTP/1.1 101 Switching Protocols',
377
      'Upgrade: websocket',
378
      'Connection: Upgrade',
379
      `Sec-WebSocket-Accept: ${digest}`
380
    ];
381

382
    const ws = new this.options.WebSocket(null);
3,660✔
383

384
    if (protocols.size) {
3,660✔
385
      //
386
      // Optionally call external protocol selection handler.
387
      //
388
      const protocol = this.options.handleProtocols
108✔
389
        ? this.options.handleProtocols(protocols, req)
390
        : protocols.values().next().value;
391

392
      if (protocol) {
108✔
393
        headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
81✔
394
        ws._protocol = protocol;
81✔
395
      }
396
    }
397

398
    if (extensions[PerMessageDeflate.extensionName]) {
3,660✔
399
      const params = extensions[PerMessageDeflate.extensionName].params;
513✔
400
      const value = extension.format({
513✔
401
        [PerMessageDeflate.extensionName]: [params]
402
      });
403
      headers.push(`Sec-WebSocket-Extensions: ${value}`);
513✔
404
      ws._extensions = extensions;
513✔
405
    }
406

407
    //
408
    // Allow external modification/inspection of handshake headers.
409
    //
410
    this.emit('headers', headers, req);
3,660✔
411

412
    socket.write(headers.concat('\r\n').join('\r\n'));
3,660✔
413
    socket.removeListener('error', socketOnError);
3,660✔
414

415
    ws.setSocket(socket, head, {
3,660✔
416
      allowSynchronousEvents: this.options.allowSynchronousEvents,
417
      maxPayload: this.options.maxPayload,
418
      skipUTF8Validation: this.options.skipUTF8Validation
419
    });
420

421
    if (this.clients) {
3,660✔
422
      this.clients.add(ws);
3,633✔
423
      ws.on('close', () => {
3,633✔
424
        this.clients.delete(ws);
3,633✔
425

426
        if (this._shouldEmitClose && !this.clients.size) {
3,633✔
427
          process.nextTick(emitClose, this);
114✔
428
        }
429
      });
430
    }
431

432
    cb(ws, req);
3,660✔
433
  }
434
}
435

436
module.exports = WebSocketServer;
27✔
437

438
/**
439
 * Add event listeners on an `EventEmitter` using a map of <event, listener>
440
 * pairs.
441
 *
442
 * @param {EventEmitter} server The event emitter
443
 * @param {Object.<String, Function>} map The listeners to add
444
 * @return {Function} A function that will remove the added listeners when
445
 *     called
446
 * @private
447
 */
448
function addListeners(server, map) {
449
  for (const event of Object.keys(map)) server.on(event, map[event]);
13,086✔
450

451
  return function removeListeners() {
4,362✔
452
    for (const event of Object.keys(map)) {
4,011✔
453
      server.removeListener(event, map[event]);
12,033✔
454
    }
455
  };
456
}
457

458
/**
459
 * Emit a `'close'` event on an `EventEmitter`.
460
 *
461
 * @param {EventEmitter} server The event emitter
462
 * @private
463
 */
464
function emitClose(server) {
465
  server._state = CLOSED;
4,173✔
466
  server.emit('close');
4,173✔
467
}
468

469
/**
470
 * Handle socket errors.
471
 *
472
 * @private
473
 */
474
function socketOnError() {
475
  this.destroy();
×
476
}
477

478
/**
479
 * Close the connection when preconditions are not fulfilled.
480
 *
481
 * @param {Duplex} socket The socket of the upgrade request
482
 * @param {Number} code The HTTP response status code
483
 * @param {String} [message] The HTTP response body
484
 * @param {Object} [headers] Additional HTTP response headers
485
 * @private
486
 */
487
function abortHandshake(socket, code, message, headers) {
488
  //
489
  // The socket is writable unless the user destroyed or ended it before calling
490
  // `server.handleUpgrade()` or in the `verifyClient` function, which is a user
491
  // error. Handling this does not make much sense as the worst that can happen
492
  // is that some of the data written by the user might be discarded due to the
493
  // call to `socket.end()` below, which triggers an `'error'` event that in
494
  // turn causes the socket to be destroyed.
495
  //
496
  message = message || http.STATUS_CODES[code];
405✔
497
  headers = {
405✔
498
    Connection: 'close',
499
    'Content-Type': 'text/html',
500
    'Content-Length': Buffer.byteLength(message),
501
    ...headers
502
  };
503

504
  socket.once('finish', socket.destroy);
405✔
505

506
  socket.end(
405✔
507
    `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` +
508
      Object.keys(headers)
509
        .map((h) => `${h}: ${headers[h]}`)
1,242✔
510
        .join('\r\n') +
511
      '\r\n\r\n' +
512
      message
513
  );
514
}
515

516
/**
517
 * Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least
518
 * one listener for it, otherwise call `abortHandshake()`.
519
 *
520
 * @param {WebSocketServer} server The WebSocket server
521
 * @param {http.IncomingMessage} req The request object
522
 * @param {Duplex} socket The socket of the upgrade request
523
 * @param {Number} code The HTTP response status code
524
 * @param {String} message The HTTP response body
525
 * @private
526
 */
527
function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) {
528
  if (server.listenerCount('wsClientError')) {
270✔
529
    const err = new Error(message);
27✔
530
    Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);
27✔
531

532
    server.emit('wsClientError', err, socket, req);
27✔
533
  } else {
534
    abortHandshake(socket, code, message);
243✔
535
  }
536
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc