/ cloudformation-templates / node_modules / ws / lib / websocket-server.js
websocket-server.js
  1  /* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */
  2  
  3  'use strict';
  4  
  5  const EventEmitter = require('events');
  6  const http = require('http');
  7  const https = require('https');
  8  const net = require('net');
  9  const tls = require('tls');
 10  const { createHash } = require('crypto');
 11  
 12  const PerMessageDeflate = require('./permessage-deflate');
 13  const WebSocket = require('./websocket');
 14  const { format, parse } = require('./extension');
 15  const { GUID, kWebSocket } = require('./constants');
 16  
 17  const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
 18  
 19  const RUNNING = 0;
 20  const CLOSING = 1;
 21  const CLOSED = 2;
 22  
 23  /**
 24   * Class representing a WebSocket server.
 25   *
 26   * @extends EventEmitter
 27   */
 28  class WebSocketServer extends EventEmitter {
 29    /**
 30     * Create a `WebSocketServer` instance.
 31     *
 32     * @param {Object} options Configuration options
 33     * @param {Number} [options.backlog=511] The maximum length of the queue of
 34     *     pending connections
 35     * @param {Boolean} [options.clientTracking=true] Specifies whether or not to
 36     *     track clients
 37     * @param {Function} [options.handleProtocols] A hook to handle protocols
 38     * @param {String} [options.host] The hostname where to bind the server
 39     * @param {Number} [options.maxPayload=104857600] The maximum allowed message
 40     *     size
 41     * @param {Boolean} [options.noServer=false] Enable no server mode
 42     * @param {String} [options.path] Accept only connections matching this path
 43     * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable
 44     *     permessage-deflate
 45     * @param {Number} [options.port] The port where to bind the server
 46     * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S
 47     *     server to use
 48     * @param {Function} [options.verifyClient] A hook to reject connections
 49     * @param {Function} [callback] A listener for the `listening` event
 50     */
 51    constructor(options, callback) {
 52      super();
 53  
 54      options = {
 55        maxPayload: 100 * 1024 * 1024,
 56        perMessageDeflate: false,
 57        handleProtocols: null,
 58        clientTracking: true,
 59        verifyClient: null,
 60        noServer: false,
 61        backlog: null, // use default (511 as implemented in net.js)
 62        server: null,
 63        host: null,
 64        path: null,
 65        port: null,
 66        ...options
 67      };
 68  
 69      if (
 70        (options.port == null && !options.server && !options.noServer) ||
 71        (options.port != null && (options.server || options.noServer)) ||
 72        (options.server && options.noServer)
 73      ) {
 74        throw new TypeError(
 75          'One and only one of the "port", "server", or "noServer" options ' +
 76            'must be specified'
 77        );
 78      }
 79  
 80      if (options.port != null) {
 81        this._server = http.createServer((req, res) => {
 82          const body = http.STATUS_CODES[426];
 83  
 84          res.writeHead(426, {
 85            'Content-Length': body.length,
 86            'Content-Type': 'text/plain'
 87          });
 88          res.end(body);
 89        });
 90        this._server.listen(
 91          options.port,
 92          options.host,
 93          options.backlog,
 94          callback
 95        );
 96      } else if (options.server) {
 97        this._server = options.server;
 98      }
 99  
100      if (this._server) {
101        const emitConnection = this.emit.bind(this, 'connection');
102  
103        this._removeListeners = addListeners(this._server, {
104          listening: this.emit.bind(this, 'listening'),
105          error: this.emit.bind(this, 'error'),
106          upgrade: (req, socket, head) => {
107            this.handleUpgrade(req, socket, head, emitConnection);
108          }
109        });
110      }
111  
112      if (options.perMessageDeflate === true) options.perMessageDeflate = {};
113      if (options.clientTracking) this.clients = new Set();
114      this.options = options;
115      this._state = RUNNING;
116    }
117  
118    /**
119     * Returns the bound address, the address family name, and port of the server
120     * as reported by the operating system if listening on an IP socket.
121     * If the server is listening on a pipe or UNIX domain socket, the name is
122     * returned as a string.
123     *
124     * @return {(Object|String|null)} The address of the server
125     * @public
126     */
127    address() {
128      if (this.options.noServer) {
129        throw new Error('The server is operating in "noServer" mode');
130      }
131  
132      if (!this._server) return null;
133      return this._server.address();
134    }
135  
136    /**
137     * Close the server.
138     *
139     * @param {Function} [cb] Callback
140     * @public
141     */
142    close(cb) {
143      if (cb) this.once('close', cb);
144  
145      if (this._state === CLOSED) {
146        process.nextTick(emitClose, this);
147        return;
148      }
149  
150      if (this._state === CLOSING) return;
151      this._state = CLOSING;
152  
153      //
154      // Terminate all associated clients.
155      //
156      if (this.clients) {
157        for (const client of this.clients) client.terminate();
158      }
159  
160      const server = this._server;
161  
162      if (server) {
163        this._removeListeners();
164        this._removeListeners = this._server = null;
165  
166        //
167        // Close the http server if it was internally created.
168        //
169        if (this.options.port != null) {
170          server.close(emitClose.bind(undefined, this));
171          return;
172        }
173      }
174  
175      process.nextTick(emitClose, this);
176    }
177  
178    /**
179     * See if a given request should be handled by this server instance.
180     *
181     * @param {http.IncomingMessage} req Request object to inspect
182     * @return {Boolean} `true` if the request is valid, else `false`
183     * @public
184     */
185    shouldHandle(req) {
186      if (this.options.path) {
187        const index = req.url.indexOf('?');
188        const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
189  
190        if (pathname !== this.options.path) return false;
191      }
192  
193      return true;
194    }
195  
196    /**
197     * Handle a HTTP Upgrade request.
198     *
199     * @param {http.IncomingMessage} req The request object
200     * @param {(net.Socket|tls.Socket)} socket The network socket between the
201     *     server and client
202     * @param {Buffer} head The first packet of the upgraded stream
203     * @param {Function} cb Callback
204     * @public
205     */
206    handleUpgrade(req, socket, head, cb) {
207      socket.on('error', socketOnError);
208  
209      const key =
210        req.headers['sec-websocket-key'] !== undefined
211          ? req.headers['sec-websocket-key'].trim()
212          : false;
213      const version = +req.headers['sec-websocket-version'];
214      const extensions = {};
215  
216      if (
217        req.method !== 'GET' ||
218        req.headers.upgrade.toLowerCase() !== 'websocket' ||
219        !key ||
220        !keyRegex.test(key) ||
221        (version !== 8 && version !== 13) ||
222        !this.shouldHandle(req)
223      ) {
224        return abortHandshake(socket, 400);
225      }
226  
227      if (this.options.perMessageDeflate) {
228        const perMessageDeflate = new PerMessageDeflate(
229          this.options.perMessageDeflate,
230          true,
231          this.options.maxPayload
232        );
233  
234        try {
235          const offers = parse(req.headers['sec-websocket-extensions']);
236  
237          if (offers[PerMessageDeflate.extensionName]) {
238            perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
239            extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
240          }
241        } catch (err) {
242          return abortHandshake(socket, 400);
243        }
244      }
245  
246      //
247      // Optionally call external client verification handler.
248      //
249      if (this.options.verifyClient) {
250        const info = {
251          origin:
252            req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
253          secure: !!(req.socket.authorized || req.socket.encrypted),
254          req
255        };
256  
257        if (this.options.verifyClient.length === 2) {
258          this.options.verifyClient(info, (verified, code, message, headers) => {
259            if (!verified) {
260              return abortHandshake(socket, code || 401, message, headers);
261            }
262  
263            this.completeUpgrade(key, extensions, req, socket, head, cb);
264          });
265          return;
266        }
267  
268        if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
269      }
270  
271      this.completeUpgrade(key, extensions, req, socket, head, cb);
272    }
273  
274    /**
275     * Upgrade the connection to WebSocket.
276     *
277     * @param {String} key The value of the `Sec-WebSocket-Key` header
278     * @param {Object} extensions The accepted extensions
279     * @param {http.IncomingMessage} req The request object
280     * @param {(net.Socket|tls.Socket)} socket The network socket between the
281     *     server and client
282     * @param {Buffer} head The first packet of the upgraded stream
283     * @param {Function} cb Callback
284     * @throws {Error} If called more than once with the same socket
285     * @private
286     */
287    completeUpgrade(key, extensions, req, socket, head, cb) {
288      //
289      // Destroy the socket if the client has already sent a FIN packet.
290      //
291      if (!socket.readable || !socket.writable) return socket.destroy();
292  
293      if (socket[kWebSocket]) {
294        throw new Error(
295          'server.handleUpgrade() was called more than once with the same ' +
296            'socket, possibly due to a misconfiguration'
297        );
298      }
299  
300      if (this._state > RUNNING) return abortHandshake(socket, 503);
301  
302      const digest = createHash('sha1')
303        .update(key + GUID)
304        .digest('base64');
305  
306      const headers = [
307        'HTTP/1.1 101 Switching Protocols',
308        'Upgrade: websocket',
309        'Connection: Upgrade',
310        `Sec-WebSocket-Accept: ${digest}`
311      ];
312  
313      const ws = new WebSocket(null);
314      let protocol = req.headers['sec-websocket-protocol'];
315  
316      if (protocol) {
317        protocol = protocol.split(',').map(trim);
318  
319        //
320        // Optionally call external protocol selection handler.
321        //
322        if (this.options.handleProtocols) {
323          protocol = this.options.handleProtocols(protocol, req);
324        } else {
325          protocol = protocol[0];
326        }
327  
328        if (protocol) {
329          headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
330          ws._protocol = protocol;
331        }
332      }
333  
334      if (extensions[PerMessageDeflate.extensionName]) {
335        const params = extensions[PerMessageDeflate.extensionName].params;
336        const value = format({
337          [PerMessageDeflate.extensionName]: [params]
338        });
339        headers.push(`Sec-WebSocket-Extensions: ${value}`);
340        ws._extensions = extensions;
341      }
342  
343      //
344      // Allow external modification/inspection of handshake headers.
345      //
346      this.emit('headers', headers, req);
347  
348      socket.write(headers.concat('\r\n').join('\r\n'));
349      socket.removeListener('error', socketOnError);
350  
351      ws.setSocket(socket, head, this.options.maxPayload);
352  
353      if (this.clients) {
354        this.clients.add(ws);
355        ws.on('close', () => this.clients.delete(ws));
356      }
357  
358      cb(ws, req);
359    }
360  }
361  
362  module.exports = WebSocketServer;
363  
364  /**
365   * Add event listeners on an `EventEmitter` using a map of <event, listener>
366   * pairs.
367   *
368   * @param {EventEmitter} server The event emitter
369   * @param {Object.<String, Function>} map The listeners to add
370   * @return {Function} A function that will remove the added listeners when
371   *     called
372   * @private
373   */
374  function addListeners(server, map) {
375    for (const event of Object.keys(map)) server.on(event, map[event]);
376  
377    return function removeListeners() {
378      for (const event of Object.keys(map)) {
379        server.removeListener(event, map[event]);
380      }
381    };
382  }
383  
384  /**
385   * Emit a `'close'` event on an `EventEmitter`.
386   *
387   * @param {EventEmitter} server The event emitter
388   * @private
389   */
390  function emitClose(server) {
391    server._state = CLOSED;
392    server.emit('close');
393  }
394  
395  /**
396   * Handle premature socket errors.
397   *
398   * @private
399   */
400  function socketOnError() {
401    this.destroy();
402  }
403  
404  /**
405   * Close the connection when preconditions are not fulfilled.
406   *
407   * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request
408   * @param {Number} code The HTTP response status code
409   * @param {String} [message] The HTTP response body
410   * @param {Object} [headers] Additional HTTP response headers
411   * @private
412   */
413  function abortHandshake(socket, code, message, headers) {
414    if (socket.writable) {
415      message = message || http.STATUS_CODES[code];
416      headers = {
417        Connection: 'close',
418        'Content-Type': 'text/html',
419        'Content-Length': Buffer.byteLength(message),
420        ...headers
421      };
422  
423      socket.write(
424        `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` +
425          Object.keys(headers)
426            .map((h) => `${h}: ${headers[h]}`)
427            .join('\r\n') +
428          '\r\n\r\n' +
429          message
430      );
431    }
432  
433    socket.removeListener('error', socketOnError);
434    socket.destroy();
435  }
436  
437  /**
438   * Remove whitespace characters from both ends of a string.
439   *
440   * @param {String} str The string
441   * @return {String} A new string representing `str` stripped of whitespace
442   *     characters from both its beginning and end
443   * @private
444   */
445  function trim(str) {
446    return str.trim();
447  }