source: frontend/node_modules/ws/lib/websocket-server.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

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