| /*! |
| * ws: a node.js websocket client |
| * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com> |
| * MIT Licensed |
| */ |
| |
| 'use strict'; |
| |
| const safeBuffer = require('safe-buffer'); |
| const EventEmitter = require('events'); |
| const crypto = require('crypto'); |
| const Ultron = require('ultron'); |
| const http = require('http'); |
| const url = require('url'); |
| |
| const PerMessageDeflate = require('./PerMessageDeflate'); |
| const Extensions = require('./Extensions'); |
| const constants = require('./Constants'); |
| const WebSocket = require('./WebSocket'); |
| |
| const Buffer = safeBuffer.Buffer; |
| |
| /** |
| * 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('missing or invalid options'); |
| } |
| |
| 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._ultron = new Ultron(this._server); |
| this._ultron.on('listening', () => this.emit('listening')); |
| this._ultron.on('error', (err) => this.emit('error', err)); |
| this._ultron.on('upgrade', (req, socket, head) => { |
| this.handleUpgrade(req, socket, head, (client) => { |
| this.emit('connection', client, req); |
| }); |
| }); |
| } |
| |
| if (options.perMessageDeflate === true) options.perMessageDeflate = {}; |
| if (options.clientTracking) this.clients = new Set(); |
| this.options = options; |
| } |
| |
| /** |
| * 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._ultron.destroy(); |
| this._ultron = 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', socketError); |
| |
| 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 abortConnection(socket, 400); |
| } |
| |
| if (this.options.perMessageDeflate) { |
| const perMessageDeflate = new PerMessageDeflate( |
| this.options.perMessageDeflate, |
| true, |
| this.options.maxPayload |
| ); |
| |
| try { |
| const offers = Extensions.parse( |
| req.headers['sec-websocket-extensions'] |
| ); |
| |
| if (offers[PerMessageDeflate.extensionName]) { |
| perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); |
| extensions[PerMessageDeflate.extensionName] = perMessageDeflate; |
| } |
| } catch (err) { |
| return abortConnection(socket, 400); |
| } |
| } |
| |
| var protocol = (req.headers['sec-websocket-protocol'] || '').split(/, */); |
| |
| // |
| // Optionally call external protocol selection handler. |
| // |
| if (this.options.handleProtocols) { |
| protocol = this.options.handleProtocols(protocol, req); |
| if (protocol === false) return abortConnection(socket, 401); |
| } else { |
| protocol = protocol[0]; |
| } |
| |
| // |
| // 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) => { |
| if (!verified) return abortConnection(socket, code || 401, message); |
| |
| this.completeUpgrade( |
| protocol, |
| extensions, |
| version, |
| req, |
| socket, |
| head, |
| cb |
| ); |
| }); |
| return; |
| } |
| |
| if (!this.options.verifyClient(info)) return abortConnection(socket, 401); |
| } |
| |
| this.completeUpgrade(protocol, extensions, version, req, socket, head, cb); |
| } |
| |
| /** |
| * Upgrade the connection to WebSocket. |
| * |
| * @param {String} protocol The chosen subprotocol |
| * @param {Object} extensions The accepted extensions |
| * @param {Number} version The WebSocket protocol version |
| * @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 (protocol, extensions, version, 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}` |
| ]; |
| |
| if (protocol) headers.push(`Sec-WebSocket-Protocol: ${protocol}`); |
| if (extensions[PerMessageDeflate.extensionName]) { |
| const params = extensions[PerMessageDeflate.extensionName].params; |
| const value = Extensions.format({ |
| [PerMessageDeflate.extensionName]: [params] |
| }); |
| headers.push(`Sec-WebSocket-Extensions: ${value}`); |
| } |
| |
| // |
| // Allow external modification/inspection of handshake headers. |
| // |
| this.emit('headers', headers, req); |
| |
| socket.write(headers.concat('\r\n').join('\r\n')); |
| |
| const client = new WebSocket([socket, head], null, { |
| maxPayload: this.options.maxPayload, |
| protocolVersion: version, |
| extensions, |
| protocol |
| }); |
| |
| if (this.clients) { |
| this.clients.add(client); |
| client.on('close', () => this.clients.delete(client)); |
| } |
| |
| socket.removeListener('error', socketError); |
| cb(client); |
| } |
| } |
| |
| module.exports = WebSocketServer; |
| |
| /** |
| * Handle premature socket errors. |
| * |
| * @private |
| */ |
| function socketError () { |
| 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 |
| * @private |
| */ |
| function abortConnection (socket, code, message) { |
| if (socket.writable) { |
| message = message || http.STATUS_CODES[code]; |
| socket.write( |
| `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + |
| 'Connection: close\r\n' + |
| 'Content-type: text/html\r\n' + |
| `Content-Length: ${Buffer.byteLength(message)}\r\n` + |
| '\r\n' + |
| message |
| ); |
| } |
| |
| socket.removeListener('error', socketError); |
| socket.destroy(); |
| } |