twitst4tz

twitter statistics web application
Log | Files | Refs | README | LICENSE

websocket-server.js (10875B)


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