buddy

node MVC discord bot
Log | Files | Refs | README

websocket-server.js (11398B)


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