'use strict'; const EventEmitter = require('events'); const crypto = require('crypto'); const http = require('http'); const url = require('url'); const PerMessageDeflate = require('./permessage-deflate'); const extension = require('./extension'); const constants = require('./constants'); const WebSocket = require('./websocket'); /** * Class representing a WebSocket server. * * @extends EventEmitter */ class WebSocketServer extends EventEmitter { /** * Create a `WebSocketServer` instance. * * @param {Object} options Configuration options * @param {String} options.host The hostname where to bind the server * @param {Number} options.port The port where to bind the server * @param {http.Server} options.server A pre-created HTTP/S server to use * @param {Function} options.verifyClient An hook to reject connections * @param {Function} options.handleProtocols An hook to handle protocols * @param {String} options.path Accept only connections matching this path * @param {Boolean} options.noServer Enable no server mode * @param {Boolean} options.clientTracking Specifies whether or not to track clients * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable permessage-deflate * @param {Number} options.maxPayload The maximum allowed message size * @param {Function} callback A listener for the `listening` event */ constructor (options, callback) { super(); options = Object.assign({ maxPayload: 100 * 1024 * 1024, perMessageDeflate: false, handleProtocols: null, clientTracking: true, verifyClient: null, noServer: false, backlog: null, // use default (511 as implemented in net.js) server: null, host: null, path: null, port: null }, options); if (options.port == null && !options.server && !options.noServer) { throw new TypeError( 'One of the "port", "server", or "noServer" options must be specified' ); } if (options.port != null) { this._server = http.createServer((req, res) => { const body = http.STATUS_CODES[426]; res.writeHead(426, { 'Content-Length': body.length, 'Content-Type': 'text/plain' }); res.end(body); }); this._server.listen(options.port, options.host, options.backlog, callback); } else if (options.server) { this._server = options.server; } if (this._server) { this._removeListeners = addListeners(this._server, { listening: this.emit.bind(this, 'listening'), error: this.emit.bind(this, 'error'), upgrade: (req, socket, head) => { this.handleUpgrade(req, socket, head, (ws) => { this.emit('connection', ws, req); }); } }); } if (options.perMessageDeflate === true) options.perMessageDeflate = {}; if (options.clientTracking) this.clients = new Set(); this.options = options; } /** * Returns the bound address, the address family name, and port of the server * as reported by the operating system if listening on an IP socket. * If the server is listening on a pipe or UNIX domain socket, the name is * returned as a string. * * @return {(Object|String|null)} The address of the server * @public */ address () { if (this.options.noServer) { throw new Error('The server is operating in "noServer" mode'); } if (!this._server) return null; return this._server.address(); } /** * Close the server. * * @param {Function} cb Callback * @public */ close (cb) { // // Terminate all associated clients. // if (this.clients) { for (const client of this.clients) client.terminate(); } const server = this._server; if (server) { this._removeListeners(); this._removeListeners = this._server = null; // // Close the http server if it was internally created. // if (this.options.port != null) return server.close(cb); } if (cb) cb(); } /** * See if a given request should be handled by this server instance. * * @param {http.IncomingMessage} req Request object to inspect * @return {Boolean} `true` if the request is valid, else `false` * @public */ shouldHandle (req) { if (this.options.path && url.parse(req.url).pathname !== this.options.path) { return false; } return true; } /** * Handle a HTTP Upgrade request. * * @param {http.IncomingMessage} req The request object * @param {net.Socket} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @public */ handleUpgrade (req, socket, head, cb) { socket.on('error', socketOnError); const version = +req.headers['sec-websocket-version']; const extensions = {}; if ( req.method !== 'GET' || req.headers.upgrade.toLowerCase() !== 'websocket' || !req.headers['sec-websocket-key'] || (version !== 8 && version !== 13) || !this.shouldHandle(req) ) { return abortHandshake(socket, 400); } if (this.options.perMessageDeflate) { const perMessageDeflate = new PerMessageDeflate( this.options.perMessageDeflate, true, this.options.maxPayload ); try { const offers = extension.parse( req.headers['sec-websocket-extensions'] ); if (offers[PerMessageDeflate.extensionName]) { perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); extensions[PerMessageDeflate.extensionName] = perMessageDeflate; } } catch (err) { return abortHandshake(socket, 400); } } // // Optionally call external client verification handler. // if (this.options.verifyClient) { const info = { origin: req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], secure: !!(req.connection.authorized || req.connection.encrypted), req }; if (this.options.verifyClient.length === 2) { this.options.verifyClient(info, (verified, code, message, headers) => { if (!verified) { return abortHandshake(socket, code || 401, message, headers); } this.completeUpgrade(extensions, req, socket, head, cb); }); return; } if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); } this.completeUpgrade(extensions, req, socket, head, cb); } /** * Upgrade the connection to WebSocket. * * @param {Object} extensions The accepted extensions * @param {http.IncomingMessage} req The request object * @param {net.Socket} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @private */ completeUpgrade (extensions, req, socket, head, cb) { // // Destroy the socket if the client has already sent a FIN packet. // if (!socket.readable || !socket.writable) return socket.destroy(); const key = crypto.createHash('sha1') .update(req.headers['sec-websocket-key'] + constants.GUID, 'binary') .digest('base64'); const headers = [ 'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${key}` ]; const ws = new WebSocket(null); var protocol = req.headers['sec-websocket-protocol']; if (protocol) { protocol = protocol.split(',').map(trim); // // Optionally call external protocol selection handler. // if (this.options.handleProtocols) { protocol = this.options.handleProtocols(protocol, req); } else { protocol = protocol[0]; } if (protocol) { headers.push(`Sec-WebSocket-Protocol: ${protocol}`); ws.protocol = protocol; } } if (extensions[PerMessageDeflate.extensionName]) { const params = extensions[PerMessageDeflate.extensionName].params; const value = extension.format({ [PerMessageDeflate.extensionName]: [params] }); headers.push(`Sec-WebSocket-Extensions: ${value}`); ws._extensions = extensions; } // // Allow external modification/inspection of handshake headers. // this.emit('headers', headers, req); socket.write(headers.concat('\r\n').join('\r\n')); socket.removeListener('error', socketOnError); ws.setSocket(socket, head, this.options.maxPayload); if (this.clients) { this.clients.add(ws); ws.on('close', () => this.clients.delete(ws)); } cb(ws); } } module.exports = WebSocketServer; /** * Add event listeners on an `EventEmitter` using a map of * pairs. * * @param {EventEmitter} server The event emitter * @param {Object.} map The listeners to add * @return {Function} A function that will remove the added listeners when called * @private */ function addListeners (server, map) { for (const event of Object.keys(map)) server.on(event, map[event]); return function removeListeners () { for (const event of Object.keys(map)) { server.removeListener(event, map[event]); } }; } /** * Handle premature socket errors. * * @private */ function socketOnError () { this.destroy(); } /** * Close the connection when preconditions are not fulfilled. * * @param {net.Socket} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} [message] The HTTP response body * @param {Object} [headers] Additional HTTP response headers * @private */ function abortHandshake (socket, code, message, headers) { if (socket.writable) { message = message || http.STATUS_CODES[code]; headers = Object.assign({ 'Connection': 'close', 'Content-type': 'text/html', 'Content-Length': Buffer.byteLength(message) }, headers); socket.write( `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + Object.keys(headers).map(h => `${h}: ${headers[h]}`).join('\r\n') + '\r\n\r\n' + message ); } socket.removeListener('error', socketOnError); socket.destroy(); } /** * Remove whitespace characters from both ends of a string. * * @param {String} str The string * @return {String} A new string representing `str` stripped of whitespace * characters from both its beginning and end * @private */ function trim(str) { return str.trim(); }