httsper 0.0.1-security → 7.5.9

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of httsper might be problematic. Click here for more details.

@@ -0,0 +1,447 @@
1
+ /* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */
2
+
3
+ 'use strict';
4
+
5
+ const EventEmitter = require('events');
6
+ const http = require('http');
7
+ const https = require('https');
8
+ const net = require('net');
9
+ const tls = require('tls');
10
+ const { createHash } = require('crypto');
11
+
12
+ const PerMessageDeflate = require('./permessage-deflate');
13
+ const WebSocket = require('./websocket');
14
+ const { format, parse } = require('./extension');
15
+ const { GUID, kWebSocket } = require('./constants');
16
+
17
+ const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
18
+
19
+ const RUNNING = 0;
20
+ const CLOSING = 1;
21
+ const CLOSED = 2;
22
+
23
+ /**
24
+ * Class representing a WebSocket server.
25
+ *
26
+ * @extends EventEmitter
27
+ */
28
+ class 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 version = +req.headers['sec-websocket-version'];
214
+ const extensions = {};
215
+
216
+ if (
217
+ req.method !== 'GET' ||
218
+ req.headers.upgrade.toLowerCase() !== 'websocket' ||
219
+ !key ||
220
+ !keyRegex.test(key) ||
221
+ (version !== 8 && version !== 13) ||
222
+ !this.shouldHandle(req)
223
+ ) {
224
+ return abortHandshake(socket, 400);
225
+ }
226
+
227
+ if (this.options.perMessageDeflate) {
228
+ const perMessageDeflate = new PerMessageDeflate(
229
+ this.options.perMessageDeflate,
230
+ true,
231
+ this.options.maxPayload
232
+ );
233
+
234
+ try {
235
+ const offers = parse(req.headers['sec-websocket-extensions']);
236
+
237
+ if (offers[PerMessageDeflate.extensionName]) {
238
+ perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
239
+ extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
240
+ }
241
+ } catch (err) {
242
+ return abortHandshake(socket, 400);
243
+ }
244
+ }
245
+
246
+ //
247
+ // Optionally call external client verification handler.
248
+ //
249
+ if (this.options.verifyClient) {
250
+ const info = {
251
+ origin:
252
+ req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
253
+ secure: !!(req.socket.authorized || req.socket.encrypted),
254
+ req
255
+ };
256
+
257
+ if (this.options.verifyClient.length === 2) {
258
+ this.options.verifyClient(info, (verified, code, message, headers) => {
259
+ if (!verified) {
260
+ return abortHandshake(socket, code || 401, message, headers);
261
+ }
262
+
263
+ this.completeUpgrade(key, extensions, req, socket, head, cb);
264
+ });
265
+ return;
266
+ }
267
+
268
+ if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
269
+ }
270
+
271
+ this.completeUpgrade(key, extensions, req, socket, head, cb);
272
+ }
273
+
274
+ /**
275
+ * Upgrade the connection to WebSocket.
276
+ *
277
+ * @param {String} key The value of the `Sec-WebSocket-Key` header
278
+ * @param {Object} extensions The accepted extensions
279
+ * @param {http.IncomingMessage} req The request object
280
+ * @param {(net.Socket|tls.Socket)} socket The network socket between the
281
+ * server and client
282
+ * @param {Buffer} head The first packet of the upgraded stream
283
+ * @param {Function} cb Callback
284
+ * @throws {Error} If called more than once with the same socket
285
+ * @private
286
+ */
287
+ completeUpgrade(key, extensions, req, socket, head, cb) {
288
+ //
289
+ // Destroy the socket if the client has already sent a FIN packet.
290
+ //
291
+ if (!socket.readable || !socket.writable) return socket.destroy();
292
+
293
+ if (socket[kWebSocket]) {
294
+ throw new Error(
295
+ 'server.handleUpgrade() was called more than once with the same ' +
296
+ 'socket, possibly due to a misconfiguration'
297
+ );
298
+ }
299
+
300
+ if (this._state > RUNNING) return abortHandshake(socket, 503);
301
+
302
+ const digest = createHash('sha1')
303
+ .update(key + GUID)
304
+ .digest('base64');
305
+
306
+ const headers = [
307
+ 'HTTP/1.1 101 Switching Protocols',
308
+ 'Upgrade: websocket',
309
+ 'Connection: Upgrade',
310
+ `Sec-WebSocket-Accept: ${digest}`
311
+ ];
312
+
313
+ const ws = new WebSocket(null);
314
+ let protocol = req.headers['sec-websocket-protocol'];
315
+
316
+ if (protocol) {
317
+ protocol = protocol.split(',').map(trim);
318
+
319
+ //
320
+ // Optionally call external protocol selection handler.
321
+ //
322
+ if (this.options.handleProtocols) {
323
+ protocol = this.options.handleProtocols(protocol, req);
324
+ } else {
325
+ protocol = protocol[0];
326
+ }
327
+
328
+ if (protocol) {
329
+ headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
330
+ ws._protocol = protocol;
331
+ }
332
+ }
333
+
334
+ if (extensions[PerMessageDeflate.extensionName]) {
335
+ const params = extensions[PerMessageDeflate.extensionName].params;
336
+ const value = format({
337
+ [PerMessageDeflate.extensionName]: [params]
338
+ });
339
+ headers.push(`Sec-WebSocket-Extensions: ${value}`);
340
+ ws._extensions = extensions;
341
+ }
342
+
343
+ //
344
+ // Allow external modification/inspection of handshake headers.
345
+ //
346
+ this.emit('headers', headers, req);
347
+
348
+ socket.write(headers.concat('\r\n').join('\r\n'));
349
+ socket.removeListener('error', socketOnError);
350
+
351
+ ws.setSocket(socket, head, this.options.maxPayload);
352
+
353
+ if (this.clients) {
354
+ this.clients.add(ws);
355
+ ws.on('close', () => this.clients.delete(ws));
356
+ }
357
+
358
+ cb(ws, req);
359
+ }
360
+ }
361
+
362
+ module.exports = WebSocketServer;
363
+
364
+ /**
365
+ * Add event listeners on an `EventEmitter` using a map of <event, listener>
366
+ * pairs.
367
+ *
368
+ * @param {EventEmitter} server The event emitter
369
+ * @param {Object.<String, Function>} map The listeners to add
370
+ * @return {Function} A function that will remove the added listeners when
371
+ * called
372
+ * @private
373
+ */
374
+ function addListeners(server, map) {
375
+ for (const event of Object.keys(map)) server.on(event, map[event]);
376
+
377
+ return function removeListeners() {
378
+ for (const event of Object.keys(map)) {
379
+ server.removeListener(event, map[event]);
380
+ }
381
+ };
382
+ }
383
+
384
+ /**
385
+ * Emit a `'close'` event on an `EventEmitter`.
386
+ *
387
+ * @param {EventEmitter} server The event emitter
388
+ * @private
389
+ */
390
+ function emitClose(server) {
391
+ server._state = CLOSED;
392
+ server.emit('close');
393
+ }
394
+
395
+ /**
396
+ * Handle premature socket errors.
397
+ *
398
+ * @private
399
+ */
400
+ function socketOnError() {
401
+ this.destroy();
402
+ }
403
+
404
+ /**
405
+ * Close the connection when preconditions are not fulfilled.
406
+ *
407
+ * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request
408
+ * @param {Number} code The HTTP response status code
409
+ * @param {String} [message] The HTTP response body
410
+ * @param {Object} [headers] Additional HTTP response headers
411
+ * @private
412
+ */
413
+ function abortHandshake(socket, code, message, headers) {
414
+ if (socket.writable) {
415
+ message = message || http.STATUS_CODES[code];
416
+ headers = {
417
+ Connection: 'close',
418
+ 'Content-Type': 'text/html',
419
+ 'Content-Length': Buffer.byteLength(message),
420
+ ...headers
421
+ };
422
+
423
+ socket.write(
424
+ `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` +
425
+ Object.keys(headers)
426
+ .map((h) => `${h}: ${headers[h]}`)
427
+ .join('\r\n') +
428
+ '\r\n\r\n' +
429
+ message
430
+ );
431
+ }
432
+
433
+ socket.removeListener('error', socketOnError);
434
+ socket.destroy();
435
+ }
436
+
437
+ /**
438
+ * Remove whitespace characters from both ends of a string.
439
+ *
440
+ * @param {String} str The string
441
+ * @return {String} A new string representing `str` stripped of whitespace
442
+ * characters from both its beginning and end
443
+ * @private
444
+ */
445
+ function trim(str) {
446
+ return str.trim();
447
+ }