3 const EventEmitter = require('events');
4 const crypto = require('crypto');
5 const http = require('http');
7 const PerMessageDeflate = require('./permessage-deflate');
8 const extension = require('./extension');
9 const constants = require('./constants');
10 const WebSocket = require('./websocket');
13 * Class representing a WebSocket server.
15 * @extends EventEmitter
17 class WebSocketServer extends EventEmitter {
19 * Create a `WebSocketServer` instance.
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
34 constructor(options, callback) {
37 options = Object.assign(
39 maxPayload: 100 * 1024 * 1024,
40 perMessageDeflate: false,
41 handleProtocols: null,
45 backlog: null, // use default (511 as implemented in net.js)
54 if (options.port == null && !options.server && !options.noServer) {
56 'One of the "port", "server", or "noServer" options must be specified'
60 if (options.port != null) {
61 this._server = http.createServer((req, res) => {
62 const body = http.STATUS_CODES[426];
65 'Content-Length': body.length,
66 'Content-Type': 'text/plain'
76 } else if (options.server) {
77 this._server = options.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);
92 if (options.perMessageDeflate === true) options.perMessageDeflate = {};
93 if (options.clientTracking) this.clients = new Set();
94 this.options = options;
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.
103 * @return {(Object|String|null)} The address of the server
107 if (this.options.noServer) {
108 throw new Error('The server is operating in "noServer" mode');
111 if (!this._server) return null;
112 return this._server.address();
118 * @param {Function} cb Callback
122 if (cb) this.once('close', cb);
125 // Terminate all associated clients.
128 for (const client of this.clients) client.terminate();
131 const server = this._server;
134 this._removeListeners();
135 this._removeListeners = this._server = null;
138 // Close the http server if it was internally created.
140 if (this.options.port != null) {
141 server.close(() => this.emit('close'));
146 process.nextTick(emitClose, this);
150 * See if a given request should be handled by this server instance.
152 * @param {http.IncomingMessage} req Request object to inspect
153 * @return {Boolean} `true` if the request is valid, else `false`
157 if (this.options.path) {
158 const index = req.url.indexOf('?');
159 const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
161 if (pathname !== this.options.path) return false;
168 * Handle a HTTP Upgrade request.
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
176 handleUpgrade(req, socket, head, cb) {
177 socket.on('error', socketOnError);
179 const version = +req.headers['sec-websocket-version'];
180 const extensions = {};
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)
189 return abortHandshake(socket, 400);
192 if (this.options.perMessageDeflate) {
193 const perMessageDeflate = new PerMessageDeflate(
194 this.options.perMessageDeflate,
196 this.options.maxPayload
200 const offers = extension.parse(req.headers['sec-websocket-extensions']);
202 if (offers[PerMessageDeflate.extensionName]) {
203 perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
204 extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
207 return abortHandshake(socket, 400);
212 // Optionally call external client verification handler.
214 if (this.options.verifyClient) {
217 req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
218 secure: !!(req.connection.authorized || req.connection.encrypted),
222 if (this.options.verifyClient.length === 2) {
223 this.options.verifyClient(info, (verified, code, message, headers) => {
225 return abortHandshake(socket, code || 401, message, headers);
228 this.completeUpgrade(extensions, req, socket, head, cb);
233 if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
236 this.completeUpgrade(extensions, req, socket, head, cb);
240 * Upgrade the connection to WebSocket.
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
249 completeUpgrade(extensions, req, socket, head, cb) {
251 // Destroy the socket if the client has already sent a FIN packet.
253 if (!socket.readable || !socket.writable) return socket.destroy();
257 .update(req.headers['sec-websocket-key'] + constants.GUID, 'binary')
261 'HTTP/1.1 101 Switching Protocols',
262 'Upgrade: websocket',
263 'Connection: Upgrade',
264 `Sec-WebSocket-Accept: ${key}`
267 const ws = new WebSocket(null);
268 var protocol = req.headers['sec-websocket-protocol'];
271 protocol = protocol.trim().split(/ *, */);
274 // Optionally call external protocol selection handler.
276 if (this.options.handleProtocols) {
277 protocol = this.options.handleProtocols(protocol, req);
279 protocol = protocol[0];
283 headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
284 ws.protocol = protocol;
288 if (extensions[PerMessageDeflate.extensionName]) {
289 const params = extensions[PerMessageDeflate.extensionName].params;
290 const value = extension.format({
291 [PerMessageDeflate.extensionName]: [params]
293 headers.push(`Sec-WebSocket-Extensions: ${value}`);
294 ws._extensions = extensions;
298 // Allow external modification/inspection of handshake headers.
300 this.emit('headers', headers, req);
302 socket.write(headers.concat('\r\n').join('\r\n'));
303 socket.removeListener('error', socketOnError);
305 ws.setSocket(socket, head, this.options.maxPayload);
308 this.clients.add(ws);
309 ws.on('close', () => this.clients.delete(ws));
316 module.exports = WebSocketServer;
319 * Add event listeners on an `EventEmitter` using a map of <event, listener>
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
327 function addListeners(server, map) {
328 for (const event of Object.keys(map)) server.on(event, map[event]);
330 return function removeListeners() {
331 for (const event of Object.keys(map)) {
332 server.removeListener(event, map[event]);
338 * Emit a `'close'` event on an `EventEmitter`.
340 * @param {EventEmitter} server The event emitter
343 function emitClose(server) {
344 server.emit('close');
348 * Handle premature socket errors.
352 function socketOnError() {
357 * Close the connection when preconditions are not fulfilled.
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
365 function abortHandshake(socket, code, message, headers) {
366 if (socket.writable) {
367 message = message || http.STATUS_CODES[code];
368 headers = Object.assign(
371 'Content-type': 'text/html',
372 'Content-Length': Buffer.byteLength(message)
378 `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` +
380 .map((h) => `${h}: ${headers[h]}`)
387 socket.removeListener('error', socketOnError);